diff --git a/crates/pptx/src/custgeom.rs b/crates/pptx/src/custgeom.rs index 981cb2e24..0eeb37cd5 100644 --- a/crates/pptx/src/custgeom.rs +++ b/crates/pptx/src/custgeom.rs @@ -15,6 +15,10 @@ use crate::xml::{A, El, W}; const DEG: f64 = 60_000.0; +/// Distinct path coordinate spaces whose guides are evaluated for one custom geometry; paths in +/// further spaces are left out. +const MAX_SPACES: usize = 16; + /// Guide variables for one shape. pub struct Guides { vars: HashMap, @@ -151,14 +155,24 @@ fn pt(g: &Guides, e: &El) -> (f64, f64) { pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec { let mut out = vec![]; let Some(pl) = cg.child("pathLst") else { return out }; + let mut spaces: HashMap<(u64, u64), Guides> = HashMap::new(); for p in pl.children_named("path").take(1000) { let pw = p.f64("w").unwrap_or(0.0); let ph = p.f64("h").unwrap_or(0.0); // Guides are evaluated in the path's own space when it has one. let (gw, gh) = if pw > 0.0 || ph > 0.0 { (pw, ph) } else { (w_emu, h_emu) }; - let mut g = Guides::new(gw, gh); - g.eval_list(cg.child("avLst")); - g.eval_list(cg.child("gdLst")); + // The guide lists are evaluated once per coordinate space, not once per path. + let key = (gw.to_bits(), gh.to_bits()); + if !spaces.contains_key(&key) { + if spaces.len() >= MAX_SPACES { + continue; + } + let mut g = Guides::new(gw, gh); + g.eval_list(cg.child("avLst")); + g.eval_list(cg.child("gdLst")); + spaces.insert(key, g); + } + let Some(g) = spaces.get(&key) else { continue }; let mut d = String::new(); let mut cur = (0.0, 0.0); let mut start = (0.0, 0.0); @@ -166,7 +180,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec { match c.local() { "moveTo" => { if let Some(q) = c.child("pt") { - cur = pt(&g, q); + cur = pt(g, q); start = cur; d.push_str("M "); num(&mut d, cur.0); @@ -177,7 +191,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec { } "lnTo" => { if let Some(q) = c.child("pt") { - cur = pt(&g, q); + cur = pt(g, q); d.push_str("L "); num(&mut d, cur.0); d.push(' '); @@ -186,7 +200,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec { } } "cubicBezTo" => { - let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(&g, q)).collect(); + let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(g, q)).collect(); if let [a, b, e] = pts.as_slice() { d.push_str("C "); for (x, y) in [a, b, e] { @@ -199,7 +213,7 @@ pub fn read_cust_geom(cg: &El, w_emu: f64, h_emu: f64) -> Vec { } } "quadBezTo" => { - let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(&g, q)).collect(); + let pts: Vec<(f64, f64)> = c.children_named("pt").map(|q| pt(g, q)).collect(); if let [a, e] = pts.as_slice() { d.push_str("Q "); for (x, y) in [a, e] { @@ -470,6 +484,21 @@ mod tests { assert_eq!(paths[1].d, "M 1 1 Q 2 2 3 1"); } + #[test] + fn paths_share_guides_and_distinct_spaces_are_capped() { + let path = |w: usize| format!(r#""#); + let paths: String = (1..=40).map(|i| path(10 * i)).collect(); + let x = format!( + r#"{paths}{}"#, + path(10) + ); + let d = parse(x.as_bytes()).unwrap(); + let out = read_cust_geom(&d.root, 1.0, 1.0); + assert_eq!(out.len(), MAX_SPACES + 1, "16 spaces plus the repeat of the first"); + assert!(out[0].d.starts_with("M 5 1"), "{}", out[0].d); + assert!(out[MAX_SPACES].d.starts_with("M 5 1"), "{}", out[MAX_SPACES].d); + } + #[test] fn write_then_read_path() { let p = CustomPath { w: 100.0, h: 50.0, d: "M 0 0 L 100 0 C 100 10 90 50 50 50 Q 0 50 0 0 Z".into(), fill: FillMode::Norm, stroke: true };