Merge pull request #823 from marcoarnulfo:fix/xlsx-keep-default-font

This commit is contained in:
Brandon Thomas
2026-10-13 00:08:47 -04:00
4 changed files with 64 additions and 4 deletions
+6
View File
@@ -377,6 +377,12 @@ impl Default for StyleTable {
}
impl StyleTable {
/// A table whose default (Normal) style is `normal`, such as a file's own Normal font.
pub fn with_default(normal: Style) -> Self {
let mut index = HashMap::new();
index.insert(normal.clone(), StyleId::DEFAULT);
StyleTable { styles: vec![normal], index }
}
pub fn get(&self, id: StyleId) -> &Style {
self.styles.get(id.0 as usize).or_else(|| self.styles.first()).unwrap_or(&DEFAULT_STYLE)
}
+4
View File
@@ -123,6 +123,10 @@ pub fn read_xlsx(bytes: &[u8]) -> Result<(Workbook, ReadReport), IoError> {
if let Some(x) = cx.optional_xml(&r.target)? {
let StylesIn { xfs, dxfs, palette, named, num_fmts } = read_styles(&x, &checkboxes);
cx.num_fmts = num_fmts;
// Cell format 0 is the Normal style: its font is what every unformatted cell shows.
if let Some(normal) = xfs.first() {
cx.styles = StyleTable::with_default(Style { font: normal.font.clone(), ..Style::default() });
}
cx.xf_map = xfs.into_iter().enumerate().map(|(i, s)| if i == 0 { StyleId::DEFAULT } else { cx.styles.intern(s) }).collect();
cx.dxfs = dxfs;
cx.palette = palette;
+9 -4
View File
@@ -396,7 +396,8 @@ pub fn color_attrs(c: &Color) -> String {
}
}
fn font_xml(f: &Font, dxf: bool) -> String {
/// `minor`: the font follows the theme's body font (`<scheme val="minor"/>`).
fn font_xml(f: &Font, dxf: bool, minor: bool) -> String {
let mut s = String::from("<font>");
if f.bold {
s.push_str("<b/>");
@@ -429,6 +430,9 @@ fn font_xml(f: &Font, dxf: bool) -> String {
}
if !dxf {
let _ = write!(s, "<name val=\"{}\"/><family val=\"2\"/>", esc_attr(&f.name));
if minor {
s.push_str("<scheme val=\"minor\"/>");
}
}
s.push_str("</font>");
s
@@ -667,7 +671,7 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St
|| d.font.color != Color::Auto
|| d.font.vert != VertAlign::Baseline
{
dx.push_str(&font_xml(&d.font, true));
dx.push_str(&font_xml(&d.font, true, false));
}
if d.num_fmt != def.num_fmt {
let id = nf.id(d.num_fmt.as_str());
@@ -699,8 +703,9 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St
s.push_str("</numFmts>");
}
let _ = write!(s, "<fonts count=\"{}\">", fonts.len());
for f in &fonts {
s.push_str(&font_xml(f, false));
for (i, f) in fonts.iter().enumerate() {
// A Normal font in the theme's body face follows the theme, as Excel writes it (#805).
s.push_str(&font_xml(f, false, i == 0 && f.name == wb.theme.minor_font));
}
let _ = write!(s, "</fonts><fills count=\"{}\">", fills.len());
for f in &fills {
+45
View File
@@ -1051,6 +1051,51 @@ fn package_kind_sets_workbook_content_type() {
assert_eq!(crate::XlsxKind::from_path("noext"), crate::XlsxKind::Workbook);
}
/// The workbook's default font (`fonts[0]`, the Normal style's) is what every default-formatted
/// cell shows, typed or empty; saving keeps it instead of resetting it to Calibri 11 (#805).
#[test]
fn default_font_survives_a_round_trip() {
let styles = |font0: &str| {
format!(
r#"<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="2"><font>{font0}</font><font><b/><sz val="14"/><color theme="1"/><name val="Arial"/><family val="2"/></font></fonts>
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>"#
)
};
let rels = r#"<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>"#;
let body = r#"<sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>typed text</t></is></c><c r="B1" s="1"><v>1</v></c></row></sheetData>"#;
let font0 = |bytes: &[u8]| {
let mut pkg = Package::open(bytes).unwrap();
let st = pkg.read_xml("xl/styles.xml").unwrap().unwrap();
let f = st.child("fonts").unwrap().kids("font").next().unwrap().clone();
let val = |n: &str| f.child(n).and_then(|e| e.attr("val")).map(str::to_string);
(val("sz"), val("name"), val("family"), val("scheme"))
};
let file =
super::minimal(body, &[("xl/styles.xml", &styles(r#"<sz val="10"/><color theme="1"/><name val="Arial"/><family val="2"/>"#))], rels, "");
let (wb, _) = read_xlsx(&file).unwrap();
let saved = write_xlsx(&wb).unwrap();
assert_eq!(font0(&saved), (Some("10".into()), Some("Arial".into()), Some("2".into()), None));
let (back, _) = read_xlsx(&saved).unwrap();
let normal = &back.styles.get(StyleId::DEFAULT).font;
assert_eq!((normal.name.as_str(), normal.size), ("Arial", 10.0), "empty cells and new input take the Normal font");
let s = back.sheet(0).unwrap();
assert_eq!(s.cell(at("A1")).unwrap().style, StyleId::DEFAULT);
let b1 = &back.styles.get(s.cell(at("B1")).unwrap().style).font;
assert!(b1.bold && b1.size == 14.0, "an explicit font is still its own: {b1:?}");
// Excel's own Normal font follows the theme's body font; the scheme link survives too.
let file = super::minimal(
body,
&[("xl/styles.xml", &styles(r#"<name val="Calibri"/><family val="2"/><color theme="1"/><sz val="12"/><scheme val="minor"/>"#))],
rels,
"",
);
let saved = write_xlsx(&read_xlsx(&file).unwrap().0).unwrap();
assert_eq!(font0(&saved), (Some("12".into()), Some("Calibri".into()), Some("2".into()), Some("minor".into())));
#[test]
fn solid_data_bar_survives_with_sparklines_and_gradient_neighbours() {
let mut wb = Workbook::new();