From 870ff1b27f8083dbccee98136f467c4aa88e5b61 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Sat, 18 Jul 2026 19:25:29 +0900 Subject: [PATCH] fix(pptx): apply style-matrix outlines (lnRef) to shapes Shapes whose outline comes from the style matrix rendered without a stroke. Two causes: the outline width lives in the theme lnStyleLst referenced by , which we never parsed; and the lnRef scheme color carrying a shade/tint transform arrives as a Start event, which only the Empty-event color arm handled, so the color was dropped (the same class of bug as the text-box shade fix in #240). Parse the theme lnStyleLst widths into ThemeData, capture the lnRef idx, fall back to that width when no explicit is present, and resolve lnRef/fillRef/fontRef colors from Start events too. Related: #318 Co-Authored-By: Claude Fable 5 Signed-off-by: Yonghye Kwon --- assets/bugfixes/issue-318/after.jpg | Bin 0 -> 5872 bytes assets/bugfixes/issue-318/before.jpg | Bin 0 -> 5406 bytes assets/bugfixes/issue-318/gt.jpg | Bin 0 -> 5752 bytes .../src/parser/pptx_shape_style_tests.rs | 41 ++++++++++++++++++ crates/office2pdf/src/parser/pptx_slides.rs | 41 +++++++++++++++++- crates/office2pdf/src/parser/pptx_theme.rs | 39 +++++++++++++++++ .../office2pdf/src/parser/pptx_theme_tests.rs | 22 ++++++++++ 7 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 assets/bugfixes/issue-318/after.jpg create mode 100644 assets/bugfixes/issue-318/before.jpg create mode 100644 assets/bugfixes/issue-318/gt.jpg diff --git a/assets/bugfixes/issue-318/after.jpg b/assets/bugfixes/issue-318/after.jpg new file mode 100644 index 0000000000000000000000000000000000000000..904c602bbeab66a670ba07b4f0d8564676c6a44b GIT binary patch literal 5872 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 literal 0 HcmV?d00001 diff --git a/assets/bugfixes/issue-318/before.jpg b/assets/bugfixes/issue-318/before.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b95e3a68b52950c2070cc0252742cf7231545fc0 GIT binary patch literal 5406 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 Magfh}%HhC&0G!U@@Bjb+ literal 0 HcmV?d00001 diff --git a/assets/bugfixes/issue-318/gt.jpg b/assets/bugfixes/issue-318/gt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ff99567a206a9aac049179902c91bea02aa0db6 GIT binary patch literal 5752 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 literal 0 HcmV?d00001 diff --git a/crates/office2pdf/src/parser/pptx_shape_style_tests.rs b/crates/office2pdf/src/parser/pptx_shape_style_tests.rs index 483d9441..dcbbc1fe 100644 --- a/crates/office2pdf/src/parser/pptx_shape_style_tests.rs +++ b/crates/office2pdf/src/parser/pptx_shape_style_tests.rs @@ -613,3 +613,44 @@ fn test_split_textbox_preserves_alignment() { typst_output.source, ); } + +#[test] +fn test_shape_style_lnref_outline_resolves_width_and_shaded_color() { + // A shape whose outline comes only from with a + // shaded scheme color (a Start-event color) must render a stroke: width + // from the theme lnStyleLst, color from the resolved scheme (issue #318). + let theme_xml = r#" + + + + + + + + + + + + + +"#; + let shape = r#""#.to_string(); + let slide = make_slide_xml(&[shape]); + let data = build_test_pptx_with_theme(SLIDE_CX, SLIDE_CY, &[slide], theme_xml); + + let parser = PptxParser; + let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap(); + let page = first_fixed_page(&doc); + let FixedElementKind::Shape(ref s) = page.elements[0].kind else { + panic!("expected shape"); + }; + let stroke = s.stroke.as_ref().expect("lnRef must produce a stroke"); + // idx=2 → theme lnStyleLst[1] = 12700 EMU = 1pt. + assert!( + (stroke.width - 1.0).abs() < 0.01, + "outline width from theme lnStyleLst idx 2, got {}", + stroke.width + ); + // accent1 (4472C4) shaded 50% ≈ half each channel. + assert_eq!(stroke.color, Color::new(0x22, 0x39, 0x62)); +} diff --git a/crates/office2pdf/src/parser/pptx_slides.rs b/crates/office2pdf/src/parser/pptx_slides.rs index a9514e4a..fa1c3555 100644 --- a/crates/office2pdf/src/parser/pptx_slides.rs +++ b/crates/office2pdf/src/parser/pptx_slides.rs @@ -557,6 +557,9 @@ struct ShapeState { adj_values: Vec, /// Fallback line color from `` scheme reference. style_ln_color: Option, + /// `` (1-based) into the theme line style list, for the + /// fallback outline width when no explicit `` is present. + style_ln_idx: Option, /// Fallback fill color from `` scheme reference. style_fill_color: Option, /// Fallback text color from `` scheme reference. @@ -595,6 +598,7 @@ impl Default for ShapeState { tail_end: ArrowHead::None, adj_values: Vec::new(), style_ln_color: None, + style_ln_idx: None, style_fill_color: None, style_font_color: None, explicit_no_fill: false, @@ -613,6 +617,7 @@ impl ShapeState { /// Finalize a shape element when `` is reached. /// Returns elements to add: for shapes with text AND non-rectangular geometry, /// returns two elements (shape background + transparent text overlay). +#[allow(clippy::too_many_arguments)] fn finalize_shape( shape: &mut ShapeState, paragraphs: &mut Vec, @@ -621,7 +626,20 @@ fn finalize_shape( text_box_no_wrap: bool, text_box_auto_fit: bool, text_box_text_rotation_deg: Option, + theme_line_style_widths: &[i64], ) -> Vec { + // Outline width: explicit `` when present, otherwise the theme + // line style referenced by `` (issue #318). + let effective_ln_width_emu: i64 = if shape.ln_width_emu > 0 { + shape.ln_width_emu + } else { + shape + .style_ln_idx + .and_then(|idx| theme_line_style_widths.get(idx - 1).copied()) + .unwrap_or(shape.ln_width_emu) + }; + let effective_ln_width_pt: f64 = emu_to_pt(effective_ln_width_emu); + // Resolve effective fill: explicit > noFill > style fallback. let effective_fill: Option = if shape.fill.is_some() { shape.fill @@ -640,7 +658,7 @@ fn finalize_shape( // Use explicit line color, falling back to style-based color from . let effective_ln_color: Option = shape.ln_color.or(shape.style_ln_color); let stroke: Option = effective_ln_color.map(|color| BorderSide { - width: emu_to_pt(shape.ln_width_emu), + width: effective_ln_width_pt, color, style: shape.ln_dash_style, }); @@ -749,7 +767,7 @@ fn finalize_shape( // Use explicit line color, falling back to style-based color from . let effective_ln_color: Option = shape.ln_color.or(shape.style_ln_color); let stroke: Option = effective_ln_color.map(|color| BorderSide { - width: emu_to_pt(shape.ln_width_emu), + width: effective_ln_width_pt, color, style: shape.ln_dash_style, }); @@ -1380,9 +1398,27 @@ impl<'a> SlideXmlParser<'a> { &mut self.pic, ); } + // Style-matrix ref colors (``/``/``) + // can carry shade/tint transforms, which arrive as Start events; + // the Empty-event arms below would miss them. + b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_ln_ref => { + let parsed = parse_color_from_start(reader, e, self.theme, self.color_map); + self.shape.style_ln_color = parsed.color; + } + b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_fill_ref => { + let parsed = parse_color_from_start(reader, e, self.theme, self.color_map); + self.shape.style_fill_color = parsed.color; + } + b"srgbClr" | b"schemeClr" | b"sysClr" if self.in_style_font_ref => { + let parsed = parse_color_from_start(reader, e, self.theme, self.color_map); + self.shape.style_font_color = parsed.color; + } // `` inside `` provides fallback line color. b"lnRef" if self.in_shape && !self.shape.in_sp_pr && !self.in_txbody => { self.in_style_ln_ref = true; + self.shape.style_ln_idx = get_attr_str(e, b"idx") + .and_then(|value| value.parse::().ok()) + .filter(|idx| *idx > 0); } // `` inside `` provides fallback fill color. b"fillRef" if self.in_shape && !self.shape.in_sp_pr && !self.in_txbody => { @@ -1730,6 +1766,7 @@ impl<'a> SlideXmlParser<'a> { self.text_box_no_wrap, self.text_box_auto_fit, self.text_box_text_rotation_deg, + &self.theme.line_style_widths, )); } self.in_shape = false; diff --git a/crates/office2pdf/src/parser/pptx_theme.rs b/crates/office2pdf/src/parser/pptx_theme.rs index dde21c37..d7b0b149 100644 --- a/crates/office2pdf/src/parser/pptx_theme.rs +++ b/crates/office2pdf/src/parser/pptx_theme.rs @@ -17,6 +17,9 @@ pub(super) struct ThemeData { /// Raw XML of each `/` entry, for /// `` idx ≥ 1001 resolution. pub(super) bg_fill_styles: Vec, + /// Line widths (EMU) of each `//` entry, + /// for `` outline width resolution. + pub(super) line_style_widths: Vec, } /// Effective scheme-color aliases for a slide part. @@ -452,10 +455,46 @@ pub(super) fn parse_theme_xml(xml: &str) -> ThemeData { theme.fill_styles = extract_fill_style_entries(xml, b"fillStyleLst"); theme.bg_fill_styles = extract_fill_style_entries(xml, b"bgFillStyleLst"); + theme.line_style_widths = extract_line_style_widths(xml); theme } +/// Extract the `w` (EMU) of each `` inside the theme ``. +fn extract_line_style_widths(xml: &str) -> Vec { + let mut reader = Reader::from_str(xml); + let mut widths: Vec = Vec::new(); + let mut in_list = false; + loop { + match reader.read_event() { + Ok(Event::Start(ref e)) => match e.local_name().as_ref() { + b"lnStyleLst" => in_list = true, + b"ln" if in_list => { + widths.push(line_width_attr(e)); + } + _ => {} + }, + Ok(Event::Empty(ref e)) if in_list && e.local_name().as_ref() == b"ln" => { + widths.push(line_width_attr(e)); + } + Ok(Event::End(ref e)) if e.local_name().as_ref() == b"lnStyleLst" => break, + Ok(Event::Eof) => break, + Err(_) => break, + _ => {} + } + } + widths +} + +fn line_width_attr(e: &BytesStart<'_>) -> i64 { + e.attributes() + .flatten() + .find(|attr| attr.key.local_name().as_ref() == b"w") + .and_then(|attr| attr.unescape_value().ok()) + .and_then(|value| value.parse::().ok()) + .unwrap_or(0) +} + /// Extract the raw XML of each top-level fill entry (``, /// ``, ...) inside the named `` list. fn extract_fill_style_entries(xml: &str, list_tag: &[u8]) -> Vec { diff --git a/crates/office2pdf/src/parser/pptx_theme_tests.rs b/crates/office2pdf/src/parser/pptx_theme_tests.rs index 616f3722..88440aea 100644 --- a/crates/office2pdf/src/parser/pptx_theme_tests.rs +++ b/crates/office2pdf/src/parser/pptx_theme_tests.rs @@ -627,3 +627,25 @@ fn test_layout_shape_uses_master_color_map_with_luminance_offset() { let shape = get_shape(&page.elements[0]); assert_eq!(shape.fill, Some(Color::new(0x80, 0x80, 0x80))); } + +#[test] +fn test_parse_theme_line_style_widths() { + // Theme lnStyleLst widths back outline resolution + // (issue #318): idx=1/2/3 map to the 1st/2nd/3rd entry's EMU width. + let theme_xml = r#" + + + + + + + + + + + + +"#; + let theme = parse_theme_xml(theme_xml); + assert_eq!(theme.line_style_widths, vec![6350, 12700, 19050]); +}