From 2038dc7849f051c2fbae925e4b69e3ab80a9090d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 22:51:22 -0800 Subject: [PATCH] SVG export: numbers are written from integers, not through the exact float formatter push_num formatted every number with write!("{:.*}"), which runs the exact decimal float formatter, then scanned and trimmed the text. That formatter was about 95% of the time to export a path of 200,000 anchors. At 9 decimals or fewer, push_num now scales the value by 10^decimals and writes the digits of the rounded integer when that rounding is certain: the scaled value is below 2^53 and further from a half than the scaling's rounding error. Ties and larger values still go through the formatter, so the output is the same byte for byte. Exporting one path of 200,000 anchors takes a median of 43.4 ms instead of 167.0 ms, and 20,000 rectangles 81.4 ms instead of 141.4 ms (Windows 11, release build). --- crates/svg/src/lib.rs | 90 +++++++++++++++++++++++++++++++++++++++ docs/target-app-parity.md | 1 + 2 files changed, 91 insertions(+) diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index ccc154a38..58cb434ed 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -624,6 +624,49 @@ pub fn xml_escape(s: &str) -> String { o } +/// [`push_num`] through integer arithmetic, for the values it writes the same as the decimal +/// formatter: `decimals` of at most 9, and `v` scaled by 10^`decimals` below 2^53 and further from +/// halfway between two integers than the scaling's rounding error. Returns false, writing nothing, +/// for any other value. +fn push_num_scaled(out: &mut String, v: f64, decimals: u8) -> bool { + const SCALE: [f64; 10] = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9]; + let Some(&scale) = SCALE.get(usize::from(decimals)) else { return false }; + // Each power of ten here is exact, so `x` is within half an ulp (`x * EPSILON / 2`) of |v| × 10^decimals. + let x = v.abs() * scale; + if x.is_nan() || x >= 9.0e15 { + return false; + } + let whole = x.floor(); + let frac = x - whole; + if (frac - 0.5).abs() <= x * f64::EPSILON { + return false; + } + let mut n = whole as u64 + u64::from(frac > 0.5); + let places = usize::from(decimals); + // The digits of `n` from the last one, at least one more than `places` of them. + let mut digits = [0u8; 24]; + let mut len = 0; + for d in digits.iter_mut() { + if n == 0 && len > places { + break; + } + *d = b'0' + (n % 10) as u8; + n /= 10; + len += 1; + } + let Some((fraction, int)) = digits.get(..len).and_then(|d| d.split_at_checked(places)) else { return false }; + let zeros = fraction.iter().take_while(|&&d| d == b'0').count(); + if v < 0.0 && (zeros < places || int.iter().any(|&d| d != b'0')) { + out.push('-'); + } + out.extend(int.iter().rev().map(|&d| char::from(d))); + if zeros < places { + out.push('.'); + out.extend(fraction.iter().skip(zeros).rev().map(|&d| char::from(d))); + } + true +} + /// Format a number with at most `decimals` places, trimming trailing zeros. pub fn fmt_num(v: f64, decimals: u8) -> String { let mut s = String::new(); @@ -635,6 +678,9 @@ pub fn fmt_num(v: f64, decimals: u8) -> String { pub fn push_num(out: &mut String, v: f64, decimals: u8) { use std::fmt::Write as _; let v = if v.is_finite() { v } else { 0.0 }; + if push_num_scaled(out, v, decimals) { + return; + } let start = out.len(); let _ = write!(out, "{:.*}", decimals as usize, v); // writing to a String can't fail let s = out.get(start..).unwrap_or_default(); @@ -674,4 +720,48 @@ mod tests { fn escaping() { assert_eq!(xml_escape("a&\"'"), "a<b>&"'"); } + + /// `v` at `decimals` places by the decimal formatter, trailing zeros trimmed. + fn formatted(v: f64, decimals: u8) -> String { + let s = format!("{:.*}", usize::from(decimals), if v.is_finite() { v } else { 0.0 }); + let s = if s.contains('.') { s.trim_end_matches('0').trim_end_matches('.') } else { &s }; + if matches!(s, "-0" | "") { "0".into() } else { s.into() } + } + + #[test] + fn numbers_match_the_decimal_formatter() { + let mut values = + vec![0.0, -0.0, 0.5, -0.5, 2.5, 0.0005, -0.0005, 1.0005, 0.125, 1e-12, 9.0e15, 4503599627370495.5, 1e20, f64::NAN, f64::INFINITY]; + // Coordinates of every size, and values on and just off the halfway points between written numbers. + let mut seed = 0x2545_f491_4f6c_dd1d_u64; + for _ in 0..20_000 { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + let unit = (seed >> 11) as f64 / (1u64 << 53) as f64; + let v = (unit - 0.5) * 10f64.powi((seed % 13) as i32 - 3); + let tie = ((v * 1000.0).trunc() + 0.5) / 1000.0; + values.extend([v, tie, f64::from_bits(tie.to_bits() + 1), f64::from_bits(tie.to_bits() - 1)]); + } + for v in values { + for d in 0..=12 { + assert_eq!(fmt_num(v, d), formatted(v, d), "{v:e} at {d} decimals"); + } + } + } + + #[test] + fn coordinates_are_written_through_integers() { + for (v, d, want) in [(123.4567, 3, "123.457"), (-0.25, 3, "-0.25"), (-0.0004, 3, "0"), (1600.0, 3, "1600"), (7.0, 0, "7")] { + let mut s = String::new(); + assert!(push_num_scaled(&mut s, v, d), "{v} at {d} decimals"); + assert_eq!(s, want); + } + // Halfway as stored, too large, or too many places: the decimal formatter writes them. + for (v, d) in [(0.5, 0), (0.125, 2), (1e16, 0), (1.0, 10)] { + let mut s = String::new(); + assert!(!push_num_scaled(&mut s, v, d), "{v} at {d} decimals"); + assert!(s.is_empty()); + } + } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 305120225..a1b419412 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1169,6 +1169,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur SVG import's style cascade, used for hidden objects, non-scaling strokes and type, tests each element only against the `