From 3af08505f819f5b52b21e8e626156fae45d5736d Mon Sep 17 00:00:00 2001 From: Ayanprogrammer11 Date: Tue, 13 Oct 2026 06:21:11 +0500 Subject: [PATCH] Replace linear scan in CSS class dedup with a HashMap lookup Export::class() deduplicated declaration blocks with classes.iter().position(), an O(n) scan per style, making exports with many distinct styles (e.g. large palettes) quadratic overall. Track the 1-based class number per declaration in a HashMap alongside the first-use-ordered Vec, so each lookup is O(1) while output stays byte-identical. Also adds a regression test covering 256 distinct declarations with recurrence of early styles, for both InternalCss and StyleEntities styling. --- crates/svg/src/export.rs | 17 ++++++++++------- crates/svg/tests/options.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/crates/svg/src/export.rs b/crates/svg/src/export.rs index bfd01003..616b4771 100644 --- a/crates/svg/src/export.rs +++ b/crates/svg/src/export.rs @@ -50,6 +50,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix: body: String::new(), defs: String::new(), classes: Vec::new(), + class_ids: HashMap::new(), used_ids: HashSet::new(), names: HashMap::new(), depth: 1, @@ -259,8 +260,10 @@ struct Writer<'a> { xf: Affine, body: String, defs: String, - /// Unique CSS declaration blocks; class name is `cls-{index + 1}`. + /// Unique CSS declaration blocks, in first-use order (used for deterministic output). classes: Vec, + /// 1-based class/entity number by declaration; avoids a linear scan for each style. + class_ids: HashMap, used_ids: HashSet, names: HashMap, depth: usize, @@ -657,13 +660,13 @@ impl Writer<'_> { /// The 1-based number of a distinct declaration block (a CSS class or a style entity). fn class(&mut self, decl: String) -> usize { - match self.classes.iter().position(|c| *c == decl) { - Some(i) => i + 1, - None => { - self.classes.push(decl); - self.classes.len() - } + if let Some(&i) = self.class_ids.get(decl.as_str()) { + return i; } + let i = self.classes.len() + 1; + self.class_ids.insert(decl.clone(), i); + self.classes.push(decl); + i } /// ``: the Dublin Core terms of File Info ([`ExportOptions::metadata`]) and the diff --git a/crates/svg/tests/options.rs b/crates/svg/tests/options.rs index d2c8cfda..0a57a103 100644 --- a/crates/svg/tests/options.rs +++ b/crates/svg/tests/options.rs @@ -29,6 +29,33 @@ fn doc() -> Document { d } +/// A large palette should create one class per distinct declaration, preserving first-use +/// numbers even when earlier styles recur after hundreds of others. +#[test] +fn many_css_styles_preserve_first_use_numbers() { + let mut d = Document::new(300.0, 10.0); + let layer = d.layers[0].id; + let colors = (0u16..256).chain([0, 128, 255]); + for (x, color) in colors.enumerate() { + let x = x as f64; + let appearance = Appearance::basic(Paint::solid(Color::rgb8(color as u8, 31, 63)), Paint::None, 0.0); + let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, 0.0, x + 0.5, 1.0)), appearance); + d.insert(Some(layer), usize::MAX, n).unwrap(); + } + + for (styling, definition_marker) in [(Styling::InternalCss, ".cls-"), (Styling::StyleEntities, " = svg.lines().filter(|line| line.contains("