From b6d76e108ae3498db360fb614430e6c7b0b415d0 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Sun, 26 Jul 2026 02:25:17 +0900 Subject: [PATCH] fix(xlsx): resolve worksheet drawing fonts through the workbook theme Excel writes a shape label's run properties as `` with no typeface at all. DrawingML resolves an absent `` to the theme's minor Latin font, but the drawing parser read only sz/b/i and never looked at a typeface or the theme, so every worksheet drawing label rendered in Typst's serif default instead of the workbook's sans-serif face. Parse `` on a run, resolve the `+mj-lt` and `+mn-lt` placeholders against a new `ThemeFontScheme` read from the same theme part the color palette already comes from, and fall back to the minor Latin font for runs that name no typeface. That alone does not change the rendered PDF, because the second half of the same path was also missing: `document_requests_font_families` inspected a sheet page's header, footer, and table but never its drawing text boxes. A workbook whose only font request comes from a shape label therefore skipped font-context resolution entirely, and the Typst compile never received the macOS Office font directories that hold Calibri - so the family office2pdf had just asked for silently fell back to the serif default. Traverse the text boxes too. The regression fixture's three labels now embed and render Calibri, matching the native Excel export. Fixes #461 Signed-off-by: Yonghye Kwon --- assets/bugfixes/issue-461/after.jpg | Bin 0 -> 27211 bytes assets/bugfixes/issue-461/before.jpg | Bin 0 -> 27253 bytes assets/bugfixes/issue-461/gt.jpg | Bin 0 -> 27447 bytes crates/office2pdf/src/parser/drawingml.rs | 66 +++++++++ crates/office2pdf/src/parser/xlsx_drawing.rs | 43 ++++-- .../src/parser/xlsx_drawing_tests.rs | 132 +++++++++++++++++- crates/office2pdf/src/render/font_subst.rs | 17 +++ 7 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 assets/bugfixes/issue-461/after.jpg create mode 100644 assets/bugfixes/issue-461/before.jpg create mode 100644 assets/bugfixes/issue-461/gt.jpg diff --git a/assets/bugfixes/issue-461/after.jpg b/assets/bugfixes/issue-461/after.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44f7f775972b75c050ef91340d5bfe8f37624b94 GIT binary patch literal 27211 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 d5L|}46$ZZ_1qn6)i12Jv}i6 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 awue%j{No^-9S0_Z8eVAJ2Ox#}6aNoE7xTpc literal 0 HcmV?d00001 diff --git a/assets/bugfixes/issue-461/gt.jpg b/assets/bugfixes/issue-461/gt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9dc9e0b96ca11f837162e5499935caaa389fed1f GIT binary patch literal 27447 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 literal 0 HcmV?d00001 diff --git a/crates/office2pdf/src/parser/drawingml.rs b/crates/office2pdf/src/parser/drawingml.rs index 6405aa42..cc106830 100644 --- a/crates/office2pdf/src/parser/drawingml.rs +++ b/crates/office2pdf/src/parser/drawingml.rs @@ -297,6 +297,72 @@ pub(crate) fn parse_theme_color_scheme(xml: &str) -> HashMap { colors } +/// The Latin typefaces a theme part's `` declares. A slot the +/// theme leaves empty stays `None`, because DrawingML spells "inherit" as an +/// empty `typeface` and an empty family would select nothing. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct ThemeFontScheme { + pub(crate) major_latin: Option, + pub(crate) minor_latin: Option, +} + +impl ThemeFontScheme { + /// Resolve a DrawingML `typeface` attribute. The placeholders `+mj-lt` + /// and `+mn-lt` name this scheme's major and minor Latin fonts; any + /// other value is a literal family name. + pub(crate) fn resolve_typeface(&self, typeface: &str) -> Option { + match typeface { + "" => None, + "+mj-lt" => self.major_latin.clone(), + "+mn-lt" => self.minor_latin.clone(), + literal => Some(literal.to_string()), + } + } +} + +/// Parse just the `` Latin typefaces out of a theme part +/// (`theme1.xml`). +/// +/// The pptx parser keeps its own combined single-pass reader because it +/// also collects colors and fill styles from the same document. +pub(crate) fn parse_theme_font_scheme(xml: &str) -> ThemeFontScheme { + let mut fonts = ThemeFontScheme::default(); + let mut reader = Reader::from_str(xml); + let mut in_major: bool = false; + let mut in_minor: bool = false; + + loop { + match reader.read_event() { + Ok(Event::Start(ref e)) => match e.local_name().as_ref() { + b"majorFont" => in_major = true, + b"minorFont" => in_minor = true, + _ => {} + }, + Ok(Event::End(ref e)) => match e.local_name().as_ref() { + b"majorFont" => in_major = false, + b"minorFont" => in_minor = false, + _ => {} + }, + Ok(Event::Empty(ref e)) if e.local_name().as_ref() == b"latin" => { + let typeface: Option = + get_attr_str(e, b"typeface").filter(|typeface| !typeface.is_empty()); + // Only a slot's first `` is its Latin face; the + // script-specific `` entries that follow are not. + if in_major { + fonts.major_latin = fonts.major_latin.take().or(typeface); + } else if in_minor { + fonts.minor_latin = fonts.minor_latin.take().or(typeface); + } + } + Ok(Event::Eof) => break, + Err(_) => break, + _ => {} + } + } + + fonts +} + #[cfg(test)] #[path = "drawingml_tests.rs"] mod tests; diff --git a/crates/office2pdf/src/parser/xlsx_drawing.rs b/crates/office2pdf/src/parser/xlsx_drawing.rs index 53107936..550ed74b 100644 --- a/crates/office2pdf/src/parser/xlsx_drawing.rs +++ b/crates/office2pdf/src/parser/xlsx_drawing.rs @@ -3,6 +3,7 @@ use std::io::Cursor; use crate::ir::Chart; use crate::parser::chart::parse_chart_xml; +use crate::parser::drawingml::ThemeFontScheme; use crate::parser::xml_util; /// Extract charts from the XLSX ZIP with their anchor positions per sheet. @@ -671,7 +672,7 @@ pub(super) fn extract_text_boxes_with_anchors( let sheet_rids = parse_workbook_sheet_rids(&workbook_xml); let workbook_rels_xml = read_zip_entry_string(&mut archive, "xl/_rels/workbook.xml.rels"); let rid_to_target = parse_rels_targets(&workbook_rels_xml); - let theme_colors = workbook_theme_colors(&mut archive, &workbook_rels_xml); + let (theme_colors, theme_fonts) = workbook_theme(&mut archive, &workbook_rels_xml); let mut result: HashMap> = HashMap::new(); @@ -693,7 +694,7 @@ pub(super) fn extract_text_boxes_with_anchors( if drawing_xml.is_empty() { continue; } - let boxes = parse_drawing_text_boxes(&drawing_xml, &theme_colors); + let boxes = parse_drawing_text_boxes(&drawing_xml, &theme_colors, &theme_fonts); if !boxes.is_empty() { result.entry(sheet_name.clone()).or_default().extend(boxes); } @@ -746,10 +747,12 @@ fn resolved_or_legacy( } /// Parse `` text boxes from a worksheet drawing, resolving scheme -/// colors against the workbook theme palette. +/// colors against the workbook theme palette and run typefaces against its +/// font scheme. pub(super) fn parse_drawing_text_boxes( xml: &str, theme_colors: &HashMap, + theme_fonts: &ThemeFontScheme, ) -> Vec { use crate::ir::{ Alignment, BorderLineStyle, BorderSide, Paragraph, ParagraphStyle, Run, TextStyle, @@ -895,6 +898,13 @@ pub(super) fn parse_drawing_text_boxes( } } } + b"latin" if in_run => { + if let Some(typeface) = xml_util::get_attr_str(e, b"typeface") + && let Some(family) = theme_fonts.resolve_typeface(&typeface) + { + current_style.font_family = Some(family); + } + } b"pPr" if current_para.is_some() => { for attr in e.attributes().flatten() { if attr.key.local_name().as_ref() == b"algn" @@ -948,9 +958,17 @@ pub(super) fn parse_drawing_text_boxes( Ok(quick_xml::events::Event::Text(ref t)) => { if in_text && let Ok(text) = t.xml_content() { if let Some(para) = current_para.as_mut() { + let mut style = current_style.clone(); + // An `` without `` - how Excel writes + // plain shape labels - resolves to the theme's minor + // Latin font in DrawingML, not to a renderer default + // (issue #461). + if style.font_family.is_none() { + style.font_family = theme_fonts.minor_latin.clone(); + } para.runs.push(Run { text: text.to_string(), - style: current_style.clone(), + style, href: None, footnote: None, }); @@ -1025,19 +1043,24 @@ pub(super) fn parse_drawing_text_boxes( result } -/// Read the workbook theme palette (`xl/theme/theme1.xml` or the -/// rels-declared target). Missing or unreadable themes yield an empty map, -/// which downgrades scheme resolution to the legacy light/dark fallback. -fn workbook_theme_colors( +/// Read the workbook theme's color palette and Latin font scheme +/// (`xl/theme/theme1.xml`, or the rels-declared target). A missing or +/// unreadable theme yields an empty palette, which downgrades scheme +/// resolution to the legacy light/dark fallback, and an empty font scheme, +/// which leaves font selection to the renderer. +fn workbook_theme( archive: &mut zip::ZipArchive>, workbook_rels_xml: &str, -) -> HashMap { +) -> (HashMap, ThemeFontScheme) { let theme_path = parse_rels_by_type(workbook_rels_xml, "theme") .first() .map(|target| resolve_relative_xl_path("xl", target)) .unwrap_or_else(|| "xl/theme/theme1.xml".to_string()); let theme_xml = read_zip_entry_string(archive, &theme_path); - crate::parser::drawingml::parse_theme_color_scheme(&theme_xml) + ( + crate::parser::drawingml::parse_theme_color_scheme(&theme_xml), + crate::parser::drawingml::parse_theme_font_scheme(&theme_xml), + ) } #[cfg(test)] diff --git a/crates/office2pdf/src/parser/xlsx_drawing_tests.rs b/crates/office2pdf/src/parser/xlsx_drawing_tests.rs index b7d4a2f4..0edbc9bc 100644 --- a/crates/office2pdf/src/parser/xlsx_drawing_tests.rs +++ b/crates/office2pdf/src/parser/xlsx_drawing_tests.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use super::*; use crate::ir::Color; +use crate::parser::drawingml::ThemeFontScheme; fn accent_theme() -> HashMap { // Office default theme slots a workbook actually ships in theme1.xml. @@ -36,7 +37,11 @@ fn drawing_with_fill(color_markup: &str) -> String { } fn fill_of(color_markup: &str, theme: &HashMap) -> Option { - let boxes = parse_drawing_text_boxes(&drawing_with_fill(color_markup), theme); + let boxes = parse_drawing_text_boxes( + &drawing_with_fill(color_markup), + theme, + &ThemeFontScheme::default(), + ); assert_eq!(boxes.len(), 1, "fixture should yield one text box"); boxes[0].fill } @@ -131,3 +136,128 @@ fn theme_color_scheme_parses_from_theme_xml() { assert_eq!(colors.get("hlink"), Some(&Color::new(0x05, 0x63, 0xC1))); assert_eq!(colors.get("accent2"), None); } + +fn office_theme_fonts() -> ThemeFontScheme { + // The font scheme an Excel-saved workbook ships in theme1.xml. + ThemeFontScheme { + major_latin: Some("Calibri Light".to_string()), + minor_latin: Some("Calibri".to_string()), + } +} + +fn drawing_with_run_properties(run_properties: &str) -> String { + format!( + r#" + + 1010 + 4040 + + + + {run_properties}accent1 + + + + +"# + ) +} + +fn font_of(run_properties: &str, fonts: &ThemeFontScheme) -> Option { + let boxes = parse_drawing_text_boxes( + &drawing_with_run_properties(run_properties), + &HashMap::new(), + fonts, + ); + assert_eq!(boxes.len(), 1, "fixture should yield one text box"); + boxes[0].paragraphs[0].runs[0].style.font_family.clone() +} + +#[test] +fn drawing_run_without_a_typeface_inherits_the_theme_minor_font() { + // Excel writes shape labels as `` with no + // typeface at all; DrawingML resolves that to the theme's minor Latin + // font, not to the renderer's serif default (issue #461). + assert_eq!( + font_of(r#""#, &office_theme_fonts()), + Some("Calibri".to_string()) + ); +} + +#[test] +fn drawing_run_without_run_properties_inherits_the_theme_minor_font() { + assert_eq!( + font_of("", &office_theme_fonts()), + Some("Calibri".to_string()) + ); +} + +#[test] +fn drawing_run_uses_its_explicit_latin_typeface() { + assert_eq!( + font_of( + r#""#, + &office_theme_fonts() + ), + Some("Georgia".to_string()) + ); +} + +#[test] +fn drawing_run_resolves_theme_typeface_placeholders() { + // `+mj-lt` and `+mn-lt` name the theme's major and minor Latin fonts. + assert_eq!( + font_of( + r#""#, + &office_theme_fonts() + ), + Some("Calibri Light".to_string()) + ); + assert_eq!( + font_of( + r#""#, + &office_theme_fonts() + ), + Some("Calibri".to_string()) + ); +} + +#[test] +fn drawing_run_font_stays_unset_without_a_theme_font_scheme() { + // A workbook with no readable theme part leaves font selection to the + // renderer's existing fallback rather than inventing a family. + assert_eq!( + font_of(r#""#, &ThemeFontScheme::default()), + None + ); +} + +#[test] +fn theme_font_scheme_parses_from_theme_xml() { + let theme_xml = r#" + + + + + + +"#; + let fonts = crate::parser::drawingml::parse_theme_font_scheme(theme_xml); + assert_eq!(fonts.major_latin.as_deref(), Some("Calibri Light")); + assert_eq!(fonts.minor_latin.as_deref(), Some("Calibri")); +} + +#[test] +fn theme_font_scheme_ignores_empty_typefaces() { + // Themes spell "inherit" as an empty typeface; it must not become a font. + let theme_xml = r#" + + + + + + +"#; + let fonts = crate::parser::drawingml::parse_theme_font_scheme(theme_xml); + assert_eq!(fonts, ThemeFontScheme::default()); +} diff --git a/crates/office2pdf/src/render/font_subst.rs b/crates/office2pdf/src/render/font_subst.rs index e48b066d..a243959a 100644 --- a/crates/office2pdf/src/render/font_subst.rs +++ b/crates/office2pdf/src/render/font_subst.rs @@ -515,6 +515,11 @@ fn collect_document_font_families(doc: &Document) -> BTreeSet { collect_header_footer_fonts(footer, &mut fonts); } collect_table_fonts(&page.table, &mut fonts); + for text_box in &page.text_boxes { + for paragraph in &text_box.paragraphs { + collect_block_fonts(&Block::Paragraph(paragraph.clone()), &mut fonts); + } + } } } } @@ -553,10 +558,22 @@ pub(crate) fn document_requests_font_families(doc: &Document) -> bool { .as_ref() .is_some_and(header_footer_requests_font_family) || table_requests_font_family(&page.table) + // Worksheet drawings carry their own runs; a workbook whose + // only font request comes from a shape label still needs the + // font search context, or the compiler never sees the + // directories that hold the requested face (issue #461). + || page.text_boxes.iter().any(sheet_text_box_requests_font_family) } }) } +fn sheet_text_box_requests_font_family(text_box: &crate::ir::SheetTextBox) -> bool { + text_box + .paragraphs + .iter() + .any(|paragraph| block_requests_font_family(&Block::Paragraph(paragraph.clone()))) +} + fn resolve_available_fallback(font_family: &str, context: &FontSearchContext) -> Option { if context.has_family(font_family) { return None;