From 57bf6de206fcac8a730f6ef59280944a1d5b54e5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 07:52:29 -0800 Subject: [PATCH] perf: skip unused SVG path bounds --- crates/svg/src/export/nodes.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/svg/src/export/nodes.rs b/crates/svg/src/export/nodes.rs index 7be748391..adcd3fd25 100644 --- a/crates/svg/src/export/nodes.rs +++ b/crates/svg/src/export/nodes.rs @@ -2,8 +2,8 @@ use super::output::{sanitize_id, valid_xml_value, xml_attr_escape, xml_attr_vali use super::paint::{StrokePlan, has_raster, pixel_gradient}; use super::text::OUTLINE_TOLERANCE; use super::{ - Affine, AppearanceItem, BezPath, BlendMode, Document, FillRule, Node, NodeKind, Paint, PathData, Props, Rect, StrokeLayer, Writer, Written, - WrittenShape, blend_css, css, fmt_num, stroke, xml_escape, + Affine, AppearanceItem, BezPath, BlendMode, Document, FillRule, Node, NodeKind, Paint, PathData, Props, Rect, StrokeAlign, StrokeLayer, Writer, + Written, WrittenShape, blend_css, css, fmt_num, stroke, xml_escape, }; /// A symbol's `` def and the instance transforms that can `` it. @@ -32,6 +32,22 @@ pub(crate) struct Reuse { /// How deep symbols may sit in one another's art (deeper instances are left out). const MAX_SYMBOL_NEST: u32 = 8; +/// Gradient placement and outside-aligned stroke masks need the shape's geometric box. +/// Solid colours and patterns do not, so avoid walking their path geometry for a box. +fn export_shape_bounds(n: &Node) -> Option { + n.appearance + .items + .iter() + .any(|item| match item { + AppearanceItem::Fill(f) => f.visible && matches!(f.paint, Paint::Gradient(_)), + AppearanceItem::Stroke(s) => { + s.visible && s.width > 0.0 && !s.paint.is_none() && (matches!(s.paint, Paint::Gradient(_)) || s.align == StrokeAlign::Outside) + } + }) + .then(|| n.geometric_bounds()) + .flatten() +} + impl Reuse { const NONE: Self = Self { moves: false, turns: false, scales: false }; @@ -578,7 +594,7 @@ impl Writer<'_> { let dashed = n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if s.dash.as_ref().is_some_and(|d| d.is_dashed()))); let basic = self.basic_shape(path, self.xf, dashed); - self.shape(n, &d, basic, &[path], *rule, n.geometric_bounds()); + self.shape(n, &d, basic, &[path], *rule, export_shape_bounds(n)); } NodeKind::Compound { children, rule } => { let paths: Vec<&PathData> = children.iter().filter(|c| c.visible).filter_map(|c| c.path_data()).filter(|p| !p.is_empty()).collect(); @@ -586,7 +602,7 @@ impl Writer<'_> { return; } let d: Vec = paths.iter().map(|p| self.path_d(p, self.xf)).collect(); - self.shape(n, &d.join(" "), None, &paths, *rule, n.geometric_bounds()); + self.shape(n, &d.join(" "), None, &paths, *rule, export_shape_bounds(n)); } NodeKind::Text(t) => self.text_node(n, t), NodeKind::Image(im) => {