diff --git a/crates/model/src/workbook.rs b/crates/model/src/workbook.rs index 2d16b46b5..dbd011427 100644 --- a/crates/model/src/workbook.rs +++ b/crates/model/src/workbook.rs @@ -200,6 +200,11 @@ pub struct Workbook { pub calc: CalcSettings, #[serde(default, skip_serializing_if = "is_all")] pub show_objects: ShowObjects, + /// Trust Center › Privacy Options › "Remove personal information from file properties on save" + /// (``): kept with the file, and while it is on the author and + /// company are left out of what GridCraft saves, as Excel leaves them empty. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub remove_personal_info: bool, /// Read and set with [`Workbook::active_sheet`] and [`Workbook::set_active_sheet`]. pub active_sheet: ActiveSheet, /// The workbook window's display settings (``), kept across an open + save. @@ -338,6 +343,7 @@ impl Workbook { date_system: DateSystem::D1900, calc: CalcSettings::default(), show_objects: ShowObjects::All, + remove_personal_info: false, active_sheet: ActiveSheet::default(), view: WorkbookView::default(), props: DocProps::default(), diff --git a/crates/xlsx/src/read.rs b/crates/xlsx/src/read.rs index 3cc2a6655..6c43feb31 100644 --- a/crates/xlsx/src/read.rs +++ b/crates/xlsx/src/read.rs @@ -200,6 +200,7 @@ fn read_workbook_properties(cx: &mut Ctx<'_>, wb: &mut Workbook, xml: &El) { if p.flag("date1904", false) { cx.date_system = DateSystem::D1904; } + wb.remove_personal_info = p.flag("filterPrivacy", false); wb.show_objects = match p.attr("showObjects") { Some("none") => ShowObjects::None, Some("placeholders") => ShowObjects::Placeholders, diff --git a/crates/xlsx/src/tests/roundtrip.rs b/crates/xlsx/src/tests/roundtrip.rs index 2bdba21b8..72ce434be 100644 --- a/crates/xlsx/src/tests/roundtrip.rs +++ b/crates/xlsx/src/tests/roundtrip.rs @@ -1696,3 +1696,30 @@ fn generated_rule_formulas_use_the_top_left_cell_of_every_area() { z.by_name("xl/worksheets/sheet1.xml").unwrap().read_to_string(&mut xml).unwrap(); assert!(xml.contains("NOT(ISERROR(SEARCH(\"x\",C2)))"), "{xml}"); } + +#[test] +fn remove_personal_information_is_kept_and_honoured() { + // #3089: `` was dropped, so a workbook cleaned of personal + // information came back with it switched off, and GridCraft wrote the author and company back. + let bytes = super::minimal("", &[], "", r#""#); + let (mut wb, _) = read_xlsx(&bytes).unwrap(); + assert!(wb.remove_personal_info); + wb.props.author = "Jane Example".into(); + wb.props.company = "Example Co".into(); + wb.props.title = "Budget".into(); + let saved = write_xlsx(&wb).unwrap(); + let mut pkg = Package::open(&saved).unwrap(); + let book = pkg.read_xml("xl/workbook.xml").unwrap().unwrap(); + assert_eq!(book.child("workbookPr").and_then(|p| p.attr("filterPrivacy")), Some("1")); + let core = String::from_utf8(pkg.read("docProps/core.xml").unwrap().unwrap()).unwrap(); + let app = String::from_utf8(pkg.read("docProps/app.xml").unwrap().unwrap()).unwrap(); + assert!(!core.contains("Jane Example") && !app.contains("Example Co"), "{core}\n{app}"); + assert!(core.contains("Budget"), "other properties stay: {core}"); + // Off (the default), nothing changes. + wb.remove_personal_info = false; + let saved = write_xlsx(&wb).unwrap(); + let mut pkg = Package::open(&saved).unwrap(); + let book = pkg.read_xml("xl/workbook.xml").unwrap().unwrap(); + assert_eq!(book.child("workbookPr").and_then(|p| p.attr("filterPrivacy")), None); + assert!(String::from_utf8(pkg.read("docProps/core.xml").unwrap().unwrap()).unwrap().contains("Jane Example")); +} diff --git a/crates/xlsx/src/write.rs b/crates/xlsx/src/write.rs index 2c1e8f60f..c53776cb6 100644 --- a/crates/xlsx/src/write.rs +++ b/crates/xlsx/src/write.rs @@ -293,6 +293,9 @@ pub fn write_xlsx_named(wb: &Workbook, kind: XlsxKind, book_name: Option<&str>) if wb.date_system == gridcraft_core::DateSystem::D1904 { w.push_str(" date1904=\"1\""); } + if wb.remove_personal_info { + w.push_str(" filterPrivacy=\"1\""); + } w.push_str("/>"); if wb.protected_structure { let pw = wb.structure_password.as_ref().map(|h| crate::sheet_write::password_attrs(h, "workbook")).unwrap_or_default(); @@ -399,13 +402,16 @@ pub fn write_xlsx_named(wb: &Workbook, kind: XlsxKind, book_name: Option<&str>) // Document properties. let p = &wb.props; + // "Remove personal information on save": the author and company are not saved (#3089). + let private = |v: &String| if wb.remove_personal_info { String::new() } else { v.clone() }; + let (author, company) = (private(&p.author), private(&p.company)); let mut core = format!( "{XML_DECL}" ); for (tag, v) in [ ("dc:title", &p.title), ("dc:subject", &p.subject), - ("dc:creator", &p.author), + ("dc:creator", &author), ("cp:keywords", &p.keywords), ("dc:description", &p.description), ] { @@ -417,8 +423,8 @@ pub fn write_xlsx_named(wb: &Workbook, kind: XlsxKind, book_name: Option<&str>) out.part("docProps/core.xml", Some("application/vnd.openxmlformats-package.core-properties+xml"), core.into_bytes()); let mut app = String::from(XML_DECL); app.push_str("GridCraft"); - if !p.company.is_empty() { - let _ = write!(app, "{}", esc(&p.company)); + if !company.is_empty() { + let _ = write!(app, "{}", esc(&company)); } app.push_str(""); out.part("docProps/app.xml", Some("application/vnd.openxmlformats-officedocument.extended-properties+xml"), app.into_bytes()); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index c4bc72bae..51c0ad309 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-11-01 · **Last updated:** 2026-11-02 · **Change:** minor (Python cells in `.xlsx` as [MS-XLSX] specifies, `_xlfn._xlws.PY(…)` naming code in the Python part, with cached values, and in GridCraft JSON, with limits on open; earlier: equations in xlsx as Office Math with a marker in `descr` so an edit made in Excel reads back, read back only up to the equation source cap, and a brace with a label and a script on its other side keeps both; Symbol font in PDF; earlier: XLSX reads and writes Page Setup's page order, `pageSetup pageOrder`, and PDF export follows it; earlier: chart data tables: native c:dTable import/export; earlier: XLSX workbook-view selection, normalized tab ratios and sheet-edit regressions; fidelity estimates unchanged; earlier: formulas from Google Sheets' XLSX export open as the original formula instead of `IFERROR(__xludf.DUMMYFUNCTION(…),…)` when GridCraft calculates every function in it, and save as it; `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; earlier: `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; CSV and text open detects each column's date order from values that read only one way, warns when every date is ambiguous, and keeps a column with both kinds as text (#2765)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-11-01 · **Last updated:** 2026-11-03 · **Change:** minor (`workbookPr filterPrivacy` kept, and honoured on save; earlier: Python cells in `.xlsx` as [MS-XLSX] specifies, `_xlfn._xlws.PY(…)` naming code in the Python part, with cached values, and in GridCraft JSON, with limits on open; earlier: equations in xlsx as Office Math with a marker in `descr` so an edit made in Excel reads back, read back only up to the equation source cap, and a brace with a label and a script on its other side keeps both; Symbol font in PDF; earlier: XLSX reads and writes Page Setup's page order, `pageSetup pageOrder`, and PDF export follows it; earlier: chart data tables: native c:dTable import/export; earlier: XLSX workbook-view selection, normalized tab ratios and sheet-edit regressions; fidelity estimates unchanged; earlier: formulas from Google Sheets' XLSX export open as the original formula instead of `IFERROR(__xludf.DUMMYFUNCTION(…),…)` when GridCraft calculates every function in it, and save as it; `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; earlier: `file.exportCsv {values: true}` writes raw values for automation; .xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors; CSV and text open detects each column's date order from values that read only one way, warns when every date is ambiguous, and keeps a column with both kinds as text (#2765)) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests. Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h** @@ -155,6 +155,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | XLSX `` (Remove personal information from file properties on save) is read and written back, and while it is on GridCraft saves no author (`dc:creator`) and no company, as Excel leaves them empty (#3089, file part) | | 2026-11-02 | minor | Python cells: `.xlsx` reads and writes `_xlfn._xlws.PY(i,0 or 1)` with the code in the Python part (and reads the string-literal form) with cached values and spills, other formats adopt PY formulas, GridCraft JSON keeps the whole cell, and opening limits code size, result size and spill ranges | | 2026-11-02 | trivial | An equation without Office Math takes its stored source only while its text, paragraphs included, is what the writer wrote for it; a paragraph split that changes no character reads back as a text box with the split text | | 2026-11-02 | trivial | Each equation box carries the marker of its own body in its description, the Office Math box and the plain-text fallback alike, and the reader takes the stored source only while the body it reads matches the marker: the text of an equation without Office Math that another application edited reads back as a text box with the edited text instead of the stale source |