js.documents 1.67.14 → 1.67.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -566,11 +566,11 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
566
566
  - **`LayoutPathSchema` (`document-schema.js`) has no quadratic or elliptical-arc segment kind, deliberately — not a scope gap that happens to be unfilled.** `writePath` (pdf-codec's own `content-write.ts`) therefore has no quadratic-to-cubic elevation and no SVG-arc-to-cubic endpoint-to-centre parameterization anywhere in it: `odf.js`'s own real-LibreOffice-output-verified `svg:d` parser (`typed/shared/path.ts`) recognises `S`/`s`/`Q`/`q`/`T`/`t`/`A`/`a` as command letters (so its own token stream stays in sync) but produces no segment for any of them — real LibreOffice output for rectangles, ellipses, freeform curves, and basic custom-shape presets never emits a quadratic or an arc in the first place, only `M`/`L`/`H`/`V`/`C`/`Z`. Building unused quadratic/arc conversion code against a segment kind that can never occur would be speculative, not root-cause work.
567
567
  - **A drawing page's `shapes` and `vectors` are two separate arrays, but their true relative paint order is carried by a shared `paintOrder` field on both.** `ContentDrawPageSchema` (`document-schema.js`) still keeps text/image/table content (`shapes`) and vector primitives (`vectors`) apart, but `ContentVector` and `ContentShape` each carry a `paintOrder` — one monotonically increasing per-page document index `odf.js`'s own reader stamps on every element it walks (`typed/draw/shapes.ts`'s `walkDrawPageContent`/`paintOrderKey`, honouring a real `draw:z-index` where a producer wrote one, falling back to document position otherwise). `convertDrawingToLayout` merges the two arrays back into one true-paint-order walk through that field (`src/model/paint-order.ts`'s `mergeByPaintOrder`), `reconstructDrawing` stamps the same field from its own single walk over a page's recovered items, and `buildOdgPackage` appends in the same merged order (document order *is* paint order in a written `.odg` — this package never emits a `draw:z-index`). A page that genuinely interleaves the two mid-stack — a text label between two rectangles, a rectangle over a picture — consequently paints in the order its author built it, and survives `convertDrawingToLayout` → `reconstructDrawing` with that interleaving intact. The historical "every vector paints before every shape" rule survives only as the documented fallback for a page missing `paintOrder` anywhere (a hand-built `ContentDocument`, or one produced before the field existed), since an item with no value has no defensible position to be sorted into and inventing one would silently reorder content.
568
568
  - **A rotated vector primitive renders as a `LayoutPath`, not as a rotated `LayoutRect`/`LayoutEllipse` — because neither of those carries a rotation field at all.** `ContentVectorSchema`'s `rect`/`ellipse`/`path` variants each carry a real `rotationDeg` (the `line` variant does not, and needs none — two endpoints already encode any orientation a line can have), `odf.js`'s own reader resolves one through the same `resolveOdfShapeGeometry` a `draw:frame` uses, and `OdgBoxVector.rotationDeg`/`OdgPathVector.rotationDeg` write one back through the same shared `applyOdfGeometry` (`src/edit/geometry.ts`) `OdpShape.rotationDeg` uses. `convertDrawingToLayout` resolves a rotated vector into a `LayoutPath` whose own points are the shape's corners/curve controls after rotation — a rotated rect becomes a genuine four-point closed subpath, a rotated ellipse its own four cubics rotated — since `LayoutRectSchema`/`LayoutEllipseSchema` model no rotation and only `LayoutText`/`LayoutImage` do (pdf-codec rotates those two through a text/image transformation matrix, which a path-painting operator sequence has no equivalent of). Nothing is approximated by this: an affine rotation maps a straight edge to a straight edge and a cubic Bézier to a cubic Bézier exactly. What a PDF round trip cannot preserve is the `rotationDeg` *field* — a recovered path records where the corners ended up, never that a right-angled box was turned to get there — so `pdfToOdg` returns a rotated rect as an unrotated `path` vector whose geometry is genuinely rotated, the same kind-narrowing every other vector already documents below.
569
- - **`ContentVector`'s `path` variant's `fillRule` is never populated by the reader always `undefined`, which `writePath` treats as nonzero.** `odf.js`'s `readDrawPathVector` does not currently resolve an evenodd fill rule from real ODF output, so every path this pipeline reads paints with PDF's default nonzero winding rule. `LayoutPathSchema`/`writePath` fully support `fillRule: 'evenodd'` regardless a caller constructing a `LayoutPath` (or a future `ContentVector` producer) directly can still set it; it just never arrives via `odgToPdf` today.
569
+ - **`ContentVector`'s `path` variant's `fillRule` is read from real ODF markup.** `odf.js`'s `readOdfFillAndStroke` (`src/typed/draw/shapes.ts`) resolves the real `svg:fill-rule` attribute (`nonzero`/`evenodd`) when a path declares one, tested against real fixtures including a two-subpath "letter O" donut shape proving the attribute survives the full `readDrawPageContent` path `fillRule` is `undefined` only when the source markup genuinely has no `svg:fill-rule` attribute at all, in which case `writePath` correctly falls back to PDF's default nonzero winding rule.
570
570
  - **A cell's declared border still renders solid whatever its `style` says — but the reason moved from "the layout schema has nowhere to carry a dash pattern" to "pdf-codec doesn't read the field yet".** `ContentSheetCellSchema` and `ContentTableCellSchema` both carry real per-cell `background`/`borders` (and `ContentSheetCellSchema` also `alignment`/`verticalAlignment`), `odf.js`'s own reader populates every one of them from a cell's resolved style chain, and `sheets.ts`/`engine.ts` render all of them: a background becomes a real `LayoutRect`, each declared border edge a real `LayoutLine`, and a cell's own alignment/vertical alignment override the value-kind default and the bottom default respectively. `ContentBorder.style` (`solid`/`dashed`/`dotted`/`double`) now genuinely reaches the `LayoutDocument`: as of `document-schema.js` 2.1.0, `LayoutLineSchema`/`LayoutPathSchema` both carry that same optional `style` enum, and `pushCellBorderLines` (`src/layout/shared.ts`, shared by both `engine.ts`'s docx/odt table-cell borders and `sheets.ts`'s ods/xlsx sheet-cell borders) sets it from `ContentBorder.style` on every emitted `LayoutLine` — the model-to-model plumbing this task closed. What still renders solid regardless is the PDF byte output: the installed `pdf-codec` 1.8.0's own `content-write.ts` `writeLine`/`writePath` always emit a plain stroke (`S`) operator with no dash array, so a non-solid `style` has nowhere to render differently yet — a real, separate, pdf-codec-side gap now, not a documents.js schema one. Rendering `double` as two hand-offset parallel lines was still considered and rejected regardless of which package's gap this is: the offset distance is nowhere in the model, so it would be an invented constant standing in for information the source never carried.
571
571
  - **Ordinary text in PDF output now resolves through a real font registry rather than the standard 14 alone, and the standard 14 are only the last resort in that chain.** In order: the source document's own embedded faces (docx's `word/fontTable.xml`, pptx's `p:embeddedFontLst`, ODF's `office:font-face-decls` — see [Fonts](#fonts)), then any face the caller supplied through `options.fonts`, then pdf-codec's vendored Carlito/Caladea faces (genuinely metric-compatible with Calibri/Cambria, and embedded as real subsetted TrueType programs), then the standard 14. Helvetica/Times-Roman remain metric-compatible substitutes for Arial/Times New Roman, so a document asking for either still resolves to a standard font and embeds nothing. What is still not covered: a family with no embedded face, no caller-supplied face, and no vendored substitute — Aptos, say, or any third-party typeface — still renders through the nearest standard-14 face with a width-correction factor, so line wrapping and pagination will drift slightly from what Word itself would produce. Expect a faithful visual approximation there, not a line-identical reproduction. MathML formula rendering (`odfToPdf`, and formulas embedded inside odt/odp) is separate from all of this and always was: it embeds the real STIX Two Math font, which is not a registry-resolvable face and cannot be overridden by `options.fonts` — see the CFF-embedding gotcha below, and pdf-codec's own README, for the exact scope of that embedding (the whole `CFF ` table, not glyph-subsetted).
572
572
  - **Justified paragraphs now stretch real inter-word gaps in all three layout engines — the flow one (`engine.ts`), the direct-placement one (`slides.ts`), and the spreadsheet one (`sheets.ts`).** `justifyLineGapsPt` (`src/layout/shared.ts`) recovers each wrapped line's own word-gap positions from a line's per-fragment `xOffsetPt` (a genuine gap wider than floating-point noise means a space stood there; two touching fragments are one word split across a run boundary, and stay touching), divides the line's slack evenly across every detected gap, and returns an all-zero shift whenever there is nothing to stretch (fewer than two fragments, no detected gap, or a line already at or past its target width — this function only ever adds space, never compresses). `layoutParagraphFlow`/`layoutParagraphInCell` (`engine.ts`, covering docx/odt paragraphs, docx/odt tables, and odm-assembled chapters) and `layoutParagraph` (`slides.ts`, covering pptx/odp shape text and slide-table cells) call it for every wrapped, non-final line of a `'justify'`-aligned paragraph; the paragraph's own final line (or a paragraph that never wraps at all) stays left-aligned, matching Word/LibreOffice/Impress's own convention. `sheets.ts`'s `renderCellText` calls it too, but a spreadsheet cell only ever renders one line by this module's own documented scope, so the "non-final line" case only arises when a cell's source text carries an explicit line break — `wrapRunsToWidth` then produces more than one line, of which only the first is ever rendered, and that first line is the genuinely non-final one a justified cell stretches; justification is skipped outright when that line already overflowed its cell (the numeric-`###`/string-spill-or-truncate fragments no longer reflect the natural layout the stretch needs) or for an ordinary single-line cell, matching every real spreadsheet application's own "justify only wraps, never a single line" behaviour. `alignmentOffsetPt` itself still returns `0` for `'justify'` in all three files, unchanged — the whole-line offset it computes is the wrong shape for inter-word stretching, which is why the stretch lives in a second, sibling function each caller applies on top, not a new branch inside it. No known gap remains in inter-word justification across any layout engine this package has.
573
- - **Reading arbitrary real-world PDFs, encrypted-PDF support, and unsupported image filters (`CCITTFaxDecode`/`JBIG2Decode`/`JPXDecode`) are all pdf-codec's own scope boundaries now, not this package's.** In short: the parser targets cleanly-generated output from mainstream producers rather than adversarial-input robustness; `/Encrypt` in the trailer throws rather than attempting decryption, even for the common empty-user-password case; scanned-fax and JPEG2000 images are skipped with a diagnostic while the rest of the page still reads (JPEG and PNG both pass through losslessly). See pdf-codec's own README for the full statement of each.
573
+ - **Encrypted-PDF support and `CCITTFaxDecode`/`JBIG2Decode`/`JPXDecode` image decoding are all real, implemented capabilities in pdf-codec now, not scope boundaries.** An encrypted PDF is readable whenever it opens without a real password: pdf-codec's `src/encrypt.ts` implements the full Standard Security Handler (RC4 and AES-128/256, revisions 2-6, `/EncryptMetadata false` handling, empty-user-password verification), throwing the distinct `PdfPasswordRequiredError` only when a genuine user password is needed and `PdfEncryptedError` only for a handler/version this codec doesn't implement (public-key encryption, say). CCITT Group 3/4 fax, JBIG2, and JPEG2000 images all genuinely decode via hand-written decoders (`src/image/{ccitt,jbig2,jpeg2000}.ts`), falling back to a diagnostic only for a specific feature within one of those formats the decoder doesn't cover — not unconditionally, as an earlier version of this note claimed. What remains a genuine, permanent scope boundary is adversarial/badly-malformed-input robustness: the parser targets cleanly-generated output from mainstream producers rather than the hardening a 15+-year-old library has. See pdf-codec's own README for the full statement of each.
574
574
  - **PDF → docx/pptx/odt/odp reconstruction recovers a table only from a real drawn gridline lattice, and never from text alignment.** `reconstructWordprocessing`/`reconstructPresentation` run the identical detector, thresholds, and span-consistency check `reconstructSpreadsheet` gates its own cell boundaries on (`src/layout/lattice.ts`), and synthesize a real `ContentTable` when — and only when — one fires. Aligned columns of text with wide gaps are deliberately **not** accepted as evidence: several left-aligned lines separated by a tab-sized gap are indistinguishable, from geometry alone, from a genuinely tabbed paragraph, an indented code sample, or a two-column page layout, so building a table out of one would be inventing structure the source never had rather than recovering structure it did. A wide horizontal gap on a line still becomes a tab character, exactly as before. A lattice with no text inside it is rejected too (a grid of empty boxes is far more likely a decorative frame, a chart's plot area, or a form's field outlines than a table). Where a table IS recovered it reaches the output bytes for real — `buildDocxPackage`/`buildOdtPackage` write a real table, `buildPptxPackage`/`buildOdpPackage` a real slide table — with column widths and row heights measured directly from the drawn boundaries, and the lattice's own strokes reported once, as the table's structure, rather than also as loose vectors alongside it. Gradients and shadings are still not recovered at all.
575
575
  - **A merged table cell (`colSpan`/`rowSpan`) now round-trips as merged, not as an ordinary unmerged one, through `buildDocxPackage`/`buildOdtPackage`** — and docx and ODF express a merge through two genuinely different conventions, so the two writers (`src/edit/docx/content.ts`, `src/edit/odt/content.ts`) are not mirror images of each other. docx collapses a horizontal merge into ONE real `w:tc` carrying `w:tcPr/w:gridSpan` — no element at all for the columns it consumes — while a vertical merge still needs one real `w:tc` per covered row, marked `w:tcPr/w:vMerge` (`w:val="restart"` on the top cell, a bare `<w:vMerge/>` on each covered row below); `ContentTable.rows[].cells` therefore has exactly one array entry per REAL `w:tc`, which can be fewer than the table's own column count. ODF, by contrast, always writes one array entry per grid position regardless of merge direction: a covered column in the SAME row gets a real `table:covered-table-cell` placeholder element (not just an attribute), and so does a covered row below a `rowSpan` — `table:number-columns-spanned`/`table:number-rows-spanned` mark only the master `table:table-cell`. Both writers track active merges by grid-column index as they walk each row (`DocxTableCell.colSpan`/`.verticalMerge`, `OdtTableCell.colSpan`/`.rowSpan` plus `OdtTableRow.appendCell`/`.appendCoveredCell`), and both are proven by a real build-then-read round trip in `src/edit/docx/content.test.ts`/`src/edit/odt/content.test.ts`, not merely by construction.
576
576
  - **docx headers/footers, comments, footnotes, and numbering definitions are now readable — but not through `readDocxContent`, and live `PAGE`/`NUMPAGES` field substitution still isn't read at all.** `readDocxContent` still carries none of the first four through: `ContentDocument`'s section/block shape has nowhere to put a comment, a footnote, a header/footer, or a numbering definition, so it deliberately keeps dropping them, exactly as before. What changed is that they are no longer lost outright: `readDocxExtras` (see the `src/ooxml/` Architecture entry and the Usage example above) is a second, independent read of the same package that returns them as their own `DocxExtras` value. `PAGE`/`NUMPAGES` field substitution has no equivalent — neither function reads it, since it isn't static content at all but a value Word computes at render time from the document's own live layout, which this package has no path to reproduce. Inline images, meanwhile, now ARE read by `readDocxContent` itself: `ooxml.js` 2.6.1's `readDocx` gained real `w:drawing` support, and `readDocxContent` (a thin adapter over it) inherited that for free, with zero code change on this package's side — see the docx-image round-trip entry directly below for the one thing that DID need a code change.
@@ -607,7 +607,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
607
607
  - **The operator dictionary calls `∑`/`∏`/`⋃` and the rest of the big-operator family NOT stretchy, matching MathML3's own dictionary.** This became load-bearing once a stretchy operator genuinely stretches: STIX Two Math *does* declare vertical `MathVariants` for a summation sign, so a wrong `stretchy: true` there would visibly deform a `∑` standing next to a tall fraction. A big operator grows by selecting a larger designed size in display style — the `largeop` mechanism — never by stretching to its row.
608
608
  - **A token element's (`mi`/`mn`/`mo`/`mtext`) own box height is the union of its characters' real per-glyph ink bounds, falling back to the font's nominal design ascent/descent only for a glyph that carries none.** `src/mathml/` still parses no glyph outlines itself — the bounds arrive through `MathFontMetrics.glyph`'s own `inkAscentPt`/`inkDescentPt`, which pdf-codec computes by walking the embedded font's Type 2 charstrings (see its README's own `cff-bounds.ts` note). The fallback is not dead code: a glyph that draws nothing (a space) or whose charstring that reader declines to walk reports neither bound, and takes the font-wide nominal extent instead, which is also what a `MathFontMetrics` implementation with no outline parsing at all would supply for every glyph.
609
609
  - **The MathML operator dictionary (`src/mathml/operators.ts`) is a deliberately bounded ~60-entry table, not the MathML3 specification's own multi-thousand-entry, form-dependent (prefix/infix/postfix) one.** It covers arithmetic, relational, set/logic, calculus big-operators, fences, and punctuation — the operators real formulas overwhelmingly use — with one entry per character regardless of which position it appears in, falling back to a single sane infix-shaped default (thick-space spacing, no stretch/largeop/movablelimits) for anything else.
610
- - **`mover`/`munder`/`munderover` centre an over/under-script geometrically over the wider of the two boxes, not at the base glyph's own font-declared accent-attachment point (`MathTopAccentAttachment`, which the embedded font's `MathGlyphInfo` subtable DOES carry and this package DOES parse — see the CFF-embedding gotcha above just not consumed here).** Visually correct for the common case of a single-character base (geometric centre optical centre for a roughly symmetric glyph); measurably different only for a multi-character or asymmetric base under a genuine `accent="true"` mark. A real, bounded simplification, not a data gap the metric this would need is already being parsed for a different purpose.
610
+ - **`mover`/`munder`/`munderover` centre an over/under-script at the base glyph's own font-declared accent-attachment point (`MathTopAccentAttachment`) when one is available, falling back to geometric centring otherwise.** `src/mathml/layout.ts`'s `resolveTopAccentXPt`/`AccentAttachment`/`layoutUnderOver` resolve the embedded font's real `MathTopAccentAttachment` metric for a base that is a single-codepoint token under a genuine `accent="true"`/`accentunder="true"` mark, tested in `src/mathml/layout.test.ts` against both a real attachment-point case and the geometric-fallback case (a multi-character base, where there is no single glyph's attachment point to resolve). Geometric centring survives as the correct fallback for exactly that multi-character/non-token case, not as the general rule.
611
611
  - **Greek `mathvariant` mapping covers the plain alphabet, nabla (∇), partial differential (∂), and the six OpenType/Unicode Greek "symbol variant" glyphs** (lunate epsilon/theta/kappa/phi/rho/pi symbols — U+03F5/U+03D1/U+03F0/U+03D5/U+03F1/U+03D6 — styled to bold, italic, bold-italic, bold-sans-serif, and sans-serif-bold-italic; Unicode never assigned symbol-variant glyphs for plain sans-serif, script, fraktur, or double-struck). Every entry is generated directly from Unicode's own `UnicodeData.txt` (see `src/mathml/variant.ts`'s own generation note) rather than transcribed by hand.
