From f519fd67f68fef644bcf691fa932e3cb942f6d09 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 04:49:33 +0200 Subject: [PATCH] XLSX: keep a dxf's protection, font size and face, and the font parts it turns off (part of #1209) --- crates/model/src/style.rs | 21 +++++++++ crates/xlsx/src/styles.rs | 76 +++++++++++++++++++++++-------- crates/xlsx/src/tests/fixtures.rs | 35 ++++++++++++++ 3 files changed, 112 insertions(+), 20 deletions(-) diff --git a/crates/model/src/style.rs b/crates/model/src/style.rs index d3976876e..800bb34d1 100644 --- a/crates/model/src/style.rs +++ b/crates/model/src/style.rs @@ -339,6 +339,27 @@ pub struct Style { /// Number format code; "General" by default. pub num_fmt: NumFmt, pub protection: Protection, + /// Differential formats only. + #[serde(default)] + pub dxf_stated: DxfStated, +} + +/// The parts a differential format states at a value its `Style` can't tell from unset: a dxf's +/// default parts are unset, so a bold turned off, a face, a size or a protection that matches the +/// defaults would otherwise read as nothing. The values are the style's own; this records that +/// the format states them, and only where they match the defaults, so a format built in the app +/// and the same format read back from a file stay equal. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DxfStated { + pub bold: bool, + pub italic: bool, + pub strike: bool, + pub underline: bool, + pub size: bool, + pub name: bool, + /// ``: the theme font the face follows (`major`, `minor` or `none`). + pub scheme: Option, + pub protection: bool, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/crates/xlsx/src/styles.rs b/crates/xlsx/src/styles.rs index 01f1fa632..49ef72ca5 100644 --- a/crates/xlsx/src/styles.rs +++ b/crates/xlsx/src/styles.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::fmt::Write as _; -use gridcraft_model::style::Protection; +use gridcraft_model::style::{DxfStated, Protection}; use gridcraft_model::{ Alignment, BorderLine, BorderStyle, Borders, Color, Fill, Font, HAlign, NumFmt, PatternType, Style, Underline, VAlign, VertAlign, Workbook, }; @@ -348,10 +348,23 @@ pub fn read_styles(root: &El, checkboxes: &[bool]) -> StylesIn { d.kids("dxf") .map(|x| { let mut s = Style::default(); - if let Some(f) = x.child("font") { - let f = read_font(f, &palette); - // Differential fonts rarely name a face; keep the default face and size. - s.font = Font { name: s.font.name.clone(), size: s.font.size, ..f }; + if let Some(e) = x.child("font") { + let f = read_font(e, &palette); + // Differential fonts rarely name a face; unless stated, keep the default face and size. + let has_size = e.child("sz").and_then(|n| n.attr_f64("val")).is_some(); + let has_name = e.child("name").and_then(|n| n.attr("val")).is_some(); + let name = if has_name { f.name.clone() } else { s.font.name.clone() }; + let size = if has_size { f.size } else { s.font.size }; + let def = Font::default(); + let st = &mut s.dxf_stated; + st.bold = e.child("b").is_some() && !f.bold; + st.italic = e.child("i").is_some() && !f.italic; + st.strike = e.child("strike").is_some() && !f.strike; + st.underline = e.child("u").is_some() && f.underline == Underline::None; + st.size = has_size && size == def.size; + st.name = has_name && name == def.name; + st.scheme = e.child_val("scheme").map(str::to_string); + s.font = Font { name, size, ..f }; } if let Some(n) = x.child("numFmt") { if let Some(code) = n.attr("formatCode") { @@ -371,6 +384,7 @@ pub fn read_styles(root: &El, checkboxes: &[bool]) -> StylesIn { } if let Some(p) = x.child("protection") { s.protection = read_protection(p); + s.dxf_stated.protection = s.protection == Protection::default(); } s }) @@ -396,19 +410,20 @@ pub fn color_attrs(c: &Color) -> String { } } -/// `minor`: the font follows the theme's body font (``). -fn font_xml(f: &Font, dxf: bool, minor: bool) -> String { +/// `minor`: the font follows the theme's body font (``). `dxf`: a +/// differential format's font, which writes only what it states. +fn font_xml(f: &Font, dxf: Option<&DxfStated>, minor: bool) -> String { + let stated = dxf.cloned().unwrap_or_default(); let mut s = String::from(""); - if f.bold { - s.push_str(""); - } - if f.italic { - s.push_str(""); - } - if f.strike { - s.push_str(""); + for (on, said, tag) in [(f.bold, stated.bold, "b"), (f.italic, stated.italic, "i"), (f.strike, stated.strike, "strike")] { + if on { + let _ = write!(s, "<{tag}/>"); + } else if said { + let _ = write!(s, "<{tag} val=\"0\"/>"); + } } match f.underline { + Underline::None if stated.underline => s.push_str(""), Underline::None => {} Underline::Single => s.push_str(""), Underline::Double => s.push_str(""), @@ -420,19 +435,27 @@ fn font_xml(f: &Font, dxf: bool, minor: bool) -> String { VertAlign::Superscript => s.push_str(""), VertAlign::Subscript => s.push_str(""), } - if !dxf { + let def = Font::default(); + if dxf.is_none() || stated.size || f.size != def.size { let _ = write!(s, "", num(f.size as f64)); } if f.color != Color::Auto { let _ = write!(s, "", color_attrs(&f.color)); - } else if !dxf { + } else if dxf.is_none() { s.push_str(""); } - if !dxf { + if dxf.is_none() { let _ = write!(s, "", esc_attr(&f.name)); if minor { s.push_str(""); } + } else { + if stated.name || f.name != def.name { + let _ = write!(s, "", esc_attr(&f.name)); + } + if let Some(sc) = &stated.scheme { + let _ = write!(s, "", esc_attr(sc)); + } } s.push_str(""); s @@ -664,14 +687,24 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St for d in dxfs { dx.push_str(""); let def = Style::default(); + let st = &d.dxf_stated; if d.font.bold || d.font.italic || d.font.strike || d.font.underline != Underline::None || d.font.color != Color::Auto || d.font.vert != VertAlign::Baseline + || d.font.size != def.font.size + || d.font.name != def.font.name + || st.bold + || st.italic + || st.strike + || st.underline + || st.size + || st.name + || st.scheme.is_some() { - dx.push_str(&font_xml(&d.font, true, false)); + dx.push_str(&font_xml(&d.font, Some(st), false)); } if d.num_fmt != def.num_fmt { let id = nf.id(d.num_fmt.as_str()); @@ -683,6 +716,9 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St if let Some(a) = alignment_xml(&d.align) { dx.push_str(&a); } + if st.protection || d.protection != def.protection { + let _ = write!(dx, "