diff --git a/crates/model/src/style.rs b/crates/model/src/style.rs
index 126727eda..d3976876e 100644
--- a/crates/model/src/style.rs
+++ b/crates/model/src/style.rs
@@ -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)
}
diff --git a/crates/xlsx/src/read.rs b/crates/xlsx/src/read.rs
index b379238c1..ed7996a6f 100644
--- a/crates/xlsx/src/read.rs
+++ b/crates/xlsx/src/read.rs
@@ -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;
diff --git a/crates/xlsx/src/styles.rs b/crates/xlsx/src/styles.rs
index 1c98a544c..01f1fa632 100644
--- a/crates/xlsx/src/styles.rs
+++ b/crates/xlsx/src/styles.rs
@@ -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 (``).
+fn font_xml(f: &Font, dxf: bool, minor: bool) -> String {
let mut s = String::from("");
if f.bold {
s.push_str("");
@@ -429,6 +430,9 @@ fn font_xml(f: &Font, dxf: bool) -> String {
}
if !dxf {
let _ = write!(s, "", esc_attr(&f.name));
+ if minor {
+ s.push_str("");
+ }
}
s.push_str("");
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("");
}
let _ = write!(s, "", 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, "", fills.len());
for f in &fills {
diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs
index 550c7a460..d214fd214 100644
--- a/crates/xlsx/src/tests/roundtrip.rs
+++ b/crates/xlsx/src/tests/roundtrip.rs
@@ -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#"{font0}
+
+
+
+ "#
+ )
+ };
+ let rels = r#""#;
+ let body = r#"typed text1
"#;
+ 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#""#))], 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#""#))],
+ 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();