From c44b9c609545e2a0749870701f7733d00bc06342 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 05:18:43 -0500 Subject: [PATCH 1/5] Reject truncated unsupported DNG samples --- crates/raw/src/tiffraw.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/raw/src/tiffraw.rs b/crates/raw/src/tiffraw.rs index ccbc92025..57dc83a6c 100644 --- a/crates/raw/src/tiffraw.rs +++ b/crates/raw/src/tiffraw.rs @@ -37,14 +37,6 @@ pub(crate) fn check_image(data: &[u8], info: &ImageInfo, limits: &Limits) -> Res if total > limits.samples() || total == 0 { return Err(RawError::Limit("image too large")); } - let bits = info.bits() as u32; - let float = info.sample_format == 3; - if float && !matches!(bits, 16 | 24 | 32) { - return Err(RawError::Unsupported(format!("{bits}-bit floating point samples"))); - } - if !float && !(1..=16).contains(&bits) { - return Err(RawError::Unsupported(format!("{bits}-bit integer samples"))); - } let chunks = info.chunks(data.len() as u64); if chunks.is_empty() { return Err(RawError::Corrupt("no image data chunks".into())); @@ -95,6 +87,17 @@ pub(crate) fn check_image(data: &[u8], info: &ImageInfo, limits: &Limits) -> Res if info.compression == comp::JPEG && referenced.saturating_mul(32) < total as u64 { return Err(RawError::Corrupt(format!("{referenced} bytes of lossless JPEG data cannot hold {total} samples"))); } + // Validate that the source actually contains the declared image before reporting a format + // limitation. A truncated unsupported float DNG must not be mistaken for a valid raw and + // silently replaced by its embedded preview. + let bits = info.bits() as u32; + let float = info.sample_format == 3; + if float && !matches!(bits, 16 | 24 | 32) { + return Err(RawError::Unsupported(format!("{bits}-bit floating-point samples"))); + } + if !float && !(1..=16).contains(&bits) { + return Err(RawError::Unsupported(format!("{bits}-bit integer samples"))); + } Ok(total) } From 40304f4295d98695d8a4a087197534c26a1d43d1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 05:24:44 -0500 Subject: [PATCH 2/5] Report unsupported embedded raster images --- crates/svg/src/tree.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/svg/src/tree.rs b/crates/svg/src/tree.rs index ec0c594bd..71a6df206 100644 --- a/crates/svg/src/tree.rs +++ b/crates/svg/src/tree.rs @@ -486,7 +486,10 @@ fn image_resolver_for(limits: Limits, used: Arc) -> usvg::ImageHrefResolve if let Some(format) = format { return raster_image(&used, &limits, data, format); } - if matches!(mime, "image/jpg" | "image/jpeg" | "image/png" | "image/gif" | "image/webp") { + // usvg can decode only the raster variants above. Count every other declared + // image as an invalid raster so strict consumers can reject it instead of + // silently accepting a document after the image was omitted (for example BMP). + if mime.starts_with("image/") && mime != "image/svg+xml" { used.invalid_raster.fetch_add(1, Ordering::Relaxed); } match mime { From 75d2aeaf17696488064fd845beff2d7fa10f69d9 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 04:22:44 -0500 Subject: [PATCH 3/5] Expose neutral SVG vector document --- crates/svg/src/tree.rs | 1 + crates/svg/src/tree/document.rs | 177 ++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 crates/svg/src/tree/document.rs diff --git a/crates/svg/src/tree.rs b/crates/svg/src/tree.rs index 71a6df206..3a3a4721e 100644 --- a/crates/svg/src/tree.rs +++ b/crates/svg/src/tree.rs @@ -32,6 +32,7 @@ pub use usvg; use crate::paths::{MAX_CSS_SELECTOR_CHECKS, StyleMap, computed_style, parse_stylesheets}; use crate::{guard, svgz}; +pub mod document; pub mod draw; /// What one import may use. The defaults suit a desktop app that parses on a thread with a diff --git a/crates/svg/src/tree/document.rs b/crates/svg/src/tree/document.rs new file mode 100644 index 000000000..5a4b19117 --- /dev/null +++ b/crates/svg/src/tree/document.rs @@ -0,0 +1,177 @@ +//! A format-neutral vector scene made from a bounded SVG tree. +//! +//! The scene preserves paths, groups, paints, transforms, opacity, strokes and image omissions +//! without depending on an application's document model. Coordinates and stroke widths use the +//! SVG tree's CSS-pixel coordinate system. + +use super::usvg; + +/// A parsed SVG scene. Text is represented by its outlined paths. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Document { + pub width: f64, + pub height: f64, + pub nodes: Vec, + /// Number of image nodes omitted from the vector scene. + pub images: usize, + /// True if nodes deeper than the requested depth were omitted. + pub too_deep: bool, +} + +/// A group or path in document order. +#[derive(Clone, Debug, PartialEq)] +pub enum Node { + Group { children: Vec, opacity: f32 }, + Path(Path), +} + +/// A transformed path with its resolved fill and stroke. +#[derive(Clone, Debug, PartialEq)] +pub struct Path { + pub segments: Vec, + pub fill: Option, + pub stroke: Option, +} + +/// A path segment in CSS pixels after its absolute transform. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Segment { + MoveTo([f64; 2]), + LineTo([f64; 2]), + QuadTo([f64; 2], [f64; 2]), + CubicTo([f64; 2], [f64; 2], [f64; 2]), + Close, +} + +/// A resolved color or an unsupported SVG paint server. +#[derive(Clone, Debug, PartialEq)] +pub enum Paint { + Color { rgb: [u8; 3], opacity: f32 }, + Gradient { first_rgb: Option<[u8; 3]>, opacity: f32 }, + Pattern, +} + +/// A resolved outline. +#[derive(Clone, Debug, PartialEq)] +pub struct Stroke { + pub paint: Paint, + pub width: f64, + pub cap: LineCap, + pub join: LineJoin, + pub miter_limit: f64, + pub dash_array: Option>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LineCap { + Butt, + Round, + Square, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LineJoin { + Miter, + Round, + Bevel, +} + +impl Document { + /// Convert a bounded usvg tree to a scene, omitting nodes beyond `max_depth`. + 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 + } +} + +fn color(paint: &usvg::Paint, opacity: f32) -> Paint { + match paint { + usvg::Paint::Color(c) => Paint::Color { rgb: [c.red, c.green, c.blue], opacity }, + usvg::Paint::LinearGradient(g) => { + Paint::Gradient { first_rgb: g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue]), opacity } + } + usvg::Paint::RadialGradient(g) => { + Paint::Gradient { first_rgb: g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue]), opacity } + } + usvg::Paint::Pattern(_) => Paint::Pattern, + } +} + +fn point(transform: usvg::Transform, p: usvg::tiny_skia_path::Point) -> [f64; 2] { + let mut p = p; + transform.map_point(&mut p); + [f64::from(p.x), f64::from(p.y)] +} + +fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut Document) -> Vec { + let mut result = Vec::new(); + for node in group.children() { + match node { + usvg::Node::Path(path) => { + if !path.is_visible() || path.data().is_empty() { + continue; + } + let transform = path.abs_transform(); + let segments = path + .data() + .segments() + .map(|segment| { + use usvg::tiny_skia_path::PathSegment as S; + match segment { + S::MoveTo(a) => Segment::MoveTo(point(transform, a)), + S::LineTo(a) => Segment::LineTo(point(transform, a)), + S::QuadTo(a, b) => Segment::QuadTo(point(transform, a), point(transform, b)), + S::CubicTo(a, b, c) => Segment::CubicTo(point(transform, a), point(transform, b), point(transform, c)), + S::Close => Segment::Close, + } + }) + .collect(); + let fill = path.fill().map(|f| color(f.paint(), f.opacity().get())); + let stroke = path.stroke().map(|s| { + let scale = f64::from((transform.sx * transform.sy - transform.kx * transform.ky).abs().sqrt()); + Stroke { + paint: color(s.paint(), s.opacity().get()), + width: f64::from(s.width().get()) * scale, + cap: match s.linecap() { + usvg::LineCap::Butt => LineCap::Butt, + usvg::LineCap::Round => LineCap::Round, + usvg::LineCap::Square => LineCap::Square, + }, + join: match s.linejoin() { + usvg::LineJoin::Round => LineJoin::Round, + usvg::LineJoin::Bevel => LineJoin::Bevel, + usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => LineJoin::Miter, + }, + miter_limit: f64::from(s.miterlimit().get()), + dash_array: s.dasharray().map(|d| d.iter().map(|v| f64::from(*v) * scale).collect()), + } + }); + result.push(Node::Path(Path { segments, fill, stroke })); + } + usvg::Node::Group(child) => { + if depth >= max_depth { + doc.too_deep = true; + continue; + } + let children = convert_group(child, depth + 1, max_depth, doc); + if !children.is_empty() { + result.push(Node::Group { children, opacity: child.opacity().get() }); + } + } + usvg::Node::Text(text) => { + if depth >= max_depth { + doc.too_deep = true; + continue; + } + let children = convert_group(text.flattened(), depth + 1, max_depth, doc); + if !children.is_empty() { + result.push(Node::Group { children, opacity: 1.0 }); + } + } + usvg::Node::Image(_) => doc.images += 1, + } + } + result +} From 435f093b585007309d9d26aa1d18e23a6bccb536 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 05:40:01 -0500 Subject: [PATCH 4/5] Preserve SVG gradients and fallback metadata --- crates/svg/src/tree/document.rs | 163 +++++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 13 deletions(-) diff --git a/crates/svg/src/tree/document.rs b/crates/svg/src/tree/document.rs index 5a4b19117..4474ba08a 100644 --- a/crates/svg/src/tree/document.rs +++ b/crates/svg/src/tree/document.rs @@ -21,8 +21,39 @@ pub struct Document { /// A group or path in document order. #[derive(Clone, Debug, PartialEq)] pub enum Node { - Group { children: Vec, opacity: f32 }, - Path(Path), + Group { children: Vec, opacity: f32, transform: [f64; 6], bounds: [f64; 4], features: GroupFeatures }, + Path(Box), + Image(Image), +} + +/// Features that need a renderer capable of SVG compositing or external resources. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct GroupFeatures { + pub filters: bool, + pub clip_path: bool, + pub mask: bool, + pub non_normal_blend: bool, + pub isolation: bool, +} + +/// An embedded image node and its bounded source data/placement. +#[derive(Clone, Debug, PartialEq)] +pub struct Image { + pub kind: ImageKind, + pub visible: bool, + pub transform: [f64; 6], + pub bounds: [f64; 4], + pub encoded: Option>>, + pub svg: Option>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ImageKind { + Jpeg, + Png, + Gif, + WebP, + Svg, } /// A transformed path with its resolved fill and stroke. @@ -47,10 +78,30 @@ pub enum Segment { #[derive(Clone, Debug, PartialEq)] pub enum Paint { Color { rgb: [u8; 3], opacity: f32 }, - Gradient { first_rgb: Option<[u8; 3]>, opacity: f32 }, + Gradient { kind: GradientKind, stops: Vec, transform: [f64; 6], spread: Spread, opacity: f32 }, Pattern, } +#[derive(Clone, Debug, PartialEq)] +pub enum GradientKind { + Linear { start: [f64; 2], end: [f64; 2] }, + Radial { center: [f64; 2], radius: f64, focus: [f64; 2], focus_radius: f64 }, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GradientStop { + pub offset: f64, + pub rgb: [u8; 3], + pub opacity: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Spread { + Pad, + Reflect, + Repeat, +} + /// A resolved outline. #[derive(Clone, Debug, PartialEq)] pub struct Stroke { @@ -86,15 +137,60 @@ impl Document { } } +fn transform(t: usvg::Transform) -> [f64; 6] { + [f64::from(t.sx), f64::from(t.ky), f64::from(t.kx), f64::from(t.sy), f64::from(t.tx), f64::from(t.ty)] +} + +fn rect(r: usvg::Rect) -> [f64; 4] { + [f64::from(r.x()), f64::from(r.y()), f64::from(r.width()), f64::from(r.height())] +} + +fn spread(s: usvg::SpreadMethod) -> Spread { + match s { + usvg::SpreadMethod::Pad => Spread::Pad, + usvg::SpreadMethod::Reflect => Spread::Reflect, + usvg::SpreadMethod::Repeat => Spread::Repeat, + } +} + fn color(paint: &usvg::Paint, opacity: f32) -> Paint { match paint { usvg::Paint::Color(c) => Paint::Color { rgb: [c.red, c.green, c.blue], opacity }, - usvg::Paint::LinearGradient(g) => { - Paint::Gradient { first_rgb: g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue]), opacity } - } - usvg::Paint::RadialGradient(g) => { - Paint::Gradient { first_rgb: g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue]), opacity } - } + usvg::Paint::LinearGradient(g) => Paint::Gradient { + kind: GradientKind::Linear { start: [f64::from(g.x1()), f64::from(g.y1())], end: [f64::from(g.x2()), f64::from(g.y2())] }, + stops: g + .stops() + .iter() + .map(|s| GradientStop { + offset: f64::from(s.offset().get()), + rgb: [s.color().red, s.color().green, s.color().blue], + opacity: s.opacity().get(), + }) + .collect(), + transform: transform(g.transform()), + spread: spread(g.spread_method()), + opacity, + }, + usvg::Paint::RadialGradient(g) => Paint::Gradient { + kind: GradientKind::Radial { + center: [f64::from(g.cx()), f64::from(g.cy())], + radius: f64::from(g.r().get()), + focus: [f64::from(g.fx()), f64::from(g.fy())], + focus_radius: f64::from(g.fr().get()), + }, + stops: g + .stops() + .iter() + .map(|s| GradientStop { + offset: f64::from(s.offset().get()), + rgb: [s.color().red, s.color().green, s.color().blue], + opacity: s.opacity().get(), + }) + .collect(), + transform: transform(g.transform()), + spread: spread(g.spread_method()), + opacity, + }, usvg::Paint::Pattern(_) => Paint::Pattern, } } @@ -148,7 +244,7 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut dash_array: s.dasharray().map(|d| d.iter().map(|v| f64::from(*v) * scale).collect()), } }); - result.push(Node::Path(Path { segments, fill, stroke })); + result.push(Node::Path(Box::new(Path { segments, fill, stroke }))); } usvg::Node::Group(child) => { if depth >= max_depth { @@ -157,7 +253,19 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut } let children = convert_group(child, depth + 1, max_depth, doc); if !children.is_empty() { - result.push(Node::Group { children, opacity: child.opacity().get() }); + result.push(Node::Group { + children, + opacity: child.opacity().get(), + transform: transform(child.abs_transform()), + bounds: rect(child.abs_layer_bounding_box().to_rect()), + features: GroupFeatures { + filters: !child.filters().is_empty(), + clip_path: child.clip_path().is_some(), + mask: child.mask().is_some(), + non_normal_blend: child.blend_mode() != usvg::BlendMode::Normal, + isolation: child.should_isolate(), + }, + }); } } usvg::Node::Text(text) => { @@ -167,10 +275,39 @@ fn convert_group(group: &usvg::Group, depth: usize, max_depth: usize, doc: &mut } let children = convert_group(text.flattened(), depth + 1, max_depth, doc); if !children.is_empty() { - result.push(Node::Group { children, opacity: 1.0 }); + result.push(Node::Group { + children, + opacity: 1.0, + transform: transform(text.abs_transform()), + bounds: rect(text.abs_bounding_box()), + features: GroupFeatures::default(), + }); } } - usvg::Node::Image(_) => doc.images += 1, + usvg::Node::Image(image) => { + doc.images += 1; + let kind = match image.kind() { + usvg::ImageKind::JPEG(_) => ImageKind::Jpeg, + usvg::ImageKind::PNG(_) => ImageKind::Png, + usvg::ImageKind::GIF(_) => ImageKind::Gif, + usvg::ImageKind::WEBP(_) => ImageKind::WebP, + usvg::ImageKind::SVG(_) => ImageKind::Svg, + }; + let (encoded, svg) = match image.kind() { + 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))))), + }; + result.push(Node::Image(Image { + kind, + visible: image.is_visible(), + transform: transform(image.abs_transform()), + bounds: rect(image.abs_bounding_box()), + encoded, + svg, + })); + } } } result From 490884beecbeb0e95e9355008accfefd7110d0f4 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 05:44:11 -0500 Subject: [PATCH 5/5] Add shared SVG subtree render handles --- crates/svg/src/tree/document.rs | 98 +++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 10 deletions(-) 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), })); } }