xlsx: share each custom number format across cell formats, cap font names at 31 characters

Every cell format record copied its font (name included) and built its
own copy of its number-format code, so many short records naming one
long font or code multiplied it in memory. Custom codes are now built
once and shared, and font names are cut to Excel's 31 characters.
This commit is contained in:
Dylan M. Taylor
2026-11-03 12:01:19 +00:00
parent e1604d1791
commit f8b80d4eef
14 changed files with 50 additions and 10 deletions
+1
Submodule .wt-consolidate added at e1604d1791
Submodule
+1
Submodule .wt-delim added at e1604d1791
Submodule
+1
Submodule .wt-display added at e1604d1791
Submodule
+1
Submodule .wt-engine added at e1604d1791
Submodule
+1
Submodule .wt-fill added at e1604d1791
Submodule
+1
Submodule .wt-fonts added at e1604d1791
Submodule
+1
Submodule .wt-hf added at e1604d1791
Submodule
+1
Submodule .wt-keytips added at e1604d1791
Submodule
+1
Submodule .wt-numval added at e1604d1791
+1
Submodule .wt-passthrough added at e1604d1791
+1
Submodule .wt-pivotfilter added at e1604d1791
Submodule
+1
Submodule .wt-textfn added at e1604d1791
+19 -10
View File
@@ -59,11 +59,14 @@ fn bool_prop(e: &El, name: &str) -> bool {
const DEFAULT_FONT_FAMILY: u8 = 2;
const MAX_FONT_FAMILY: u8 = 14;
/// Longest font name kept, in characters (Excel's own limit). Every cell format copies its
/// font, so a long name repeated by many formats would otherwise multiply in memory.
const MAX_FONT_NAME: usize = 31;
pub fn read_font(e: &El, palette: &[u32]) -> Font {
let mut f = Font::default();
if let Some(n) = e.child("name").or_else(|| e.child("rFont")).and_then(|n| n.attr("val")) {
f.name = n.to_string();
f.name = n.chars().take(MAX_FONT_NAME).collect();
}
if let Some(sz) = e.child("sz").and_then(|s| s.attr_f64("val")) {
f.size = sz.clamp(1.0, 409.0) as f32;
@@ -277,14 +280,14 @@ fn read_protection(p: &El) -> Protection {
Protection { locked: p.flag("locked", true), hidden: p.flag("hidden", false) }
}
fn num_fmt_code(id: u32, custom: &HashMap<u32, String>) -> NumFmt {
fn num_fmt_code(id: u32, custom: &HashMap<u32, NumFmt>) -> NumFmt {
if let Some(c) = custom.get(&id) {
return NumFmt::new(c);
return c.clone();
}
NumFmt::new(builtin_format(id).unwrap_or("General"))
}
fn read_xf(xf: &El, fonts: &[Font], fills: &[Fill], borders: &[Borders], fmts: &HashMap<u32, String>, checkboxes: &[bool]) -> Style {
fn read_xf(xf: &El, fonts: &[Font], fills: &[Fill], borders: &[Borders], fmts: &HashMap<u32, NumFmt>, checkboxes: &[bool]) -> Style {
let mut s = Style::default();
if let Some(f) = xf.attr_u32("fontId").and_then(|i| fonts.get(i as usize)) {
s.font = f.clone();
@@ -336,6 +339,8 @@ pub fn read_styles(root: &El, checkboxes: &[bool], minor_font: &str) -> StylesIn
}
}
}
// One shared copy of each custom code, which every format using it points to.
let shared_fmts: HashMap<u32, NumFmt> = fmts.iter().map(|(id, code)| (*id, NumFmt::new(code))).collect();
// The scheme link only means something on a font in the body face; elsewhere it stays as a new font has it.
let body_font = |e: &El| {
let mut f = read_font(e, &palette);
@@ -345,10 +350,14 @@ pub fn read_styles(root: &El, checkboxes: &[bool], minor_font: &str) -> StylesIn
let fonts: Vec<Font> = root.child("fonts").map(|f| f.kids("font").map(body_font).collect()).unwrap_or_default();
let fills: Vec<Fill> = root.child("fills").map(|f| f.kids("fill").map(|e| read_fill(e, &palette, false)).collect()).unwrap_or_default();
let borders: Vec<Borders> = root.child("borders").map(|f| f.kids("border").map(|e| read_border(e, &palette)).collect()).unwrap_or_default();
let xfs =
root.child("cellXfs").map(|x| x.kids("xf").map(|xf| read_xf(xf, &fonts, &fills, &borders, &fmts, checkboxes)).collect()).unwrap_or_default();
let style_xfs: Vec<Style> =
root.child("cellStyleXfs").map(|x| x.kids("xf").map(|xf| read_xf(xf, &fonts, &fills, &borders, &fmts, &[])).collect()).unwrap_or_default();
let xfs = root
.child("cellXfs")
.map(|x| x.kids("xf").map(|xf| read_xf(xf, &fonts, &fills, &borders, &shared_fmts, checkboxes)).collect())
.unwrap_or_default();
let style_xfs: Vec<Style> = root
.child("cellStyleXfs")
.map(|x| x.kids("xf").map(|xf| read_xf(xf, &fonts, &fills, &borders, &shared_fmts, &[])).collect())
.unwrap_or_default();
let mut named = Vec::new();
if let Some(cs) = root.child("cellStyles") {
for c in cs.kids("cellStyle") {
@@ -359,12 +368,12 @@ pub fn read_styles(root: &El, checkboxes: &[bool], minor_font: &str) -> StylesIn
}
}
}
let dxfs = root.child("dxfs").map(|d| d.kids("dxf").map(|x| read_dxf(x, &palette, &fmts)).collect()).unwrap_or_default();
let dxfs = root.child("dxfs").map(|d| d.kids("dxf").map(|x| read_dxf(x, &palette, &shared_fmts)).collect()).unwrap_or_default();
StylesIn { xfs, dxfs, palette, named, num_fmts: fmts }
}
/// One differential format (`dxf`): the style it sets over a cell's own.
fn read_dxf(x: &El, palette: &[u32], fmts: &HashMap<u32, String>) -> Style {
fn read_dxf(x: &El, palette: &[u32], fmts: &HashMap<u32, NumFmt>) -> Style {
let mut s = Style::default();
if let Some(e) = x.child("font") {
let f = read_font(e, palette);
+19
View File
@@ -385,3 +385,22 @@ fn outline_summary_directions_round_trip() {
let xml = String::from_utf8(crate::package::Package::open(&bytes).unwrap().read("xl/worksheets/sheet1.xml").unwrap().unwrap()).unwrap();
assert!(!xml.contains("outlinePr"), "{xml}");
}
/// Many cell formats naming one long font and one long custom number format: the code is shared
/// by every format and the font name is cut to Excel's 31 characters, so neither is copied per
/// format.
#[test]
fn cell_formats_share_their_number_format_and_keep_short_font_names() {
let xml = format!(
r#"<styleSheet><numFmts><numFmt numFmtId="164" formatCode="{}"/></numFmts><fonts><font><name val="{}"/></font></fonts><cellXfs>{}</cellXfs></styleSheet>"#,
"0".repeat(100_000),
"F".repeat(100_000),
r#"<xf numFmtId="164" fontId="0"/>"#.repeat(20_000)
);
let root = crate::xml::parse(xml.as_bytes()).unwrap();
let st = crate::styles::read_styles(&root, &[], "Calibri");
assert_eq!(st.xfs.len(), 20_000);
assert!(st.xfs.iter().all(|s| std::sync::Arc::ptr_eq(&s.num_fmt.0, &st.xfs[0].num_fmt.0)));
assert_eq!(st.xfs[0].num_fmt.as_str().len(), 100_000);
assert!(st.xfs.iter().all(|s| s.font.name.chars().count() == 31));
}