diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index e799ea84f..b211e265f 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -106,7 +106,9 @@ pub fn geometry(w: &mut W, g: &Geom) { let single = !adj1 && if defaults.is_empty() { vals.len() == 1 } else { defaults.len() == 1 }; for (i, v) in vals.iter().enumerate() { let n = if single && i == 0 { "adj".to_string() } else { format!("adj{}", i + 1) }; - let v = if v.is_finite() { v.round().clamp(-2.0e9, 2.0e9) as i64 } else { 0 }; + // A near-zero-width connector can need a large ratio for an ordinary-size bend. + // Keep guide literals bounded to the signed integer serialization domain. + let v = if v.is_finite() { v.round().clamp(i64::MIN as f64, i64::MAX as f64) as i64 } else { 0 }; w.empty("a:gd", A::new().a("name", n).a("fmla", format!("val {v}"))); } for (n, v) in extra_guides(name) { @@ -1031,6 +1033,23 @@ pub fn tree_header(w: &mut W) { mod tests { use super::*; + #[test] + fn guide_literals_remain_finite_bounded_integers() { + for (guide, expected) in [ + (f64::NAN, 0), + (f64::INFINITY, 0), + (f64::NEG_INFINITY, 0), + (f64::MAX, i64::MAX), + (f64::MIN, i64::MIN), + (12500.6, 12501), + (-12500.6, -12501), + ] { + let mut w = W::frag(); + geometry(&mut w, &Geom::Preset { name: "bentConnector3".into(), adj: vec![guide] }); + assert!(w.s.contains(&format!(r#"fmla="val {expected}""#)), "{}", w.s); + } + } + /// PowerPoint asks to repair a `star5` whose guide list lacks `hf`/`vf` (found black-box). #[test] fn preset_guides_are_complete() { diff --git a/crates/pptx/tests/it/roundtrip.rs b/crates/pptx/tests/it/roundtrip.rs index f44b0c152..e13947669 100644 --- a/crates/pptx/tests/it/roundtrip.rs +++ b/crates/pptx/tests/it/roundtrip.rs @@ -1396,6 +1396,45 @@ fn single_guide_names_follow_the_preset() { } } +/// Large finite guides position a visible bend even when the connector is only one EMU wide. +#[test] +fn large_connector_adjustments_keep_their_elbows_through_two_saves() { + let cases = [ + ("ordinary", 80.0, 50000.0), + ("near control", 1588.0 / 12700.0, -12800000.0), + ("near negative", 1.0 / 12700.0, -20320000000.0), + ("near positive", 1.0 / 12700.0, 20320000000.0), + ("zero width", 0.0, 20320000000.0), + ("ordinary negative", 80.0, -20000.0), + ]; + let mut p = defaults::new_presentation(None); + p.slides.clear(); + let mut s = new_slide(&mut p, "Connectors"); + s.shapes.clear(); + for (name, width, guide) in cases { + let mut sh = shape(&mut p, name, 100.0, 80.0, width, 140.0); + sh.kind = ShapeKind::Connector { start: None, end: None }; + sh.geom = Geom::Preset { name: "bentConnector3".into(), adj: vec![guide] }; + s.shapes.push(sh); + } + p.slides.push(Arc::new(s)); + for generation in 1..=2 { + p = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("reopen"); + for (name, width, guide) in cases { + let sh = p.slides[0].shapes.iter().find(|sh| sh.name == name).expect("connector"); + let Geom::Preset { name: preset, adj } = &sh.geom else { + panic!("preset connector"); + }; + assert_eq!(adj, &[guide], "{name}, save {generation}"); + let xfrm = sh.xfrm.expect("transform"); + assert_eq!((xfrm.w, xfrm.h), (width, 140.0)); + let expected = deckcraft_geom::preset::build("bentConnector3", width, 140.0, &[guide]).expect("expected geometry"); + let actual = deckcraft_geom::preset::build(preset, xfrm.w, xfrm.h, adj).expect("reopened geometry"); + assert_eq!(actual.outline(), expected.outline(), "{name}, save {generation}"); + } + } +} + /// #147: PowerPoint's Float Down is preset 47 (Float Up is 42); it imported as a plain fade. #[test] fn float_down_is_preset_47_both_ways() {