From 0d06481c341d27c0bea2090cc230582cee3a74ba Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:47:50 -0600 Subject: [PATCH] Preserve compatible line spacing in PPTX --- crates/model/src/resolve.rs | 1 + crates/model/src/text.rs | 2 ++ crates/pptx/src/read/dml.rs | 1 + crates/pptx/src/write/dml.rs | 3 ++- crates/pptx/tests/it/roundtrip.rs | 21 +++++++++++++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/model/src/resolve.rs b/crates/model/src/resolve.rs index 8d87229f3..51f50b82d 100644 --- a/crates/model/src/resolve.rs +++ b/crates/model/src/resolve.rs @@ -249,6 +249,7 @@ pub fn body(ctx: &Ctx, shape: &Shape) -> BodyProps { rtl_col: Some(false), upright: Some(false), space_first_last: Some(false), + compat_ln_spc: None, }); b } diff --git a/crates/model/src/text.rs b/crates/model/src/text.rs index 362f43939..0a7598f6e 100644 --- a/crates/model/src/text.rs +++ b/crates/model/src/text.rs @@ -121,6 +121,7 @@ pub struct BodyProps { pub upright: Option, /// Space before the first paragraph and after the last count too (`spcFirstLastPara`). pub space_first_last: Option, + pub compat_ln_spc: Option, } impl BodyProps { @@ -144,6 +145,7 @@ impl BodyProps { self.rtl_col = self.rtl_col.or(p.rtl_col); self.upright = self.upright.or(p.upright); self.space_first_last = self.space_first_last.or(p.space_first_last); + self.compat_ln_spc = self.compat_ln_spc.or(p.compat_ln_spc); } } diff --git a/crates/pptx/src/read/dml.rs b/crates/pptx/src/read/dml.rs index a2ed6abe7..8ef3a68ba 100644 --- a/crates/pptx/src/read/dml.rs +++ b/crates/pptx/src/read/dml.rs @@ -358,6 +358,7 @@ pub fn body_pr(b: &El) -> BodyProps { rtl_col: b.bool("rtlCol"), upright: b.bool("upright"), space_first_last: b.bool("spcFirstLastPara"), + compat_ln_spc: b.bool("compatLnSpc"), ..Default::default() }; for e in b.elements() { diff --git a/crates/pptx/src/write/dml.rs b/crates/pptx/src/write/dml.rs index 320810958..4de05d46f 100644 --- a/crates/pptx/src/write/dml.rs +++ b/crates/pptx/src/write/dml.rs @@ -468,7 +468,8 @@ pub fn body_pr(w: &mut W, b: &BodyProps) { .o("anchor", b.anchor.map(|a| a.xml())) .b("anchorCtr", b.anchor_ctr) .b("upright", b.upright) - .b("spcFirstLastPara", b.space_first_last); + .b("spcFirstLastPara", b.space_first_last) + .b("compatLnSpc", b.compat_ln_spc); let warp = b.warp.as_deref().filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric())); if warp.is_none() && b.autofit.is_none() { w.empty("a:bodyPr", a); diff --git a/crates/pptx/tests/it/roundtrip.rs b/crates/pptx/tests/it/roundtrip.rs index f651fc5c6..3c741f303 100644 --- a/crates/pptx/tests/it/roundtrip.rs +++ b/crates/pptx/tests/it/roundtrip.rs @@ -468,6 +468,27 @@ fn text_and_formatting_round_trip() { assert_eq!(run.text, "x=1"); } +#[test] +fn compatible_line_spacing_mode_survives_two_pptx_round_trips() { + let mut p = Presentation::default(); + let mut slide = new_slide(&mut p, "Compatible line spacing"); + let mut enabled = shape(&mut p, "Enabled", 10.0, 10.0, 200.0, 80.0); + let mut enabled_body = TextBody::from_text("enabled"); + enabled_body.body.compat_ln_spc = Some(true); + enabled.text = Some(enabled_body); + let mut disabled = shape(&mut p, "Disabled", 10.0, 100.0, 200.0, 80.0); + let mut disabled_body = TextBody::from_text("disabled"); + disabled_body.body.compat_ln_spc = Some(false); + disabled.text = Some(disabled_body); + slide.shapes = vec![enabled, disabled]; + p.slides.push(Arc::new(slide)); + + let twice = round(&round(&p)); + let slide = twice.slides.last().expect("added slide"); + assert_eq!(by_name(slide, "Enabled").text.as_ref().expect("text").body.compat_ln_spc, Some(true)); + assert_eq!(by_name(slide, "Disabled").text.as_ref().expect("text").body.compat_ln_spc, Some(false)); +} + #[test] fn shapes_fills_lines_effects_round_trip() { let p = rich_deck();