From 1de0101cedbe69e4e37f380c2737b84e7759ba38 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:11:12 -0500 Subject: [PATCH] Honor miter percentage schema bounds --- crates/pptx/src/read/dml.rs | 23 ++++++++++++++++++++++- crates/pptx/src/write/dml.rs | 5 ++++- crates/pptx/tests/it/roundtrip.rs | 5 ++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index e1e5dcd4..9ac99912 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -234,7 +234,7 @@ pub fn line(imp: &mut Imp, part: &Part, ln: &El) -> Line { "bevel" => l.join = Some(LineJoin::Bevel), "miter" => { l.join = Some(LineJoin::Miter); - l.miter_limit = e.attr("lim").and_then(|v| v.parse::().ok()).filter(|v| *v <= 1_000_000); + l.miter_limit = e.attr("lim").and_then(parse_miter_limit); } "headEnd" => l.head = line_end(e), "tailEnd" => l.tail = line_end(e), @@ -244,6 +244,17 @@ pub fn line(imp: &mut Imp, part: &Part, ln: &El) -> Line { l } +fn parse_miter_limit(value: &str) -> Option { + const MAX: u32 = i32::MAX as u32; + let value = value.trim(); + if let Some(percent) = value.strip_suffix('%') { + let units = percent.trim().parse::().ok()? * 1000.0; + (units.is_finite() && (0.0..=MAX as f64).contains(&units)).then(|| units.round() as u32) + } else { + value.parse::().ok().filter(|limit| *limit <= MAX) + } +} + fn line_end(e: &El) -> Option { let kind = e.attr("type").unwrap_or("none"); Some(LineEnd { kind: kind.to_string(), w: e.attr("w").unwrap_or("med").to_string(), len: e.attr("len").unwrap_or("med").to_string() }) @@ -671,6 +682,16 @@ mod tests { assert_eq!(color(&d.root).unwrap().base, ColorBase::Rgb { rgb: Rgba::rgb(255, 0, 0) }); } + #[test] + fn miter_limit_accepts_both_percentage_forms_with_schema_bounds() { + assert_eq!(parse_miter_limit("200000"), Some(200_000)); + assert_eq!(parse_miter_limit("200%"), Some(200_000)); + assert_eq!(parse_miter_limit("1000001"), Some(1_000_001)); + assert_eq!(parse_miter_limit("malformed"), None); + assert_eq!(parse_miter_limit("2147483648"), None); + assert_eq!(parse_miter_limit("2147483.648%"), None); + } + #[test] fn body_props() { let d = parse(br#""#).unwrap(); diff --git a/crates/pptx/src/write/dml.rs b/crates/pptx/src/write/dml.rs index a6d00ff4..d46a7aae 100644 --- a/crates/pptx/src/write/dml.rs +++ b/crates/pptx/src/write/dml.rs @@ -233,7 +233,10 @@ pub fn line(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, l: &Line) { match l.join { Some(LineJoin::Round) => w.empty0("a:round"), Some(LineJoin::Bevel) => w.empty0("a:bevel"), - Some(LineJoin::Miter) => w.empty("a:miter", A::new().o("lim", l.miter_limit)), + Some(LineJoin::Miter) => { + let limit = l.miter_limit.filter(|limit| *limit <= i32::MAX as u32); + w.empty("a:miter", A::new().o("lim", limit)); + } None => {} } for (t, e) in [("a:headEnd", &l.head), ("a:tailEnd", &l.tail)] { diff --git a/crates/pptx/tests/it/roundtrip.rs b/crates/pptx/tests/it/roundtrip.rs index 59904071..d0525241 100644 --- a/crates/pptx/tests/it/roundtrip.rs +++ b/crates/pptx/tests/it/roundtrip.rs @@ -495,11 +495,14 @@ fn line_miter_limit_survives_two_pptx_round_trips() { limited.line = Some(Line { join: Some(deckcraft_model::style::LineJoin::Miter), miter_limit: Some(200_000), ..Default::default() }); let mut defaulted = shape(&mut p, "Default miter", 140.0, 20.0, 100.0, 100.0); defaulted.line = Some(Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..Default::default() }); - Arc::make_mut(&mut p.slides[0]).shapes.extend([limited, defaulted]); + let mut large = shape(&mut p, "Large miter", 260.0, 20.0, 100.0, 100.0); + large.line = Some(Line { join: Some(deckcraft_model::style::LineJoin::Miter), miter_limit: Some(1_000_001), ..Default::default() }); + Arc::make_mut(&mut p.slides[0]).shapes.extend([limited, defaulted, large]); let q = round(&round(&p)); assert_eq!(by_name(&q.slides[0], "Limited miter").line.as_ref().and_then(|line| line.miter_limit), Some(200_000)); assert_eq!(by_name(&q.slides[0], "Default miter").line.as_ref().and_then(|line| line.miter_limit), None); + assert_eq!(by_name(&q.slides[0], "Large miter").line.as_ref().and_then(|line| line.miter_limit), Some(1_000_001)); } #[test]