xls-codec
v4.15.7
Published
Hand-written reader for the legacy Excel Binary File Format (.xls, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the shared document-schema.js spreadsheet model - the .xls codec for the documents.js family.
Readme
xls-codec
Hand-written reader and writer for the legacy Excel Binary File Format (
.xls, BIFF8) as specified by [MS-XLS], mapping a workbook's record stream onto the samedocument-schema.jsspreadsheet modelooxml.js's xlsx support andodf.js's ods support target. Worker-isomorphic: the same code runs under Node and inside a Cloudflare Workers isolate.
A .xls file is not one format but two nested ones. The outer container is an [MS-CFB] compound file — the same "filesystem in a file" that carries .doc and .ppt — holding a stream named Workbook. Inside that stream is BIFF8: a flat sequence of records, each a two-byte type, a two-byte size, and that many bytes of data, organised into substreams delimited by BOF/EOF. This package leaves the outer layer to archive-codec's bounded CFB reader and writer and implements the inner one, from the record framing up to a ContentDocument and back.
Status
Under active development, with real, tested read and write support. Built and shipped:
- Record framing (
src/biff/records.ts,src/biff/record-writer.ts) — the three-component record structure of [MS-XLS] 2.1.4 in both directions, with the 8224-byte data ceiling enforced and every malformed or oversized stream thrown on rather than silently truncated or split into aContinuechain the writer does not implement. - Continuation-aware cursor and strings (
src/biff/cursor.ts,src/biff/strings.ts,src/biff/string-writer.ts) —Continuerecords ([MS-XLS] 2.4.58) joined per the rules of the record being continued on read, including the case a naive reader gets wrong: anXLUnicodeRichExtendedString([MS-XLS] 2.5.293) resuming after a boundary re-states its ownfHighByteflag, which may differ from the flag the string started with. All three string shapes (XLUnicodeString,ShortXLUnicodeString,XLUnicodeRichExtendedString) are read and written, compressed (one byte per UTF-16 code unit) whenever every character allows it and uncompressed otherwise. - Workbook globals, read (
src/workbook/globals.ts) and write (src/workbook/globals-writer.ts) —BoundSheet8(sheet names, tab order, hidden state, type, and substream offsets),SSTwith itsContinuechain on read,Format(custom number-format codes),Font,XF's fixed prefix plus its trailingCellXF/StyleXFfill/border payload in both directions (src/biff/xf-colors.ts's shared bit-layout packing/unpacking; see Cell decoration),Palettein both directions, the fifteen mandatory built-inStylerecords, andDate1904. - Worksheet substreams, read (
src/workbook/sheet.ts) and write (src/workbook/sheet-writer.ts) —Dimensions,Row(height and hidden state),ColInfo(width and hidden state),MergeCells, the print-settings record group (see Print settings), and the cell-value family:Number,BoolErr,LabelSst,Blank, and nowFormula(see Formula writing) on write (MulBlank,RK,MulRk, andLabelstay read-only — see below). - Formula writing, same-sheet only (
src/biff/ptg-writer.ts) — aContentSheetCell.formula's own text, compiled back into aFormularecord'sPtgtoken stream: literal operands, same-sheet cell/range references ($-qualified), every arithmetic/comparison/unary/percent operator, explicit parentheses, and a function call resolved by name againstptg-functions.ts's ownFtabtable (PtgFuncwhen its fixed arity matches,PtgFuncVarotherwise) — see Formula writing for the exact scope boundary and why it stops at one sheet. - Cell comments (
src/workbook/comment-writer.ts) — aContentSheetCellComment'stextandauthorwritten back out as theNote/Obj/Txotriplesrc/workbook/comments.tsalready reads, one object id per commented cell;repliesandcreatedAthave nowhere to land, matching the reader's own documented gap (legacy BIFF8 has no threading or per-comment timestamp at all). - Number-format classification and date serials (
excel-number-format,src/serial.ts) — what turns a bare number into the schema's ownpercentage/currency/date/time/dateTimevalue kinds and back, honouring the workbook's own epoch flag (the writer always emits the 1900 system) and refusing the 1900 system's phantom leap day in both directions. The classification itself is a dependency, not local code: this package shares it withooxml.js's xlsx support, since it is the identical mini-language in both formats (ExaDev/documents.js#848). A cell's ownnumberFormatCodeis preserved verbatim on write when present; absent, it resolves to a representative code for its value kind (Generalfor a plain number/string/boolean/error,0%for a percentage, a[$USD]#,##0.00-shaped bracket format for a currency naming an ISO 4217 code — the one carrier the code survives the round trip through, the identical encodingooxml.js's own xlsx writer states for the same schema field — a bare$format for a currency with no code or a symbol that is not an ISO code,mm-dd-yy/h:mm:ss/m/d/yy h:mmfor date/time/dateTime), and the workbook-wideFormat/XFtable is deduplicated across every sheet so two cells sharing one code share one entry. - Formula text recovery, including shared, array, and external-reference formulas (
src/biff/ptg.ts,src/biff/ptg-functions.ts,src/workbook/sheet.ts'scollectFormulaGroups,src/workbook/globals.ts'sreadSupBook) — a Formula record's compiledrgcetoken stream ([MS-XLS] 2.5.198'sPtgvocabulary) read and rebuilt into the infix text a spreadsheet application would show: literal operands (PtgInt/PtgNum/PtgStr/PtgBool/PtgErr/PtgMissArg), cell and range references including their 3D (cross-sheet) forms (PtgRef/PtgArea/PtgRef3d/PtgArea3d,$-qualified per their own relative/absolute flags), every arithmetic/comparison/unary/percent operator and explicit parentheses, function calls through bothPtgFunc(fixed arity, resolved from a curated table of [MS-XLS]'s own Ftab grammar) andPtgFuncVar(variable arity, its own on-diskcparams), a shared formula (PtgExpjoined against theShrFmlarecord that follows its group's base cell, its relativePtgRefN/PtgAreaNtokens re-expanded for each referencing cell's own position, alongside any ordinary, non-relative token the same shared expression carries), an array formula (PtgExpjoined against anArrayrecord instead, its expression returned with no further wrapping — Excel's own{...}CSE bracing is formula-bar display syntax, never written into the formula itself, matchingooxml.js's own xlsx convention; aPtgArrayarray-constant literal's{...}is genuine syntax, not this bracing, and is resolved from its ownPtgExtraArraytrailer wherever one appears, array-entered or not), and a 3D reference's sheet name, resolved throughEXTERNSHEETandSupBookfor both a self-referencing workbook and a genuinely external one (its own file name and sheet name(s) recovered as far asSupBook'svirtPath/rgstfields allow); a DDE/OLE/add-in/same-sheet/unused link, an unresolved sheet index, or an undecodedvirtPathform has no real name to recover, and leaves the whole containing formula unresolved exactly like any other unsupported construct, rather than writing a fabricated placeholder into what would otherwise be real formula text (src/workbook/globals.ts's ownsheetRanges) — see "Formula expressions" under Read-side gaps below for the exact boundary of what still resolves to nothing at all. - Schema mapping —
readXlsContent/readXls(src/content.ts) as before, now also populatingContentSheetCell.formulawherever the Ptg reader above resolves it;writeXlsContent/writeXls(src/write.ts) the counterpart, taking aContentDocument/DocumentTreeofkind: 'spreadsheet'and producing genuine.xlsbytes: a real BIFF8Workbookstream (globals substream, one worksheet substream per sheet,BoundSheet8.lbPlyPospatched to each sheet's real byte offset once every substream's length is known) wrapped in a real [MS-CFB] compound file viaarchive-codec'swriteCompoundFile. - Document metadata —
title/subject/author/keywords/createdIso/modifiedIsoread from a"\x05SummaryInformation"stream when one is present, and written back to one whenever the input's metadata carries anything that stream can hold (see Metadata). - Cell decoration — a cell's background fill (every named
FillPatternvalue, solid and pattern alike) and per-side borders, read from and written toXF's trailingCellXFpayload plus the workbook's ownPaletterecord, in both directions and verified against real LibreOffice-produced BIFF8, not just this package's own round trip (see Cell decoration). - Print settings — every field of
ContentSheetPrintSettings: page size and orientation, all four margins, gridline and row/column-header printing, page order, print scale or fit-to-page counts, manual page breaks, the print range, and the repeated header rows and columns — read from and written to the nine worksheet-substream records and the two built-in defined names that carry them, in both directions and verified against real LibreOffice-produced BIFF8 (see Print settings). - Cell alignment — a cell's own horizontal (
left/center/right/justify) and vertical (top/middle) alignment, read from and written toXF's trailingCellXF/StyleXFpayload's own leading word, in both directions and verified against real LibreOffice-produced BIFF8 (see Cell alignment). - Per-cell fonts — a cell's own font (
ContentSheetCell.font: bold/italic/underline/strike, family, size, colour), read from the Font record its XF's ownifntindexes into the workbook's font table and written back into an interned font table the writer builds from the cells it writes (biff/font.tsholds both directions of the one record layout). A cell's font is derived by diffing that entry against the table's own first font — the Normal style's, which is what "the format's default" concretely means for a given file — so only properties the cell genuinely differs in are stated, the same default-omission policy alignment and fill already apply. - Defined names — a workbook's own defined names, read from and written to the
Lblrecords ([MS-XLS] 2.4.150) of the globals substream (workbook/defined-names.ts): the name as spelled (a built-in under its_xlnm.spelling), therefersTorebuilt from the record's compiled Ptg stream by the same parser a cell formula uses, and a sheet-local scope carried asscopeSheetIndextranslated into the document's own sheets-array position. The two print built-ins stay where they already live, on print settings. - Data validation, read-only (ExaDev/documents.js#1098) — a sheet's own
Dvrecords (src/workbook/data-validation.ts) intoContentSheet.dataValidations: type, comparison operator, one or two formulas (via the samePtgreaderFormularecords already use), allow-blank/show-input/show-error flags, error style, and prompt/error titles and messages, resolved directly against [MS-XLS]'s own publishedDv/DVal/DVParsedFormula/SqRefUfield layouts rather than a producer-specific convention. - Conditional formatting, the CF12-era spellings, written (ExaDev/documents.js#1186) — every rule variant the schema models beyond
cellIswrites as one CondFmt12 plus one CF12 record (workbook/conditional-format-write.ts): the text family as genuinect0x02 formula conditions in Excel's own generated shape (the search text as the formula'sPtgStroperand, the identical spelling the reader'sreadCfTextFilterRulerecovers it from), the operand-free family,top10,aboveAverage,timePeriod, and the three visual-scale rules (colorScale/dataBar/iconSet) through their CFGradient/CFDatabar/CFMultistate payloads with CFColor RGB triples and CFVO thresholds compiled through the samecompileFormulaTextcell formulas use. AcellIsrule still writes as a base CondFmt/CF pair; the two families share one sheet, base groups first.ipriorityis minted (smallest unused positive integer) when a rule states nopriority, since [MS-XLS] 2.4.43 requires it present and unique. What genuinely has no BIFF8 spelling throws aBiffWriteErrornaming it — see the Writer scope table below. - Charts, drawings, and images, read; images and non-chart embedded objects, now also written (ExaDev/documents.js#924, #1186) — a worksheet's own MS-ODRAW (Escher) drawing layer (
src/drawing/escher.ts,src/drawing/shapes.ts,src/drawing/blips.ts), the workbook-wide Blip Store every picture shape's ownpibproperty indexes into, and an embedded chart's own nested substream (src/workbook/chart.ts), joined intoContentSheet.imagesandContentSheet.embeddedObjectsbysrc/workbook/drawing.ts— see "Charts, drawings, and images, resolved for real" under Read-side gaps below. The write direction (src/drawing/escher-writer.ts,src/workbook/drawing-writer.ts) builds the identical Escher container tree back out — the workbook-wide Blip Store deduplicated by image bytes, a per-sheet shape tree, and theObjrecords pairing each shape with what it holds — and a non-chart embedded object's own document rides inside a genuine [MS-XLS] Embedding Storage (src/workbook/embedded-object.ts); see Images and embedded objects, written.
Verified primarily by round trip (src/write.test.ts, plus a dedicated test/workers/write.test.ts proving the whole write path inside a real workerd isolate, not just Node): build a ContentDocument, write it, read it back through this package's own independently-pinned reader, and check the result. Every record's own byte layout is additionally cited to its [MS-XLS] section in the writer's source, matching the reader's own convention.
Writer scope
What writeXlsContent/writeXls cover: every ContentCellValue kind a real .xls can hold (number, percentage, currency, boolean, date, time, dateTime, string, error; an empty cell is written as a Blank record when it carries formatting and as nothing at all when it does not — see below), merged ranges (colSpan/rowSpan), row heights and hidden rows, column widths and hidden columns, multiple sheets, explicit and default number formats, a shared string table deduplicated across the whole workbook, every field of a sheet's own print settings, a cell's own alignment, a cell's own background fill and per-side borders (see Cell decoration), a same-sheet formula (see Formula writing), a cell's own comment text and author (see Cell comments, written), a sheet's own images and non-chart embedded objects (see Images and embedded objects, written), a sheet's own data validations (one Dval wrapper plus one Dv record per rule, every schema field through the same compileFormulaText formula compiler cell formulas use), and a sheet's own conditional formats in full: a cellIs rule as one CondFmt plus one CF record (its style's text colour and solid background encoded as a DXFN through the workbook's own palette), every other variant the schema models as one CondFmt12 plus one CF12 record (the CF12-era spelling, its thresholds, template parameters, and CFColor triples written as the exact inverse of the reader's own walk). What it deliberately does not:
| Not written | Why |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Formula records for a 3D (cross-sheet or external-workbook) reference, an array-constant literal or CSE array formula, a defined name, or a call to a function outside Ftab's own vocabulary | Same-sheet formulas write (see Formula writing); these four constructs each throw a BiffWriteError naming the construct instead, rather than emit a token stream the writer cannot prove round-trips. A 3D reference needs a SupBook/ExternSheet pair this writer only ever mints today for the two built-in print-settings names (see Print settings), not for an arbitrary formula; an array constant/CSE formula needs a PtgExtraArray/Array-record trailer this writer does not build; a defined name has nowhere to resolve against, the same gap the Lbl row below describes; and Excel 2007+ added many worksheet functions BIFF8's own Ftab enumeration never named, resolved through a PtgNameX/add-in mechanism this writer does not implement. |
| Per-cell font | The reader does not read one back: ContentSheetCell has no cell-level font field at all (ooxml.js's xlsx reader makes the identical font-scope choice), so writing a real value here would be unverifiable by round trip. Every XF this writer emits still references the same single font. |
| MulBlank/RK/MulRk | Pure compaction optimisations over information a plain Blank/Number/LabelSst/BoolErr record already carries losslessly. (Blank itself is written, for a formatted empty cell — see the empty row below.) |
| An empty-kind cell carrying no formatting | Written as nothing at all, which is what round-trips: content.ts's reader drops an unformatted blank cell it reads, and a merged range's empty anchor is independently reconstructed from MergeCells alone. A formatted one (a fill, a border, or a non-default alignment) is the opposite case — its formatting exists only in the XF a cell record names, so it gets a real Blank record ([MS-XLS] 2.4.20) and round-trips with that formatting intact. |
| A 'chart'-kind embedded object | Writing one means embedding a genuine BIFF8 chart substream — the whole [MS-XLS] chart grammar its series data links drive — which is a chart engine of its own rather than a container for the flattened series table the schema carries; writeXlsContent throws BiffWriteError naming it rather than approximating a chart no BIFF8 record could actually redraw. A sheet's own images and every other embedded-object kind (wordprocessing/presentation/spreadsheet/drawing/formula) do write — see Images and embedded objects, written. |
| Defined names (Lbl) | Read only for the two built-in ones a sheet's print range and repeated header bands live in (Print settings); a user-defined name has nowhere to land in document-schema.js's spreadsheet model, so there is no round trip to verify a writer for it against. |
| A data validation whose formula text compileFormulaText cannot encode | Every other rule shape writes (see the lead paragraph); a formula naming a construct outside the same-sheet Ptg vocabulary the cell-formula writer supports throws the identical BiffWriteError that cell formula already does, rather than emit a token stream this writer cannot prove round-trips. A comparison-typed rule carrying no operator, or a two-operand operator (between/notBetween) carrying fewer or more than its two formulas, is a malformed model and throws naming it. |
| The conditional-format shapes the CF12 record vocabulary itself cannot state | Every rule variant the schema models now writes (workbook/conditional-format-write.ts): a cellIs rule as one CondFmt plus one CF record -- its operator, both formula operands through compileFormulaText, and its style's text colour and solid background as a DXFN resolved through the workbook's own palette, the write-side inverse of readCondFmtGroup/readCf/parseDxfStyle -- and every other variant as one CondFmt12 plus one CF12 record, the CF12-era spelling (ct 0x02 formula condition carrying the search text for the text family, ct 0x05 filter templates for the operand-free/top10/aboveAverage/timePeriod family, CFGradient/CFDatabar/CFMultistate for the visual-scale rules), with an absent priority minted the smallest unused ipriority because [MS-XLS] 2.4.43 requires the field present and unique. What the record vocabulary genuinely diverges from the schema on throws a BiffWriteError naming it rather than being silently dropped or approximated: a timePeriod naming thisYear/lastYear/nextYear (ODF's own calcext extension values -- no icfTemplate names a year-scoped period); an iconSetType outside [MS-XLS] 2.5.36's seventeen built-in sets (a producer-extensible vocabulary with no iIconSet byte); an icon-set threshold count disagreeing with the set the rule names (cStates is pinned to the set's own icon count); stopIfTrue on a colour-scale/data-bar/icon-set rule (2.4.43 pins fStopIfTrue to zero for the visual-scale types); an aboveAverage standard-deviation count above 2 (2.5.23's iParam table admits 0/1/2); and two rules declaring the same priority (contradictory ordering rather than something to renumber). A cellIs rule's own priority/stopIfTrue/source still have no base-CF field and are not written; the CFEx compatibility spelling -- a legacy CF-plus-extension pair keeping a pre-2007 Excel able to evaluate a rule -- is not written either, the CF12 spelling being what this package's own reader resolves either way. |
| A comment's replies or createdAt | A cell's text and author write (see the Status list above); legacy BIFF8's Note/Obj/Txo triple has no threading and no per-comment timestamp at all, the same gap src/workbook/comments.ts's own read side already documents, so there is nothing for these two fields to round-trip against. |
| A page size no iPaperSize code names | Written as [MS-XLS] 2.4.257's own custom-paper value rather than as a named paper it is not — the dimensions themselves are unwritable, since Setup addresses paper only by code. See Print settings. |
| Window1/Window2, CodePage, Index/DBCell, the legacy interface records (InterfaceHdr, WriteAccess, …) | UI and interoperability bookkeeping [MS-XLS]'s own grammar names in the globals/worksheet substreams alongside the content-carrying records above, not data. Index/DBCell specifically is a pure cell-lookup performance optimisation (see [MS-XLS]'s own "Retrieval of Last-Calculated Cell Values Without Loading Cell Table") that this reader — and Excel's own reader — does not require to find a cell; real, well-established minimal BIFF8 writers (e.g. Python's xlwt) omit the same set and produce files Excel opens correctly. The calculation-state records (CalcCount, CalcRefMode, CalcIter, CalcDelta, CalcSaveRecalc) sat in this row until print settings needed them — see Print settings for why the writer emits them now. |
| Continue-chain splitting | A record whose data would exceed the 8224-byte single-record ceiling ([MS-XLS] 2.1.4) — an extremely long shared string, an enormous shared string table, or thousands of merged ranges in one sheet — is refused with a thrown BiffWriteError rather than silently split across Continue records. |
Column widths round-trip to the nearest pixel Excel's own integer-pixel-grid quantization allows (matching the read direction's own "honestly approximate" contract, units.ts), never narrower than requested. A .xls cell outside BIFF8's own grid (65536 rows, 256 columns) is refused rather than silently wrapped or truncated.
Read-side gaps
Each deliberate rather than overlooked:
- Formula expressions, including shared, array, and external-reference formulas. A
Formularecord's compiledPtgtoken stream (src/biff/ptg.ts) is walked and rebuilt into real formula text — literal operands, cell/range references ($-qualified, including 3D cross-sheet references), every arithmetic/comparison/unary/percent operator, explicit parentheses, and both fixed- and variable-arity function calls, resolved by name against [MS-XLS]'s own built-in function table (src/biff/ptg-functions.ts, covering the whole published table — [MS-XLS] 2.5.198.17 — cited to that table's owniftabindex; PtgFunc's fixed argument count is a curated subset of it, since PtgFunc's own token carries no count and only a function [MS-XLS]'s grammar states a fixed, non-optional arity for is resolved through it, empirically confirmed against real LibreOffice-written BIFF8 rather than assumed from the grammar alone). Three constructs the read side used to leaveContentSheetCell.formulasilently absent for are now resolved: a shared formula — aPtgExptoken is joined (src/workbook/sheet.ts'scollectFormulaGroups) against theShrFmlarecord that follows its group's baseFormularecord, and the shared expression's own relative tokens (PtgRefN/PtgAreaN, [MS-XLS] 2.5.198.88/2.5.198.31) are re-expanded for each referencing cell's own row/column, including the format's own sheet-edge wraparound rule, while any ordinary (non-relative) token the same expression carries is resolved once and reused unchanged for every referencing cell; an array (CSE) formula — the samePtgExpjoin against anArrayrecord instead, its expression returned exactly as parsed, with no formula-bar bracing added (Excel's own{...}CSE braces are display syntax the formula-bar shows for an array-entered cell, never characters written into the formula itself — matchingooxml.js's own xlsx convention for the identical construct); and aPtgArrayarray-constant literal ({1,2,3}-style, whether inside an array formula or an entirely ordinary one like=SUM({1,2,3})) — this bracing genuinely is real, retypeable formula syntax, unlike CSE bracing, and its values are read from the token's ownPtgExtraArraytrailer rather than left unresolved. A 3D reference's sheet name resolves throughEXTERNSHEETandSupBookfor both a self-referencing workbook and a genuinely external one —SupBook'svirtPath(the external workbook's own path, decoded for the common relative-path forms: same-drive, startup, alternate-startup, and library-relative) andrgst(its sheet names) recover a real[Workbook.xlsx]Sheet1-style label wherever they can, and that label is trusted as real formula text only when both halves were genuinely recovered from the file. An absolute drive volume, a UNC share, or a transfer-protocol URL invirtPath; avirtPathwhose final file-path segment carries a bracket character anywhere in it, not only one already shaped like the bracketed sheet-name form ([Book.xlsx]Sheet1) — a bracket-named file is legal per [MS-XLS]'s own grammar, and a bracket's position within that final segment does not settle whether it is genuinely that form: the grammar's own bracketed alternative opens with a literal[before any directory at all, so a workbook sitting in a subdirectory ([sub+ a directory separator +Book.xlsx]Sheet1) puts its closing bracket, non-leading, in the very same final segment this reader isolates once split on the separator — a real instance of the bracketed form just as much as the leading-bracket case ([Book.xlsx]Sheet1with no directory at all) is; a bracket elsewhere in an otherwise plain, single-directory file name (My[Draft].xlsx) is the case this reader can actually rule the bracketed form out for, since with no directory separator to split on, that whole segment's own leading character settles it — but returning it as a plain file name would still collide with this reader's own[${fileName}]wrapping of the resolved label, producing a raw, unbalanced bracket inside what should read as one clean[Book.xlsx]-style pair — so the whole segment is declined regardless of where the bracket falls or whether this reader could rule the bracketed form out, trading a known false negative for never emitting a mangled label; aSupBookthat is a DDE/OLE data source, an add-in, a same-sheet, or an unused link in the first place; or a sheet index [MS-XLS] itself marks unresolvable or workbook-level — none of these has a real name to recover, and each leaves the WHOLE containing formula unresolved, exactly like any other construct this reader cannot turn into real formula text, rather than writing a fabricated placeholder into what a spreadsheet application would otherwise treat as live formula content. What remains genuinely unresolved for the same reason, still leavingformulaabsent for that cell: aPtgTbldata table, a defined name (PtgName/PtgNameX, for the same reasonLblis not read at all yet — see below), and a natural-language "Elf" reference. - Cell decoration and alignment resolved for real; font still not.
XF's trailingCellXFpayload's fill pattern/colours and per-side border style/colour are read and resolved through thePaletterecord (or the default colour table when one is absent) — see Cell decoration for the full mapping, including every named pattern beyond solid, and how this was verified against real LibreOffice-produced BIFF8. The same payload's leading word is resolved too — see Cell alignment for the fullalc/alcVmapping.Fontrecords are not read at all:ContentSheetCellhas no cell-level font field, andooxml.js's xlsx reader makes the identical scope choice. - Print settings, resolved for real. Every field of
ContentSheetPrintSettingsis read from the records that carry it, with Excel's own "Normal" preset standing in per field for whatever the file leaves unstated — see Print settings for the record map, the two of BIFF8's own conditional rules that decide how to readSetup, and the three things that genuinely do not come through. - Cell comments, resolved for real (ExaDev/documents.js#949). A legacy BIFF8 comment is split across three record kinds a single self-contained xlsx
<comment>element never needs: aNoterecord ([MS-XLS] 2.4.179, wrapping aNoteShstructure) anchors the comment to a cell and names its author, but carries no text of its own; the text lives in aTxorecord ([MS-XLS] 2.4.329), its characters carried across trailingContinuerecords; the two are joined through anObjrecord ([MS-XLS] 2.4.181) whoseFtCmonames an object id and type, theNote's ownidObjfield naming that same id, and aTxoalways immediately following theObjrecord whose shape it belongs to (src/workbook/comments.ts). Read in one independent pass over the whole worksheet substream rather than assuming stream order between aNoteand theObj+Txopair it names -- both are resolved regardless of which comes first. Legacy BIFF8 has no threading and no per-comment timestamp at all (unlike xlsx's own[MS-XLSX]threaded-comments extension), soContentSheetCellComment.repliesand.createdAtare never populated from an.xlssource; rich per-character formatting within a comment (TxORuns) is skipped rather than modelled, the same scope limit this reader already applies to the shared string table's own rich runs, sinceContentSheetCellComment.textis a plain string with nowhere to carry them. - Data validation, resolved for real (ExaDev/documents.js#1098).
Dv([MS-XLS] 2.4.95) is a fixed, published binary structure -- a bit-packed flagsDWORD(valType/errStyle/fAllowBlank/fShowInputMsg/fShowErrorMsg/typOperator), fourXLUnicodeStrings (prompt/error titles and messages), one or twoDVParsedFormulastructures, and a trailingSqRefUrange list -- read directly against that spec (src/workbook/data-validation.ts), unlikecalcext:condition's own LibreOffice-source-transcribed mini-language odf.js's equivalent reader needed. ADVParsedFormula's ownrgceis the identicalPtgtoken grammar a cell'sFormularecord carries, so its formula text is recovered with the sameparseFormulaTextthis reader already uses for ordinary cell formulas.DVal([MS-XLS] 2.4.96), the record that precedes a sheet's ownDvcollection, carries only input-window UI state (position, drop-downObjreference) with no counterpart inContentSheetDataValidationSchemaand is not read for its own fields. - Charts, drawings, and images, resolved for real (ExaDev/documents.js#924). A worksheet's own
MsoDrawingrecords ([MS-XLS] 2.1.7.20.3-adjacent,RECORD_MSODRAWING) carry MS-ODRAW (Escher) bytes that split across as many records as the drawing needs and simply concatenate, in stream order, into one continuous Escher record tree (src/drawing/escher.ts, reading [MS-ODRAW]'s ownOfficeArtRecordHeader/container-vs-atom framing directly against its spec rather than from memory). Within that tree,src/drawing/shapes.tswalks theDgContainer/SpgrContainer/SpContainerhierarchy for each real top-level shape's own type, id, and cell anchor (ClientAnchorSheet, [MS-XLS] 2.5.163 — oneSpatom'srecInstanceandClientAnchor's corner-cell-plus-fractional-offset pair, 1/1024ths of a cell's width and 1/256ths of its height), skipping the invisible "patriarch" group root every drawing wraps its real shapes in. A picture shape's ownOptproperty table names apib— a 1-based index into the workbook-wide Blip Store (OfficeArtBstoreContainer, read once from the globals substream's ownMsoDrawingGroupstream,src/drawing/blips.ts) — resolved to real image bytes for a PNG or JPEG blip (OfficeArtBlipPNG/OfficeArtBlipJPEG's own literal file bytes, past theirrgbUidheader); a DIB/EMF/WMF/PICT/TIFF blip is recognised but produces no image, sinceContentImageBlockSchema.formathas no lossless slot for any of them. Since neither an Escher shape nor aMsoDrawingrecord names what kind of object it actually holds, each shape is paired 1:1, in document order, with the worksheet substream's ownObjrecords (skipping any Note-typeObjsrc/workbook/comments.tsalready accounts for) — the same positional correlation every other real BIFF8 reader relies on. AnObjnaming a chart locates the chart's own nestedBOF(dt=0x0020)/EOFsubstream — which sits INSIDE the worksheet substream, immediately after the shape that anchors it ([MS-XLS] "Chart Area") — viasrc/biff/substreams.ts's own stack-based substream nesting (a genuinely new capability this issue needed:splitSubstreamspreviously treated any BOF met mid-substream as ending it outright, which would have silently dropped every worksheet record written after an embedded chart).src/workbook/chart.tsreads that substream'sSeries/AI(BRAI)/SeriesTextrecords — a series' own name and its category/value data links — resolving each link's value either from the chart's own on-diskSERIESDATAcache (Dimensions/SIIndex/Number/BoolErr/Blank/Label, present when the chart's data lives on another sheet or is genuinely external) or, for the common case of a chart plotting its own sheet's cells, by resolving theAI'sPtgArea3d/PtgRef3dreference token directly against that sheet's own already-mapped cells — and lands as aContentEmbeddedObjectwithobjectKind: 'chart', its cached series/category table flattened into a small one-sheet spreadsheetContentDocument, the identical shapeooxml.js's own xlsx/pptx chart reading already produces (ExaDev/documents.js#719):document-schema.jshas no richer, chart-type-aware object model yet, so this reader targets the fallback that exists rather than inventing one. Every other recognised shape (an autoshape, a text box, a line, a group) lands asobjectKind: 'drawing', a single-pageContentDocumentholding one sized-and-positionedContentShapewith no text content modelled. Not modelled at all: a shape's own rotation, fill/line formatting, or text runs; a group's own nested transform (a nested nested group's shapes are still recovered, cell-anchored, but a group's own rotation/scale is not composed into its children's positions); and a chart's own type, axes, and legend, whichdocument-schema.js's flattened-table representation has no field for regardless of what this reader could recover. - Conditional formatting, base rules resolved for real (ExaDev/documents.js#1102). A
CondFmtrecord ([MS-XLS] 2.4.56) marks the start of one to threeCFrecords ([MS-XLS] 2.4.42) sharing one cell-range list, read together as a group (src/workbook/conditional-format.ts) since aCF's own ranges live on its parentCondFmt, not on itself. Base BIFF8 (Excel 97) conditional formatting has exactly two rule shapes: a comparison ("Cell Value Is") condition, promoted toContentSheetConditionalFormat'scellIsvariant with its formula(s) recovered through the sameparseFormulaTextthis reader already uses for cell formulas andDv; and a formula condition, left unpromoted for the same "no closed-form structure without a general formula engine" reasondata-validation.tsandooxml.js's own xlsxcfRulereading already draw. A rule's resulting style -- font colour and fill background -- comes from itsCF's ownDXFNstructure, resolved through the identicalicv/Palettecolour machinery a regular cell's fill and border already use (Cell decoration); aDXFNnaming a user-defined number format (DXFNumUsr) degrades that one rule's style to absent rather than risk misreading the font/fill fields that follow it, since [MS-XLS]'s own prose forDXFNumUsr's length field does not settle whether it counts itself. Every richer rule type Excel 2007+ added has no representation in the baseCFrecord at all; it rides aCF12record instead, or, for a rule Excel keeps expressible as a legacy formula condition for pre-2007 readers, aCFExextension record that attaches richer metadata to an ordinaryCondFmt-ownedCFwithout promoting it into aCF12record at all. - Conditional formatting, CF12's colour scale/data bar/icon set rules resolved for real (ExaDev/documents.js#1104).
CF12's ownctfield picks one of six rule shapes ([MS-XLS] 2.4.43); this reader promotes the three that share a genuine array-of-thresholds building block already modelled indocument-schema.js-- colour scale, data bar, and icon set (src/workbook/conditional-format-12.ts) -- each reading an array ofCFVOthreshold objects (the identicalnum/percent/max/min/formula/percentilevocabularyContentSheetConditionalFormatValueSchemaalready carries forooxml.js's xlsx reader andodf.js's ods reader) paired withCFColorvalues. ACFColornaming an indexed colour resolves through the sameicv/Palettemachinery as baseCF's ownDXFN, then Excel's own TintAndShade model applies the colour's own tint/shade Xnum (xf-colors.ts'sapplyTint); one naming a plain RGB triple resolves directly, tint included; one naming an automatic or theme colour degrades the whole rule to absent, since this package has no BIFF8Themereader to resolve a theme reference against. Icon set's owniIconSetbyte maps to the identical ECMA-376ST_IconSetTypevocabulary (3Arrows,3TrafficLights1,5Quarters, and the rest of the seventeen built-in sets)ooxml.js's xlsxcfRulereading already carries as a plain string. A record longer than the 8224-byte single-record ceiling continues ontoContinueFrt12rather than the plainContinueevery other record in this package joins against;groupRecords(src/biff/substreams.ts) strips its own restatedFrtRefHeaderbefore joining it. - Conditional formatting, CF12's filter-dispatched template rules resolved for real (ExaDev/documents.js#1106).
ct0x05 ("filter") dispatches further throughicfTemplate, alongside a 16-byteCFExTemplateParamsblock always present regardless ofct. Of that block's five variants, only two need real parsing --CFExFilterParams(top10's ownfTop/fPercent/iParam) andCFExAveragesTemplateParams(the aboveAverage family's own standard-deviation count) -- sinceCFExDefaultTemplateParams(duplicateValues/uniqueValues/the four blank/error conditions) is 16 reserved bytes andCFExDateTemplateParams's owndateOpfield is a fixed restatement oficfTemplatefor all ten date/time periods, so both dispatch directly officfTemplatewith no further byte reading. Unlike colour scale/data bar/icon set, [MS-XLS] does not force act0x05 rule's owncbDxfto zero, so itsDXFN12can carry a genuine font/fill override; that structure resolves through the identicalparseDxfStylebaseCF's ownDXFNalready uses. A malformediParamof zero degrades a top10 rule to absent rather than promoting arankContentSheetConditionalFormatSchemaitself requires to be positive. - Conditional formatting,
containsText/notContainsText/beginsWith/endsWithandCFEx's own legacy-CFextension resolved for real (ExaDev/documents.js#1100).icfTemplate0x0008 ("Contains text") is the one filter-dispatched template that is NOT actually act0x05 rule:CFExTextTemplateParamscarries onlyctp, naming which of the four text sub-types a rule is, and neither it norct0x05's ownCFFilterrgbCThas anywhere to carry the literal search text (confirmed against a second, independent transcription of both structures -- [kinkou/unxls](https://github.com/kinkou