612
612
  - **A formula anchored to a spreadsheet cell renders for real now (`src/layout/sheets.ts`'s `renderAnchoredFormulas`), and closing it needed both sibling packages to move first — it was never something this module could wire around on its own.** `odf.js` had to learn to emit a cell-anchored formula sub-object at all (2.1.0 gave `readOds` a real `TableCursor` walk and true row/column anchoring, but its embedded-object classifier still recognised only wordprocessing/presentation/spreadsheet/drawing sub-documents, so a formula was skipped outright; 2.2.0 classifies one), and `document-schema.js` had to give `ContentEmbeddedObject` somewhere to record which cell it belongs to (2.2.0's optional `anchorRow`/`anchorColumn`/`offsetXPt`/`offsetYPt` quartet, mirroring what `ContentSheetImage` already carried). That quartet is exactly what makes placement possible: a cell-anchored `draw:frame`'s own `svg:x`/`svg:y` is relative to **that cell's** top-left corner, not the sheet's origin, so without an anchor there is no coordinate space its frame can be interpreted in at all. `sheets.ts` resolves the anchor against its own already-positioned column/row axes — so band membership, the repeat band, the header gutter, and fit-to-page scaling are all accounted for by construction — and applies the cell-relative offset **unscaled**, matching this module's own existing treatment of every other cell-local inset (cell text padding, header-label padding): fit-to-page scales the grid's geometry, never a cell's internal padding or its text's point size. Three consequences worth naming. (1) The print range **widens** to cover a formula's anchor cell when the sheet declares no explicit `table:print-ranges` — a cell-anchored drawing genuinely extends a sheet's used area in Calc/Excel, and without this a formula anchored past the last populated cell would fall outside every band and silently never render; the union is over anchor *cells* only, never each formula's own rendered box, so an oversized formula overflows over whatever follows exactly as it does in Calc rather than reserving empty rows nothing occupies. An *explicit* print range is still honoured verbatim, so a formula anchored outside one is correctly not printed. (2) A formula anchored inside a repeat row/column band renders on **every** page that band appears on, which is what a repeat band means — no special case, since the band is simply present in every page's own axis. (3) A formula anchored to a hidden row or column is skipped outright, exactly as that cell's own content is. `ContentSheet.images` remains the separate, still-open gap, and now on the layout side alone: `buildOdsPackage` writes a real floating `draw:frame`/`draw:image` for one and `odf.js` 2.2.0's `readOds` reads it back, but `sheets.ts` still emits no `LayoutImage` for it.
