View › Page Layout (and the status-bar button) now shows the sheet as printed
pages on a desk instead of overlaying page outlines on the grid.
- Model/XLSX: per-sheet view mode (normal / pageLayout / pageBreakPreview),
saved as <sheetView view="…"> and read back. view.* commands set it per
sheet ({sheet?}) without an undo step or dirtying a clean workbook.
- Engine: paper::PageGrid paginates the sheet from A1 with the print
pagination (print::split_bands, shared with PDF export and printing) and
maps cells to pages and back; header/footer section helpers
(split/join, &[Page] field names) shared with the PDF header code.
pageLayout.headerFooter takes {part, section, text} for one section and a
sheet; header/footer strings are capped at 255 characters.
view.ruler toggles the rulers.
- UI: Geo carries the page grid, so painting, hit-testing, selection,
editing, objects and scrolling work through the page transform; frozen
panes are ignored in this view. Pages with margins and gaps, "Click to add
data" pages, rulers in inches (English) or centimetres, headers/footers
drawn on printed pages, click a margin section to edit it in place; a
click elsewhere commits (one undo step), Escape cancels. Header & Footer
contextual tab: presets, Page Number, Number of Pages, Current Date/Time,
File Path, File Name, Sheet Name, Go to Header/Footer. View › Ruler.
- New strings in every catalog; new code-drawn icons; desk colour token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass-through of package content GridCraft does not model: on read the raw
parts are kept with their content types and relationship graph per owner
(package root, workbook, sheet); on write they get fresh part names where
needed and new relationship ids, and the r:ids in kept XML are rewritten.
Kept: custom document properties, ribbon customisation, add-in task panes,
custom XML, links to external workbooks with <externalReferences> (never
followed), connections, sheet query tables, printer settings with the
<pageSetup r:id> (#324), form and ActiveX controls, OLE objects and sheet
backgrounds with their VML shapes (merged with GridCraft's note shapes).
Content whose graph reaches parts GridCraft rewrites is dropped with a
warning; sheet content goes with its sheet and is not copied by Copy Sheet.
Fixes#324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean-room import of Excel 97-2003 workbooks from the public [MS-CFB] and
[MS-XLS] specifications: compound-file streams (FAT, mini FAT, directory
tree), the BIFF8 Workbook stream with Continue records, shared strings,
all cell records, formulas decoded from Ptg tokens (shared and array
formulas, 3-D refs, names, add-in/newer functions by name) with cached
results, number formats, the 1904 date system, merges, column widths, row
heights, defined names and print areas/titles. Encrypted (FILEPASS) and
BIFF5 files are refused with a clear message; everything else is skipped
with a warning. .xls opens as a new unsaved workbook (Save As .xlsx).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AAA exponential smoothing (Hyndman et al. 2008, ETS(A,A,A)) over an evenly
spaced timeline: documented seasonality, data completion and aggregation
arguments, calendar-month date timelines, autocorrelation seasonality
detection, state-space prediction intervals, statistic types 1-8.
Smoothing parameters minimise the in-sample SSE (grid + clamped
Nelder-Mead). XLSX export treats the family's range arguments as
array-taking (no spurious dynamic-array metadata), FORECAST/FORECAST.LINEAR
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chart linked to a PivotTable: series are built from the report
(innermost row items x column items/value fields, totals excluded) and
rebuilt on every refresh or layout change; c:pivotSource round trip;
chart series accept union references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Model: Sheet.edit_ranges (title, ranges, optional password hash,
security descriptor) and gridcraft_model::password: the ECMA-376 legacy
16-bit hash and the salted, iterated SHA-2 hash (SHA-512/384/256; other
algorithms, damaged hashes and spin counts over 10,000,000 are refused).
New passwords use SHA-512, spin count 100000. Adds the sha2 crate.
- XLSX: <protectedRanges> read and written; an unreadable password keeps
the range locked.
- review.allowEditRanges {action: list|add|modify|delete|unlock, ...}: empty
params list; ranges can't change while the sheet is protected; unlock
verifies the password for the session. Not journaled (passwords).
- Locked-cell checks (commit guard, cell.set, picture in cell) let edits
through in open or unlocked ranges; ranges move with structural edits.
- Ribbon button and a form dialog (menu invocation only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value field can be a calculated field whose formula is evaluated on
the sums of the fields it names for each report cell (Excel's documented
semantics). Command to add/change/delete, dialog from the field pane,
XLSX cacheField formula round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
insert.checkbox existed but had no ribbon button, replaced existing values
with FALSE and was written to XLSX as an invalid number format. Now:
Insert > Checkbox (Cell Controls); empty cells start FALSE, other values
stay and display as General; toggling an emptied box checks it and only
checkbox cells toggle; Delete unchecks first, then removes the boxes; XLSX
reads and writes the checkbox as Excel's feature-property-bag cell control
([MS-XLSX] 2.1.22, 2.3.9, 2.4.99).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Writer: one xl/threadedComments/threadedCommentN.xml per sheet with
comments (thread, replies via parentId, done for resolved, dT when known)
and xl/persons/person.xml (one person per author), with the [MS-XLSX]
relationship and content types. Each thread keeps its legacy note, linked
by the tc={id} author and xr:uid; its body is the text plus replies.
Ids are GUID-shaped and deterministic.
- Reader: keeps dT timestamps (Comment.dates, JSON-compatible), drops
replies whose thread is missing, and reads note visibility from the VML
<x:Visible/> (best effort, broken VML skipped silently).
- Invalid dates are never written. Round-trip and hostile-input tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evaluator special form returning the range (or array) without its blank
outer rows and columns; trim codes 0-3 as documented; written as
_xlfn.TRIMRANGE. The trim-reference operators are not parsed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PERCENTOF = SUM(subset) / SUM(all), written as _xlfn.PERCENTOF.
EUROCONVERT uses the fixed EU conversion rates for the currencies the
function documents, with currency rounding and triangulation precision.
New Add-in category for EUROCONVERT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file format has no `@` operator, but formulas with an explicit
implicit intersection were written with a literal `@`
(`<f>@A1:A3*2</f>`). Excel writes `@x` as `_xlfn.SINGLE(x)`, which only
dynamic-array formulas carry. The writer now does the same and marks such
formulas cm="1"; the parser reads `SINGLE(x)` back as `@x`, as it already
does for `ANCHORARRAY` and `#`.
Two cases in the #83 metadata test expected a redundant `@` (`SQRT(@A1:A3)`,
`@A1:A3*2`) to be written as an ordinary formula. Dropping the `@` there is
only lossless if the reader puts it back on legacy formulas, which it
doesn't yet (the read side of #64), so they now expect cm.
generated with Claude Code
An icon-set rule without stored thresholds (every rule made by
home.conditionalFormat, and the sales sample) was written as an empty
<iconSet>, which CT_IconSet forbids (cfvo minOccurs 2); openpyxl drops it.
The writer now always emits one cfvo per icon: the stored thresholds when
there is one per icon, the lower bound prepended when it is missing, else
Excel's defaults (percent 0/33/67, 0/25/50/75, 0/20/40/60/80).
Evaluation read the thresholds as one fewer than the icons, but files
carry one per icon (the first is the lower bound), so thresholds read from
a file were ignored. Both now share icon_count and default_icon_thresholds
in gridcraft-model; icon_count is clamped to 3..=5 so a set name like
"0Foo" can't underflow k - 1.
generated with Claude Code
The ODS and XLSB readers parse untrusted package metadata and content through
`quick_xml::NsReader`. In 0.38.4 that is RUSTSEC-2026-0195 (CVSS 7.5): the namespace
resolver pushes a binding for every `xmlns` / `xmlns:*` declaration on a start tag, with
no cap on how many there are, and `NsReader::read_event` reaches that path - not only
`read_resolved_event`.
None of our caps bound it. MAX_PART (512 MiB), MAX_TOTAL (1 GiB), MAX_ENTRIES,
MAX_CELLS, MAX_TEXT and MAX_DEPTH bound part size, entry count, cells, text and nesting,
never declarations per tag.
Measured with `gridcraft-cli info` on crafted .ods files, before this change:
754 B, 0 declarations -> 8.6 MB, exit 0
24 KB, 1,000,000 -> 52.6 MB, exit 0
187 KB, 8,000,000 -> 367.8 MB, exit 0
770 KB, 33,000,000 -> 1.46 GB, exit 0
About 45 bytes of resident memory per declaration, roughly 1,900x the file size, and no
error at all. At MAX_PART a ~1 MB file reaches ~1.9 GB, and every part is held in memory
while the package is read. Both readers share the path, so a downloaded .ods or .xlsb
was enough. Nothing panicked, so `Session::execute` never saw it.
0.41.0 caps declarations per start tag at 256 (DEFAULT_MAX_DECLARATIONS_PER_ELEMENT,
raisable through NamespaceResolver::set_max_declarations_per_element) and returns
`NamespaceError::TooManyDeclarations` beyond it. The same fixtures after:
754 B -> 8.6 MB, ok 24 KB -> 19 MB, error
187 KB -> 101.8 MB, error 770 KB -> 397 MB, error
with "start tag declares more than 256 namespace bindings". The residual peak is the
part itself: MAX_PART still lets one part decompress to 256 MiB, the existing intended
bound.
API changes for 0.41 (0.x bumps may break; all four are in the xlsx crate):
- `resolve_element` and `resolve_attribute` moved off `NsReader` on to the resolver:
`reader.resolver_mut().resolve_element(..)`. `attr` and `tag` in ods.rs take
`&mut NsReader` for that.
- `Attribute::unescape_value` is deprecated in favour of `normalized_value`, which also
applies the attribute-value normalization the XML specification requires.
- `BytesText::xml_content` now takes the document's XML version.
This also clears RUSTSEC-2026-0194, the quadratic duplicate-attribute check. xml.rs
already passed `with_checks(false)`, but ods.rs and xlsb.rs used the checked iterator, so
they were on the quadratic path as well.
Regression test: `ods_tests::ods_namespace_declaration_bomb_is_rejected` asserts that a
tag with thousands of declarations is an error and that a document with a handful still
reads. It fails on 0.38.4 (the tag is accepted) and passes on 0.41.0.
Verified: `cargo xtask ci`; the full xlsx suite (80 tests) plus the malformed-input
tests; and the four measurements above, before and after, with no panics either way.
Add static pictures inside cells with XLSX roundtrip
Rebased onto main (conflicts with #83/#98 and main resolved by maintainer); the contributor's branch does not allow maintainer edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve array evaluation for non-spilling XLSX formulas
Rebased onto main with a maintainer fix-up (LET bindings via split_last/as_chunks for clippy, no slice indexing); the contributor's branch does not allow maintainer edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every legacy .xls/.doc is a CFB/OLE2 file, so the signature alone isn't encryption. Walk the
compound file's directory (bounds-checked, bounded loops; raw UTF-16 scan when the structure
is unreadable) and report Encrypted only for an EncryptedPackage/EncryptionInfo stream;
other compound files are LegacyBinary with a "save as .xlsx" message. Tests build CFB bytes
in code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blank cell and row repeats that run past 16384 columns / 1048576 rows
(padding from writers with larger sheets) are clipped instead of
rejecting the file; any data or merge past the edge is still an error.
convert() builds its Cell with ..Cell::value(value) so new Cell fields
take their defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smooth_run gives every segment the same number of sub-segments (cap / n, 1..100) so long
series are smoothed end to end within the point cap, and clamps the Catmull-Rom curve to
the plot box. List "smooth" in chart.addElement's params doc, drop the smooth_demo
example, and use the test sheet's own name in series refs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the shared integration points for both formats: combined sniff,
file.open spec, file filters, MCP description, docs. open_bytes now lets
sniffed content (ODS or XLSB) win over the extension.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PhRun is 10 bytes per [MS-XLSB] 2.5.103 (four u16 fields plus a 16-bit
phType/alcH/unused1 word), so the documented size is kept; since the
RichStr ends its record and phonetic data is not imported, bytes after
the documented runs are now ignored instead of rejecting the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Histogram, box & whisker, waterfall, funnel, treemap and sunburst charts
were written as clustered columns, and reading skipped such charts in
files made by Excel, which keeps them as cx:chartSpace (chartex) parts.
The writer now writes these kinds as chartEx parts, as Excel lays them
out: the data goes through hidden _xlchart.v1.N workbook names, each
part comes with chart style and colour parts, and the drawing frames it
in mc:AlternateContent with a text-box fallback. Fills are written out
(series in theme accents, waterfall decreases and treemap/sunburst boxes
in their own, a white chart area), so they don't depend on the style
part. The reader reads the parts back (resolving the _xlchart names,
which aren't kept as names) with data, title, axis titles, legend,
labels and colours, and follows mc:AlternateContent in drawings.
Each header was checked against every name before it, once per suffix
tried, so a source whose headers repeat (a row of thousands of equal
labels, such as check boxes) took cubic time: 4,000 columns took 28 s
in Recommended PivotTables, and a row of 16,384 hung the app, in
PivotTables, in Insert Table and in Get Data from Table/Range. Taken
names and the next suffix of each header are now kept in hash maps, in
the pivot source, in table creation and in the XLSX pivot cache; the
names are the same as before.
A Ctrl+Shift+Enter array formula was evaluated like a dynamic array:
- Read from a file, the cached values in its range were kept as
constants, so the formula showed #SPILL! (any multi-cell array
formula saved by Excel or LibreOffice).
- Entered over C2:C4 with a two-value result, C4 stayed blank where
Excel shows #N/A, and a single-cell array formula spilled.
- On save, the values of its other cells were not cached.
The result is now fitted to the range: a single row or column repeats
across it, cells past the result show #N/A, and a larger result is cut
off. Reading drops the cached values inside the range, entering clears
the range, and saving caches its values. The formula bar shows the
formula in braces, {=B2:B3*2}.
A combo chart whose series have no kind of their own (as insert.chart
makes them) was written as one column group, and a stock chart as
clustered columns, so both reopened as clustered columns. Combo series
are now written as drawn (the first as columns, the others as lines),
and stock charts as a stockChart group (as lines when they don't have
the 3 or 4 series a stock chart needs).
formula::parse("Sheet1!$D$1#") failed with "unknown error literal".
A # right after a cell reference is now the spill range operator: it
evaluates to the anchor's current spill range, or #REF! when the cell
doesn't spill (or its spill is blocked). Its users depend on the
anchor, so they recalculate when the spill grows or shrinks. Names can
use it.
Files have no # operator: like Excel, the writer stores A1# as
_xlfn.ANCHORARRAY(A1), and the parser reads that back as A1#.