mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user