613
613
  - **`convertSpreadsheetToLayout` returns `{ document, formulas }`, not a bare `LayoutDocument`** — the same `SpreadsheetLayoutResult` shape `convertWordprocessingToLayout`/`convertPresentationToLayout` have always returned, for the same reason: a formula's CID-font glyph runs cannot travel through `LayoutDocument.pages[].items` at all (see the Gotchas entry on why), so they come back alongside the document and are handed to `writePdf({ formulas })`. `odsToPdf` threads them through exactly as `odtToPdf`/`odpToPdf` already did. A caller of the exported `convertSpreadsheetToLayout` reads `.document` where it previously used the return value directly. `convertDrawingToLayout` still returns a bare `LayoutDocument`, since `readOdgContent` runs no formula detection and a drawing page consequently never carries a formula block.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.documents",
3
- "version": "1.67.14",
3
+ "version": "1.67.16",
4
4
  "description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -62,7 +62,7 @@
62
62
  "fflate": "^0.8.3",
63
63
  "markdown-codec": "^1.0.0",
64
64
  "odf.js": "^2.2.10",
65
- "ooxml.js": "^2.6.9",
65
+ "ooxml.js": "^2.6.10",
66
66
  "pdf-codec": "^1.10.10",
67
67
  "zod": "^4.4.3"
68
68
  },