Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/doc-codec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ Built and shipped, on the read side:
- **`isDocBytes`** — distinguishes a `.doc` from the `.xls`, `.ppt` and OLE embeddings that share its container, by looking for a `WordDocument` stream carrying `FibBase.wIdent`.
- **Document metadata** — `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` read from a `"\x05SummaryInformation"` stream when one is present (see [Metadata](#metadata)); `comments` and `lastPrintedIso` remain unread, since `LayoutMetadata` has no field for either.
- **Numbering definitions** — `readDocContent`'s own `numbering` field: every list's glyph/format, level-text template, and start-at value, resolved from `PlfLst`/`PlfLfo` and keyed by the same `listId` a paragraph's `ContentParagraph.list.numId` already carries. Read-only; see [Numbering definitions](#numbering-definitions).
- **The one section's own page size and margins** — `prop/sep.ts`'s `readSectionProperties` resolves `PlcfSed`/`Sepx` (`sprmSXaPage`/`sprmSYaPage`/`sprmSDxaLeft`/`sprmSDxaRight`/`sprmSDyaTop`/`sprmSDyaBottom`) into `ContentSection.pageSize`/`.margins`, falling back field by field to Word's own new-document default (US Letter, one-inch margins) for a document carrying no `PlcfSed` at all, or for any one sprm it leaves unstated. This reader only ever resolves the section spanning the whole main document — the "Section boundaries" row below states what "one section" still means.

Built and shipped, on the write side — see [Writing](#writing) for the full scope statement:

- **`writeDocContent`** — a `'wordprocessing'` `ContentDocument` (one section, paragraphs of runs and tables) to genuine [MS-DOC] bytes: a real piece table, real `ChpxFkp`/`PapxFkp` pages (splitting across as many as a document's own formatting needs, not just the common one-page case), a spec-conformant empty style sheet, a font table when a run names one, and a `"\x05SummaryInformation"` stream when the input's metadata carries anything that stream can hold (see [Metadata](#metadata)) — wrapped in a real [MS-CFB] compound file via `archive-codec`'s `writeCompoundFile`. A `ContentTable` block is expanded by `table/write.ts`'s `flattenSectionBlocks` into the same flat paragraph sequence every other block already is (see [Tables](#tables)), so table paragraphs flow through the identical `ChpxFkp`/`PapxFkp` paging as every other paragraph rather than a separate table-only path. Each cell's own `background` and `borders` ride along into the row's TAP (see [Cell decoration](#cell-decoration)).
- Every property `writeDocContent` writes is verified by reading it back through this package's own `readDocContent` (`src/write.test.ts`), and additionally against a real, independent [MS-DOC] implementation: LibreOffice opened, rendered, and re-exported a `writeDocContent` sample without error or content loss, including bold/italic/underline/strike/size/colour/font-family runs, paragraph alignment and indentation, non-Latin-1 and non-BMP text (accented Latin, CJK, an emoji surrogate pair), and a table — recognised as a genuine `table:table`, its row/column/cell structure and both horizontal and vertical merges intact, matching real `table:number-columns-spanned`/`table:number-rows-spanned` attributes and `table:covered-table-cell` elements, and each cell's own background fill and per-side borders recovered with their exact colours, exactly as [Tables](#tables) confirms in full.
- **`writeDocContent`** — a `'wordprocessing'` `ContentDocument` (one section, paragraphs of runs and tables) to genuine [MS-DOC] bytes: a real piece table, real `ChpxFkp`/`PapxFkp` pages (splitting across as many as a document's own formatting needs, not just the common one-page case), a spec-conformant empty style sheet, a font table when a run names one, a real `PlcfSed`/`Sepx` stating the one section's own page size and margins, and a `"\x05SummaryInformation"` stream when the input's metadata carries anything that stream can hold (see [Metadata](#metadata)) — wrapped in a real [MS-CFB] compound file via `archive-codec`'s `writeCompoundFile`. A `ContentTable` block is expanded by `table/write.ts`'s `flattenSectionBlocks` into the same flat paragraph sequence every other block already is (see [Tables](#tables)), so table paragraphs flow through the identical `ChpxFkp`/`PapxFkp` paging as every other paragraph rather than a separate table-only path. Each cell's own `background` and `borders` ride along into the row's TAP (see [Cell decoration](#cell-decoration)).
- Every property `writeDocContent` writes is verified by reading it back through this package's own `readDocContent` (`src/write.test.ts`), and additionally against a real, independent [MS-DOC] implementation: LibreOffice opened, rendered, and re-exported a `writeDocContent` sample without error or content loss, including bold/italic/underline/strike/size/colour/font-family runs, paragraph alignment and indentation, non-Latin-1 and non-BMP text (accented Latin, CJK, an emoji surrogate pair), a section's own page size and all four margins (a non-default 600x800pt page with a 90/54/45/36pt left/right/top/bottom margin, confirmed against LibreOffice's own `fo:page-width`/`fo:page-height`/`fo:margin-*` export to the exact point), and a table — recognised as a genuine `table:table`, its row/column/cell structure and both horizontal and vertical merges intact, matching real `table:number-columns-spanned`/`table:number-rows-spanned` attributes and `table:covered-table-cell` elements, and each cell's own background fill and per-side borders recovered with their exact colours, exactly as [Tables](#tables) confirms in full.

**Not built, and not approximated, on either side.** Each of these is a genuine layer of [MS-DOC] that this package does not implement; none is silently faked, and a document using one reads (or fails to write) as though it did not:

Expand All @@ -38,7 +39,7 @@ Built and shipped, on the write side — see [Writing](#writing) for the full sc
| **Images and drawn objects** | The anchor characters (`U+0001`, `U+0008`) are dropped rather than emitted as control characters. No picture data is read. `writeDocContent` refuses an image block. |
| **Style-inherited formatting** | A style's own property sets live in the `STD`'s `grLPUpxSw` and are not read, so a paragraph's formatting is the document defaults plus its own direct exceptions. A `Heading 1` paragraph reports its `styleId` and `headingLevel` but not the boldness or size its style would supply. `writeDocContent` writes no paragraph styles at all (every paragraph is `istd` 0) and does not round-trip `styleId`/`headingLevel`. |
| **Subdocuments** | Only the main document (character positions 0 to `ccpText`) is converted. Footnotes, endnotes, headers, footers, comments and text boxes are not, in either direction. |
| **Section properties** | Section boundaries are not read, so the whole document is one section, and its page size and margins are a US Letter placeholder rather than the document's own. `writeDocContent` refuses a `ContentDocument` with more than one section, rather than silently merging their content into what would read back as one. |
| **Section boundaries** | Section boundaries themselves are not read the whole document is always treated as one section, spanning the entire main document — so a multi-section `.doc`'s later sections' own page size and margins are never consulted, only the first's (see the read-side page-size/margins bullet above for what is now read of that one section). `writeDocContent` refuses a `ContentDocument` with more than one section, rather than silently merging their content into what would read back as one. |
| **Extended and user-defined document properties** | `title`/`subject`/`author`/`keywords`/`createdIso`/`modifiedIso` are read from and written to a `"\x05SummaryInformation"` stream when present (see [Metadata](#metadata)); the sibling `"\x05DocumentSummaryInformation"` stream (company, manager, and custom user-defined properties) is not read or written at all. |
| **Encryption** | An encrypted or XOR-obfuscated document is refused with a `DocUnsupportedError` rather than read as plaintext. `writeDocContent` never encrypts. |
| **`sprmPHugePapx` / `sprmPTableProps`** | Paragraph properties stored indirectly in the Data stream are not followed, so such a paragraph reads with fewer properties than it states. [MS-DOC] 2.4.3's own Overview of Tables text names `sprmPTableProps` as a real, legal alternative to `sprmTDefTable` some applications process — but a real producer's row mark is not shown to prefer it: a genuine LibreOffice-authored `.doc` table's own row mark states its TAP through the identical direct `sprmTDefTable` this package's reader and writer already use (confirmed by parsing a LibreOffice 26.2.5.2-authored table's raw `PapxFkp` bytes; see [ExaDev/documents.js#892](https://github.com/ExaDev/documents.js/issues/892)), matching 2.4.3's own compatibility guidance ("An application SHOULD use sprmTDefTable to define table cells for applications that do not process sprmPTableProps"). `writeDocContent` never writes an indirect Papx. |
Expand Down
5 changes: 5 additions & 0 deletions packages/doc-codec/src/fib/fib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export interface Fib {
readonly fcClx: number;
readonly lcbClx: number;

readonly fcPlcfSed: number;
readonly lcbPlcfSed: number;

readonly fcSttbfFfn: number;
readonly lcbSttbfFfn: number;

Expand Down Expand Up @@ -119,6 +122,8 @@ export function parseFib(wordDocument: Uint8Array): Fib {
lcbPlcfBtePapx: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfBtePapx),
fcClx: fcLcb(FC_LCB_VALUE_INDEX.fcClx),
lcbClx: fcLcb(FC_LCB_VALUE_INDEX.lcbClx),
fcPlcfSed: fcLcb(FC_LCB_VALUE_INDEX.fcPlcfSed),
lcbPlcfSed: fcLcb(FC_LCB_VALUE_INDEX.lcbPlcfSed),
fcSttbfFfn: fcLcb(FC_LCB_VALUE_INDEX.fcSttbfFfn),
lcbSttbfFfn: fcLcb(FC_LCB_VALUE_INDEX.lcbSttbfFfn),
fcPlfLst: fcLcb(FC_LCB_VALUE_INDEX.fcPlfLst),
Expand Down
3 changes: 3 additions & 0 deletions packages/doc-codec/src/fib/offsets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const LW_OFFSET = {
export const FC_LCB_VALUE_INDEX = {
fcStshf: 2,
lcbStshf: 3,
// Pair 6, counted the same way as the fcSttbfFfn comment below counts pair 15: fcStshfOrig(0), fcStshf(1), fcPlcffndRef(2), fcPlcffndTxt(3), fcPlcfandRef(4), fcPlcfandTxt(5), fcPlcfSed(6).
fcPlcfSed: 12,
lcbPlcfSed: 13,
fcPlcfBteChpx: 24,
lcbPlcfBteChpx: 25,
fcPlcfBtePapx: 26,
Expand Down
3 changes: 3 additions & 0 deletions packages/doc-codec/src/fib/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface FibWriteSpec {
readonly cbMac: number;
readonly fcClx: number;
readonly lcbClx: number;
readonly fcPlcfSed: number;
readonly lcbPlcfSed: number;
readonly fcPlcfBteChpx: number;
readonly lcbPlcfBteChpx: number;
readonly fcPlcfBtePapx: number;
Expand Down Expand Up @@ -83,6 +85,7 @@ export function buildFib(spec: FibWriteSpec): Uint8Array<ArrayBuffer> {
);
pair(FC_LCB_VALUE_INDEX.fcSttbfFfn, spec.fcSttbfFfn, spec.lcbSttbfFfn);
pair(FC_LCB_VALUE_INDEX.fcClx, spec.fcClx, spec.lcbClx);
pair(FC_LCB_VALUE_INDEX.fcPlcfSed, spec.fcPlcfSed, spec.lcbPlcfSed);
// cswNew (the 2 bytes at FIB_FC_LCB_BLOB_OFFSET + blobBytes) stays 0, which [MS-DOC] mandates for nFib 0x00C1 and which correctly leaves fibRgCswNew absent.

if (readUint16LE(bytes, 0) !== FIB_W_IDENT) {
Expand Down
2 changes: 2 additions & 0 deletions packages/doc-codec/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export * from "./prop/chp";
export * from "./prop/chp-write";
export * from "./prop/pap";
export * from "./prop/pap-write";
export * from "./prop/sep";
export * from "./prop/sep-write";
export * from "./style/stsh";
export * from "./style/fonts";
export * from "./list/numbering";
Expand Down
Loading