perf(svg): compute unique ID prefix via single-pass placeholder replacement (#4555)

This commit is contained in:
el-j
2026-10-29 22:12:21 +07:00
committed by bflatastic
parent b96d35139c
commit dc4150d7db
2 changed files with 49 additions and 5 deletions
+26 -5
View File
@@ -32,6 +32,8 @@ pub(crate) use output::sanitize_id;
use output::{entity_value, latin1_refs, valid_xml_value, xml_attr_escape, xml_text};
use text::FontUse;
const UNIQUE_PLACEHOLDER: &str = "u_vc_unique_-";
pub(crate) fn export(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>) -> Output {
// Live geometry effects (Roughen, Warp, Offset Path, Effect → Pathfinder…) export as their result.
let baked = vectorcraft_effects::bake_document(doc);
@@ -39,11 +41,30 @@ pub(crate) fn export(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>
if opts.object_ids != ObjectIds::Unique {
return write(doc, opts, native, "");
}
// Unique ids: every id and class name gets a prefix hashed from the output written without
// one, so the same document always gets the same ids and different ones never share any.
let plain = write(doc, opts, native, "");
let prefix = format!("u{:010x}-", fnv1a(plain.svg.as_bytes()) & 0xff_ffff_ffff);
write(doc, opts, native, &prefix)
// Unique ids: write the body once with a placeholder prefix of fixed length (12 bytes),
// hash the result, and replace the placeholder with the computed prefix in a single pass.
let mut out = write(doc, opts, native, UNIQUE_PLACEHOLDER);
let prefix = format!("u{:010x}-", fnv1a(out.svg.as_bytes()) & 0xff_ffff_ffff);
out.svg = out.svg.replace(UNIQUE_PLACEHOLDER, &prefix);
if opts.preserve_editing && native.is_some() {
update_editing_hash(&mut out.svg);
}
out
}
/// After replacing the unique placeholder prefix, recompute the editing body hash so that
/// `editing(&svg).unwrap().intact` remains valid when `preserve_editing` is enabled.
fn update_editing_hash(svg: &mut String) {
let opts = usvg::roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
let Some(xml) = usvg::roxmltree::Document::parse_with_options(svg, opts).ok() else { return };
let Some(el) = xml.descendants().find(|n| n.tag_name().namespace() == Some(EDITING_NS) && n.tag_name().name() == "document") else { return };
let (Some(old_h), Some(meta)) = (el.attribute("hash"), el.parent_element()) else { return };
let r = meta.range();
let (head, tail) = (svg.get(..r.start).unwrap_or(""), svg.get(r.end..).unwrap_or(""));
let new_h = body_hash(&[head, tail]);
if new_h != old_h {
*svg = svg.replacen(&format!("hash=\"{old_h}\""), &format!("hash=\"{new_h}\""), 1);
}
}
/// `doc` as SVG with `opts`, the native file `native` embedded for Preserve Editing, its generated
+23
View File
@@ -136,3 +136,26 @@ fn label_attributes_name_objects() {
assert_eq!(d.layers[0].name.as_deref(), Some("Background"));
assert_eq!(names(&d), ["Background", "Sky box", "Sun disc", "Badge", "plain"]);
}
#[test]
fn unique_ids_differ_when_only_gradient_stops_differ() {
let make_doc = |stop_color: Color| {
let mut d = Document::new(200.0, 200.0);
d.title = "Identical Structure".into();
let l = d.layers[0].id;
let stops = vec![GradientStop::new(0.0, stop_color), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))];
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops))));
let n = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(10.0, 10.0, 60.0, 60.0)), Appearance::basic(g, Paint::None, 0.0));
d.insert(Some(l), usize::MAX, n).unwrap();
d
};
let opts = ExportOptions { object_ids: ObjectIds::Unique, ..Default::default() };
let svg1 = export(&make_doc(Color::rgb(1.0, 0.0, 0.0)), &opts);
let svg2 = export(&make_doc(Color::rgb(0.0, 1.0, 0.0)), &opts);
let ids1 = ids(&svg1);
let ids2 = ids(&svg2);
assert!(!ids1.is_empty() && !ids2.is_empty());
assert!(ids1.iter().all(|id| !ids2.contains(id)), "IDs must not collide: {ids1:?} vs {ids2:?}");
}