M13.2: PDF / Illustrator (.ai) / EPS vector footage (effectcraft-pdf): continuous rasterisation, layered composition import, Create Shapes from Vector Layer; SVG render tree clip groups

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 04:12:21 -04:00
co-authored by Claude Opus 5.5
parent 30a92ba82f
commit da0ec526f3
29 changed files with 3875 additions and 28 deletions
+9 -6
View File
@@ -170,6 +170,9 @@ pub struct Group {
pub transform: Affine,
pub opacity: f64,
pub children: Vec<Node>,
/// Clipping paths in the group's space (after `transform`); the children show only where
/// every one of them covers (PDF / EPS clipping; SVG `clipPath` is not read yet).
pub clip: Vec<(BezPath, FillRule)>,
}
#[derive(Clone, Debug, PartialEq)]
@@ -666,7 +669,7 @@ impl<'a, 'i> Parser<'a, 'i> {
for c in n.children() {
self.node(c, &st, &mut children, depth + 1);
}
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children }));
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children, clip: vec![] }));
}
"svg" => {
// Nested viewport.
@@ -685,7 +688,7 @@ impl<'a, 'i> Parser<'a, 'i> {
self.node(c, &st, &mut children, depth + 1);
}
(self.vw, self.vh) = saved;
out.push(Node::Group(Group { name: Self::element_name(n), transform: Affine::translate((x, y)) * vb, opacity, children }));
out.push(Node::Group(Group { name: Self::element_name(n), transform: Affine::translate((x, y)) * vb, opacity, children, clip: vec![] }));
}
"use" => {
let Some(target) = self.href(n) else { return };
@@ -706,11 +709,11 @@ impl<'a, 'i> Parser<'a, 'i> {
for c in target.children() {
self.node(c, &sst, &mut inner, depth + 1);
}
children.push(Node::Group(Group { name: Self::element_name(target), transform: vb, opacity: o, children: inner }));
children.push(Node::Group(Group { name: Self::element_name(target), transform: vb, opacity: o, children: inner, clip: vec![] }));
} else {
self.node(target, &st, &mut children, depth + 1);
}
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children }));
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children, clip: vec![] }));
}
"path" | "rect" | "circle" | "ellipse" | "line" | "polyline" | "polygon" => {
if !st.visible {
@@ -750,7 +753,7 @@ impl<'a, 'i> Parser<'a, 'i> {
self.node(c, &st, &mut children, depth + 1);
}
if !children.is_empty() {
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children }));
out.push(Node::Group(Group { name: Self::element_name(n), transform, opacity, children, clip: vec![] }));
}
}
}
@@ -942,7 +945,7 @@ pub fn parse(bytes: &[u8]) -> Result<Doc, Error> {
p.node(c, &rst, &mut children, 1);
}
let transform = viewbox_transform(root, dw, dh);
Ok(Doc { width: dw, height: dh, root: Group { name: "svg".into(), transform, opacity, children }, skipped: std::mem::take(&mut p.skipped) })
Ok(Doc { width: dw, height: dh, root: Group { name: "svg".into(), transform, opacity, children, clip: vec![] }, skipped: std::mem::take(&mut p.skipped) })
}
#[cfg(test)]
+21
View File
@@ -31,6 +31,27 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
if o <= 0.0 {
return;
}
if !g.clip.is_empty() {
// Draw the children on their own, keep what every clip path covers, composite.
let mut tmp = Image::new(dst.width, dst.height);
for c in &g.children {
draw_node(c, m, 1.0, &mut tmp);
}
for (path, rule) in &g.clip {
let rule = match rule {
FillRule::NonZero => effectcraft_path::FillRule::NonZero,
FillRule::EvenOdd => effectcraft_path::FillRule::EvenOdd,
};
let cov = effectcraft_path::fill_coverage(std::slice::from_ref(path), &mat3(m), dst.width, dst.height, rule);
tmp.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(p, c)| {
for v in p.iter_mut() {
*v *= c;
}
});
}
over(dst, &tmp, o as f32);
return;
}
if g.opacity < 1.0 && g.children.len() > 1 {
let mut tmp = Image::new(dst.width, dst.height);
for c in &g.children {