XLSX: keep a dxf's protection, font size and face, and the font parts it turns off (part of #1209)

This commit is contained in:
Marco Arnulfo
2026-10-14 04:49:33 +02:00
parent 84acf6293b
commit f519fd67f6
3 changed files with 112 additions and 20 deletions
+21
View File
@@ -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,
/// `<scheme val>`: the theme font the face follows (`major`, `minor` or `none`).
pub scheme: Option<String>,
pub protection: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+56 -20
View File
@@ -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 (`<scheme val="minor"/>`).
fn font_xml(f: &Font, dxf: bool, minor: bool) -> String {
/// `minor`: the font follows the theme's body font (`<scheme val="minor"/>`). `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("<font>");
if f.bold {
s.push_str("<b/>");
}
if f.italic {
s.push_str("<i/>");
}
if f.strike {
s.push_str("<strike/>");
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("<u val=\"none\"/>"),
Underline::None => {}
Underline::Single => s.push_str("<u/>"),
Underline::Double => s.push_str("<u val=\"double\"/>"),
@@ -420,19 +435,27 @@ fn font_xml(f: &Font, dxf: bool, minor: bool) -> String {
VertAlign::Superscript => s.push_str("<vertAlign val=\"superscript\"/>"),
VertAlign::Subscript => s.push_str("<vertAlign val=\"subscript\"/>"),
}
if !dxf {
let def = Font::default();
if dxf.is_none() || stated.size || f.size != def.size {
let _ = write!(s, "<sz val=\"{}\"/>", num(f.size as f64));
}
if f.color != Color::Auto {
let _ = write!(s, "<color {}/>", color_attrs(&f.color));
} else if !dxf {
} else if dxf.is_none() {
s.push_str("<color theme=\"1\"/>");
}
if !dxf {
if dxf.is_none() {
let _ = write!(s, "<name val=\"{}\"/><family val=\"2\"/>", esc_attr(&f.name));
if minor {
s.push_str("<scheme val=\"minor\"/>");
}
} else {
if stated.name || f.name != def.name {
let _ = write!(s, "<name val=\"{}\"/>", esc_attr(&f.name));
}
if let Some(sc) = &stated.scheme {
let _ = write!(s, "<scheme val=\"{}\"/>", esc_attr(sc));
}
}
s.push_str("</font>");
s
@@ -664,14 +687,24 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St
for d in dxfs {
dx.push_str("<dxf>");
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, "<protection locked=\"{}\" hidden=\"{}\"/>", d.protection.locked as u8, d.protection.hidden as u8);
}
if d.borders != def.borders {
dx.push_str(&border_xml(&d.borders));
}
@@ -705,7 +741,7 @@ pub fn write_styles(wb: &Workbook, dxfs: &[Style], extra_fmts: &[String]) -> (St
let _ = write!(s, "<fonts count=\"{}\">", fonts.len());
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));
s.push_str(&font_xml(f, None, i == 0 && f.name == wb.theme.minor_font));
}
let _ = write!(s, "</fonts><fills count=\"{}\">", fills.len());
for f in &fills {
+35
View File
@@ -189,6 +189,41 @@ fn styles_theme_and_sizes() {
assert_eq!(s.print.fit_to, Some((1, 0)));
}
/// A differential format's face, size and scheme survive a save, and so do the parts it states at
/// their default value: a bold, italic or underline turned off, a protection (#1209). Read as
/// unset, a header dxf that turns bold off lost it and the table style made the header bold again.
#[test]
fn dxf_font_and_parts_stated_at_their_defaults_survive_save() {
let dxfs = concat!(
r#"<dxf><font><b val="0"/><i val="0"/><strike val="0"/><u val="none"/><sz val="11"/><name val="Calibri"/><scheme val="minor"/></font></dxf>"#,
r#"<dxf><protection locked="1" hidden="0"/></dxf>"#,
r#"<dxf><font><b/></font><protection locked="0" hidden="1"/></dxf>"#,
r#"<dxf><font><sz val="9"/><name val="Arial"/></font></dxf>"#,
);
let styles = format!(
r#"<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
<borders count="1"><border/></borders>
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellXfs>
<dxfs count="4">{dxfs}</dxfs>
</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"><v>1</v></c></row></sheetData>
<conditionalFormatting sqref="A1"><cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan"><formula>0</formula></cfRule></conditionalFormatting>
<conditionalFormatting sqref="A2"><cfRule type="cellIs" dxfId="1" priority="2" operator="greaterThan"><formula>0</formula></cfRule></conditionalFormatting>
<conditionalFormatting sqref="A3"><cfRule type="cellIs" dxfId="2" priority="3" operator="greaterThan"><formula>0</formula></cfRule></conditionalFormatting>
<conditionalFormatting sqref="A4"><cfRule type="cellIs" dxfId="3" priority="4" operator="greaterThan"><formula>0</formula></cfRule></conditionalFormatting>"#;
let bytes = minimal(body, &[("xl/styles.xml", &styles)], rels, "");
let (wb, _) = read_xlsx(&bytes).unwrap();
let out = crate::write_xlsx(&wb).unwrap();
let st = crate::package::Package::open(&out).unwrap().read("xl/styles.xml").unwrap().unwrap();
let st = String::from_utf8(st).unwrap();
let written = st.split_once("<dxfs").and_then(|(_, r)| r.split_once("</dxfs>")).map(|(d, _)| d);
assert_eq!(written, Some(format!(r#" count="4">{dxfs}"#).as_str()));
}
#[test]
fn strict_namespace_and_shared_strings() {
let ct = super::CT;