diff --git a/crates/svg/src/tree/document.rs b/crates/svg/src/tree/document.rs index 4474ba08a..f77d5a8c0 100644 --- a/crates/svg/src/tree/document.rs +++ b/crates/svg/src/tree/document.rs @@ -1,13 +1,15 @@ //! A format-neutral vector scene made from a bounded SVG tree. //! -//! The scene preserves paths, groups, paints, transforms, opacity, strokes and image omissions +//! The scene preserves paths, groups, paints, transforms, opacity, strokes and image sources //! without depending on an application's document model. Coordinates and stroke widths use the //! SVG tree's CSS-pixel coordinate system. +#[cfg(feature = "tree-render")] +use super::draw::Refusal; use super::usvg; /// A parsed SVG scene. Text is represented by its outlined paths. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct Document { pub width: f64, pub height: f64, @@ -16,12 +18,33 @@ pub struct Document { pub images: usize, /// True if nodes deeper than the requested depth were omitted. pub too_deep: bool, + source: Option>, +} + +impl PartialEq for Document { + fn eq(&self, other: &Self) -> bool { + self.width == other.width + && self.height == other.height + && self.nodes == other.nodes + && self.images == other.images + && self.too_deep == other.too_deep + } +} + +/// Stable node indices from the root of the source usvg tree. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NodePath(Vec); + +impl NodePath { + pub fn indices(&self) -> &[usize] { + &self.0 + } } /// A group or path in document order. #[derive(Clone, Debug, PartialEq)] pub enum Node { - Group { children: Vec, opacity: f32, transform: [f64; 6], bounds: [f64; 4], features: GroupFeatures }, + Group { children: Vec, opacity: f32, transform: [f64; 6], bounds: [f64; 4], features: GroupFeatures, source_path: NodePath }, Path(Box), Image(Image), } @@ -45,6 +68,7 @@ pub struct Image { pub bounds: [f64; 4], pub encoded: Option>>, pub svg: Option>, + pub source_path: NodePath, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -62,6 +86,7 @@ pub struct Path { pub segments: Vec, pub fill: Option, pub stroke: Option, + pub source_path: NodePath, } /// A path segment in CSS pixels after its absolute transform. @@ -111,6 +136,7 @@ pub struct Stroke { pub join: LineJoin, pub miter_limit: f64, pub dash_array: Option>, + pub dash_offset: f64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -132,9 +158,53 @@ impl Document { pub fn from_tree(tree: &usvg::Tree, max_depth: usize) -> Self { let size = tree.size(); let mut document = Self { width: f64::from(size.width()), height: f64::from(size.height()), ..Self::default() }; - document.nodes = convert_group(tree.root(), 0, max_depth, &mut document); + document.nodes = convert_group(tree.root(), 0, max_depth, &mut document, &[]); document } + + /// Build a scene that retains its source tree for fallback rendering. + pub fn from_owned_tree(tree: usvg::Tree, max_depth: usize) -> Self { + let size = tree.size(); + let mut document = Self { width: f64::from(size.width()), height: f64::from(size.height()), ..Self::default() }; + document.nodes = convert_group(tree.root(), 0, max_depth, &mut document, &[]); + document.source = Some(Box::new(tree)); + document + } + + /// Draw a source subtree named by a [`NodePath`] without exposing usvg traversal to callers. + #[cfg(feature = "tree-render")] + pub fn draw_subtree( + &self, + path: &NodePath, + transform: usvg::Transform, + target: &mut super::draw::tiny_skia::Pixmap, + held_bytes: u64, + budget_bytes: u64, + ) -> Result { + let tree = self.source.as_deref().ok_or_else(|| Refusal { + complex: true, + memory: false, + message: "the SVG scene does not retain its source tree".into(), + })?; + let mut group = tree.root(); + let mut node = None; + for (i, index) in path.0.iter().enumerate() { + node = Some(group.children().get(*index).ok_or_else(|| Refusal { + complex: true, + memory: false, + message: "the SVG subtree path is no longer valid".into(), + })?); + if i + 1 < path.0.len() { + group = match node { + Some(usvg::Node::Group(g)) => g, + Some(usvg::Node::Text(t)) => t.flattened(), + _ => return Err(Refusal { complex: true, memory: false, message: "the SVG subtree path does not name a group".into() }), + }; + } + } + let node = node.ok_or_else(|| Refusal { complex: true, memory: false, message: "the SVG subtree path is empty".into() })?; + super::draw::render_node(node, transform, target, held_bytes, budget_bytes) + } } fn transform(t: usvg::Transform) -> [f64; 6] { @@ -201,9 +271,11 @@ fn point(transform: usvg::Transform, p: usvg::tiny_skia_path::Point) -> [f64; 2] [f64::from(p.x), f64::from(p.y)] } -fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut Document) -> Vec { +fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut Document, parent_path: &[usize]) -> Vec { let mut result = Vec::new(); - for node in group.children() { + for (index, node) in group.children().iter().enumerate() { + let mut source_path = parent_path.to_vec(); + source_path.push(index); match node { usvg::Node::Path(path) => { if !path.is_visible() || path.data().is_empty() { @@ -242,16 +314,17 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut }, miter_limit: f64::from(s.miterlimit().get()), dash_array: s.dasharray().map(|d| d.iter().map(|v| f64::from(*v) * scale).collect()), + dash_offset: f64::from(s.dashoffset()) * scale, } }); - result.push(Node::Path(Box::new(Path { segments, fill, stroke }))); + result.push(Node::Path(Box::new(Path { segments, fill, stroke, source_path: NodePath(source_path) }))); } usvg::Node::Group(child) => { if depth >= max_depth { doc.too_deep = true; continue; } - let children = convert_group(child, depth + 1, max_depth, doc); + let children = convert_group(child, depth + 1, max_depth, doc, &source_path); if !children.is_empty() { result.push(Node::Group { children, @@ -265,6 +338,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut non_normal_blend: child.blend_mode() != usvg::BlendMode::Normal, isolation: child.should_isolate(), }, + source_path: NodePath(source_path), }); } } @@ -273,7 +347,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut doc.too_deep = true; continue; } - let children = convert_group(text.flattened(), depth + 1, max_depth, doc); + let children = convert_group(text.flattened(), depth + 1, max_depth, doc, &source_path); if !children.is_empty() { result.push(Node::Group { children, @@ -281,6 +355,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut transform: transform(text.abs_transform()), bounds: rect(text.abs_bounding_box()), features: GroupFeatures::default(), + source_path: NodePath(source_path), }); } } @@ -297,7 +372,9 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut usvg::ImageKind::JPEG(data) | usvg::ImageKind::PNG(data) | usvg::ImageKind::GIF(data) | usvg::ImageKind::WEBP(data) => { (Some(data.clone()), None) } - usvg::ImageKind::SVG(tree) => (None, Some(Box::new(Document::from_tree(tree, max_depth.saturating_sub(depth + 1))))), + usvg::ImageKind::SVG(tree) => { + (None, Some(Box::new(Document::from_owned_tree(tree.clone(), max_depth.saturating_sub(depth + 1))))) + } }; result.push(Node::Image(Image { kind, @@ -306,6 +383,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut bounds: rect(image.abs_bounding_box()), encoded, svg, + source_path: NodePath(source_path), })); } }