js.documents 1.81.8 → 1.82.0

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.
package/README.md CHANGED
@@ -624,12 +624,12 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
624
624
  - **`readPdf` recovers a rect, an ellipse, and a line as their own `LayoutRect`/`LayoutEllipse`/`LayoutLine` kinds, not merely as generic paths — pdf-codec's own shape-pattern detection, and the reason every vector kind now survives a `odgToPdf` → `pdfToOdg` round trip.** PDF has exactly one shape operator (`re`, itself defined as a four-point rectangle subpath) and no ellipse or line operator at all, so a writer has no way to record what a path *was*; pdf-codec recovers it from the geometry instead — an axis-aligned closed four-corner subpath is a `LayoutRect` (any combination of fill and stroke, and a 90°-rotated CTM as well as an unrotated one), a closed subpath of four cubic segments meeting its bounding box at the four cardinal points with kappa-ratio control points is a `LayoutEllipse`, and an open single-straight-segment stroke-only subpath is a `LayoutLine`. See pdf-codec's own README for the tolerances and the honest caveat that these are bounded heuristics: a false positive can change an item's *kind*, never its geometry. What still narrows to a generic `LayoutPath`: an off-axis rotation, a freeform curve, a multi-subpath figure. A practical consequence for `pdfToOds`: a gridline written by `sheets.ts`'s own `renderGridlines` now comes back as a real `LayoutLine`, but `reconstructSpreadsheet`'s lattice detection still accepts the stroked-single-segment `LayoutPath` shape too, so a hand-built `LayoutDocument` and one from a producer other than `readPdf` detect identically.
625
625
  - **`pdfToOds` re-types a recovered cell heuristically, and this is explicitly PROBABILISTIC BEST-EFFORT RECOVERY, not a fidelity guarantee.** A rendered PDF genuinely never carries a spreadsheet cell's own typed value — a page holds only the string the authoring application chose to print — so every re-typed value below is an inference from that string alone, and a string that looks exactly like a number may genuinely have *been* a string in the source spreadsheet (a part number, a version, a phone extension). Nothing in this package can tell those apart with certainty, and no further heuristic would change that. What is guaranteed: `ContentSheetCell.displayText` is a required field carrying the rendered string verbatim regardless of what was inferred from it, so the printed form is never lost. `src/layout/cell-typing.ts` re-types only where the string has exactly **one** defensible reading, which resolves to four concrete requirements: the decimal must be exactly representable as a JS number (checked by round-tripping it, not by a digit-count limit — this is what keeps a 19-digit barcode a string); the separators must be unambiguous (`.` reads as the decimal separator and `,` as grouping, but a lone comma group like `"1,234"` is **declined**, since the competing European reading of the identical string is 1.234, a thousandfold error — `"1,234,567"` and `"1,234.50"` have no such competing reading and are accepted); a leading zero (`"007"`, `"01.5"`) is declined outright, since a spreadsheet never prints a numeric value with one; and a date's component roles must be stated by the text itself — ISO ordering (`"2024-01-15"`) or a named month (`"15 Jan 2024"`, `"Jan 15, 2024"`) is accepted, an all-numeric separated date (`"01/02/2024"`) is declined regardless of whether one component happens to exceed 12 in that particular cell, because resolving it per cell would type one column inconsistently. `TRUE`/`FALSE` are re-typed as booleans; `Yes`/`No`/`Y`/`N`/`On`/`Off` are **declined**, since no mainstream spreadsheet prints a boolean that way by default, so a `"Yes"` cell is far more likely genuine text. Percentages recover ODF's own fraction convention (`"15%"` → `0.15`); a currency symbol names an ISO code only where it identifies exactly one (`£`→GBP, `€`→EUR; `$` and `¥` re-type as `currency` with the code left `undefined`). `'time'`/`'dateTime'`/`'error'` are deliberately out of scope, and a formula is never claimed. Two ways to tell an inferred value from an untouched one: `value.kind !== 'string'` is itself the flag, and `ReconstructOptions.onCellTypeInference` reports every decision — both a re-typing (with the rule that fired) and a deliberate refusal (with the named ambiguity), the latter being information the output alone cannot carry, since a declined cell is indistinguishable from one that was never number-shaped at all. `inferCellValue` is exported standalone for a caller who wants to replay the same decision over their own text.
626
626
  - **`reconstructWordprocessing`/`reconstructPresentation` recover a page's vector primitives too, in a nested drawing document, and all four OOXML/ODF builders now write them out as real shapes.** Both directions used to filter each page down to its text and image items and discard every stroke and fill; they now run the same `layoutItemToVector` classification `reconstructDrawing` does (one implementation, not two) and carry the result in a `ContentEmbeddedObjectBlock` whose `objectKind` is `'drawing'` and whose nested `document` is a real one-page drawing `ContentDocument` — the shared schema's own designed mechanism, since `ContentSection.blocks` and `ContentSlide.shapes` have no vector vocabulary of their own. One honest consequence of the recovery itself: a PDF does not distinguish a stroke drawn to decorate from one drawn as structure, so a rule under a heading, an underline (pdf-codec writes one as a filled rectangle), and a table cell's background fill are all recovered as vectors — that is intended, since discarding real content because it *might* be incidental is exactly the silent loss this package's conventions rule out, but it does mean a reconstructed document carries more than its text alone. A table's own gridlines are the one case deliberately not double-counted: when the table recovery above claims a lattice, the strokes that formed it are excluded from vector recovery. On the write side, `buildDocxPackage` and `buildPptxPackage` emit real DrawingML (`a:prstGeom prst="rect"/"ellipse"/"line"`, and a genuine `a:custGeom` with `a:moveTo`/`a:lnTo`/`a:cubicBezTo`/`a:close` for a path), and `buildOdtPackage`/`buildOdpPackage` emit real `draw:rect`/`draw:ellipse`/`draw:line`/`draw:path` — see the dedicated write-side gotcha below for how each format wraps them. `pdfToMarkdown` still drops the block entirely rather than emitting a marker for it: a rect carries no text to stand in for, unlike a formula, and CommonMark has no vector construct regardless.
627
- - **A recovered vector reaches the output FILE for all four formats, but not back through this package's own readers.** `buildDocxPackage`/`buildPptxPackage`/`buildOdtPackage`/`buildOdpPackage` all write real vector shapes now, so a `pdfToDocx`/`pdfToPptx`/`pdfToOdt`/`pdfToOdp` output opens in Word, PowerPoint, or LibreOffice with the recovered geometry in place. Re-reading that same file through `readDocxContent`/`readPptxContent`/`readOdtContent`/`readOdpContent` does **not** give the embedded-drawing block back: those four are thin adapters over `ooxml.js`'s `readDocx`/`readPptx` and `odf.js`'s `readOdt`/`readOdp`, none of which reads vector geometry into a `ContentDocument` at all and for the ODF pair, `ContentSection.blocks`/`ContentSlide.shapes` have no vector vocabulary to read one into regardless. The practical consequence: the six PDF-bypassing bridges (`odtToDocx` and friends) carry no vectors across, since a bridge is `readXContent` `buildYPackage` and the read half drops them. Closing this is reader-side work — the OOXML/ODF mirror of the second pass `src/odf/formula/detect.ts` already runs for embedded formulas — a tracked, bounded gap, not a silent one.
627
+ - **A recovered vector now round-trips back through all four of this package's own readers, not only reaches the output file.** `buildDocxPackage`/`buildPptxPackage`/`buildOdtPackage`/`buildOdpPackage` all write real vector shapes, and re-reading that same file through `readDocxContent`/`readPptxContent`/`readOdtContent`/`readOdpContent` now gives the embedded-drawing block back: the ODF pair (`src/odf/odt/read.ts`/`odp/read.ts`) run `collectContainerVectors` — a second pass over the same `content.xml`, mirroring the formula-detection pass — straight through to `odf.js`'s own `readDrawPageContent`; the OOXML pair (`src/ooxml/docx/read.ts` via `spliceDocxEmbeddedObjects`, `src/ooxml/pptx/read.ts` via `collapseVectorShapeRuns`) do the same over `word/document.xml` and each slide's own `p:sld`. A recovered vector is a real `ContentEmbeddedObjectBlock` whose `objectKind` is `'drawing'`, so the six PDF-bypassing bridges (`odtToDocx` and friends) now carry vector geometry across too, since a bridge's own `readXContent` half no longer drops it.
628
628
  - **Each format wraps a vector shape in the construct that format actually has for one, and the two families each share one writer.** `src/edit/drawingml/vector.ts` holds everything inside a DrawingML shape-properties element (`a:xfrm`, the preset or custom geometry, `a:solidFill`/`a:noFill`, `a:ln`) — identical for docx and pptx, since `CT_ShapeProperties` is one type in both. Only the wrapper differs: pptx gets a plain `p:sp` on the slide's `p:spTree` (no `p:txBody` at all — a geometric primitive carries no text, and inventing an empty paragraph would make `readPptx` report a text shape where the source had pure geometry), while docx gets a `w:drawing`/`wp:anchor` positioned `relativeFrom="page"` on both axes, with `behindDoc="1"` and `wp:wrapNone`, carrying a `wps:wsp` inside `a:graphicData` — the wordprocessingShape extension part, the only DrawingML vocabulary WordprocessingML has for a non-picture shape (the pre-2010 alternative is VML, deprecated by ECMA-376 itself and deliberately not written). Anchoring rather than `wp:inline` is what preserves the recovered page-absolute coordinates; the honest limit is that the anchor still belongs to a paragraph, so which page the geometry lands on follows that paragraph if the document reflows differently in Word than it laid out in the source PDF. On the ODF side, `buildOdtPackage`/`buildOdpPackage` import `src/edit/odg/vector.ts`'s writer wholesale rather than reimplementing it — `draw:rect`/`draw:ellipse`/`draw:line`/`draw:path` carry byte-for-byte the same attribute vocabulary in a text document, a presentation, and a drawing, and `odf.js`'s own `readDrawPageContent` reads all three through one function. odp appends them directly to the slide's `draw:page` (a slide positions geometry against the page, exactly as a drawing page does); odt anchors them in a `text:p` of their own with `text:anchor-type="paragraph"` **plus** `style:horizontal-rel`/`style:vertical-rel="page"` in the graphic style, since a recovered vector's coordinates are page-absolute and a paragraph-relative reference would offset every shape by wherever its anchor paragraph flowed to. `style:wrap="run-through"`/`style:run-through="background"` are the ODF counterpart of docx's `wp:wrapNone`/`behindDoc="1"` pair.
629
- - **`ContentStroke.style` (`solid`/`dashed`/`dotted`/`double`) is not written by any vector writer, ODF or DrawingML.** Nothing in this package produces one — `LayoutLine` and `LayoutPath` both carry a stroke of colour and width only (`document-schema.js`'s `layout.ts`), so no reconstruction path can populate it — and `a:prstDash` has no `double` member to map the fourth value onto regardless. A hand-built `ContentVector` setting it consequently paints solid. A real, bounded gap rather than an oversight.
629
+ - **`ContentStroke.style` (`solid`/`dashed`/`dotted`/`double`) is not written by any vector writer, ODF or DrawingML.** `LayoutLine`/`LayoutPath` DO carry an optional `style` enum now (`document-schema.js` 2.1.0), and a reconstruction path could populate it — but neither vector writer reads the field: `src/edit/drawingml/vector.ts`'s `outline()` writes `a:ln` with width + colour only (no `a:prstDash`), and `src/edit/odg/style.ts` writes `draw:stroke` + `svg:stroke-color`/`svg:stroke-width` only (no `draw:stroke-dash`). A hand-built `ContentVector` setting `stroke.style` consequently paints solid; a real, bounded gap rather than an oversight. Cell borders are a separate path — `pushCellBorderLines` does set the style and pdf-codec renders it (see the cell-border gotcha below) — and `a:prstDash` has no `double` member to map the fourth value onto regardless.
630
630
  - **`pdfToOds` recovers what was printed, not what was entered.** `reconstructSpreadsheet` (`src/layout/reconstruct.ts`) tries a real gridline lattice first: it scans the page's `LayoutLine`/stroked-single-segment-`LayoutPath` items (see the `interpret.ts` gotcha above) for enough parallel horizontal and vertical lines at consistent positions to call it a printed grid (`MIN_GRIDLINE_COUNT_PER_AXIS = 3` per axis, i.e. at least a 2×2 grid, and a span-consistency check that rejects a scatter of unrelated short strokes — a page border or a couple of decorative rules — as not a genuine lattice), and uses those line positions DIRECTLY as cell boundaries when found. Absent a lattice, it clusters text into a grid from geometry alone instead: rows reuse `clusterIntoLines` verbatim (a spreadsheet cell's own text is never wrapped across lines, so a text line already IS a row), and columns generalise `clusterIntoParagraphs`'s own single `dominantLeftX` to several recurring x-position anchors, first merging directly-adjacent same-line fragments (`splitLineByLargeGaps`, the same >2em-gap signal `reconstructPresentation`'s own block clustering uses) so a cell whose text arrived as several run-level-split `LayoutText` items isn't scattered across spurious columns. Column widths and row heights are genuinely measured from whichever geometry was used (drawn gridline gaps, or measured text/anchor extents), never invented. Every recovered cell always carries its own extracted `displayText` verbatim, and additionally carries a **heuristically re-typed** `value` wherever `src/layout/cell-typing.ts` finds exactly one defensible reading of that string; a formula is still never claimed. See the dedicated heuristic-re-typing gotcha below and [Fidelity](#fidelity) for the full framing. `buildOdsPackage` (`src/edit/ods/content.ts`) is `pdfToOds`'s own package-building half, mirroring `buildOdtPackage`/`buildOdpPackage`/`buildOdgPackage`'s role for `pdfToOdt`/`pdfToOdp`/`pdfToOdg`.
631
631
  - **`OdsSheet.printSettings` (`src/edit/ods/print-settings.ts`) now round-trips every field `ContentSheetPrintSettingsSchema` carries, not just the five it started with.** `pageSize`/`margins`/`gridlines`/`headers`/`pageOrder` resolve through the `table:style-name` → `style:style[family="table"]` → `style:master-page-name` → `style:master-page` → `style:page-layout` → `style:page-layout-properties` chain (`odf.js`'s own exported `findStyleElement`/`resolvePageLayoutProperties`/`parsePageSize`/`parseMargins`); the setter mints a fresh `style:page-layout` + `style:master-page` + `style:style[family="table"]` triple and repoints the sheet's own `table:style-name` to it on every call, the same append-only style-editing convention `src/edit/odg/style.ts` already documents. The remaining, previously-unimplemented fields are now implemented too: `printRange` reads/writes `table:print-ranges` directly on `table:table`; `scalePercent`/`fitToPages` read/write `style:scale-to` and `style:scale-to-X`/`style:scale-to-Y` on the page-layout-properties element; `repeatColumns`/`repeatRows` are read via `scanTableStructure`, a scoped-down mirror of `odf.js`'s own private `readTable`'s table-wide column/row cursor tracking (the same walk that function performs before ever calling its own `readPrintSettings`), and written by moving the real `table:table-column`/`table:table-row` elements covering the given range into a fresh `table:table-header-columns`/`table:table-header-rows` wrapper; `manualBreaks` read/write `fo:break-before="page"` on the named row/column's own style. Writing `repeatRows`/`repeatColumns` required teaching `address.ts`'s row/column addressing that a row/column may now live nested one level inside a header wrapper rather than as a direct `table:table` child, so a subsequent cell/column/row write against a wrapped index finds the real element instead of creating a spurious duplicate outside it; the width/height and manual-break writers all target the same `style:table-column-properties`/`style:table-row-properties` element, so each reads the column/row's current style first and mints a fresh style carrying the merged result, rather than a naive single-property mint clobbering whatever an earlier call had already set. No known gap remains in `ContentSheetPrintSettingsSchema` coverage.
632
- - **`OdsSheet` now has a real column-width/row-height setter (`setColumnWidth`/`setRowHeight`, `src/edit/ods/column-row.ts`), closing a gap that escalated from cosmetic to a genuine correctness bug once `xlsxToPdf`/`pdfToXlsx` started composing through `buildOdsPackage` internally.** `OdsSheet.cell()`'s own column/row-materialisation (`address.ts`) creates a real, explicit `table:table-column`/`table:table-row` element for any position a caller ever addresses, but previously never gave it a width/height style. This is a genuinely different failure shape from a column/row with NO element at all: `sheets.ts`'s own `resolveAxis` only falls back to `DEFAULT_COLUMN_WIDTH_PT`/`DEFAULT_ROW_HEIGHT_PT` for an index with no `ContentSheetColumn`/`ContentSheetRow` entry whatsoever — an explicit-but-unstyled element reads back at `widthPt`/`heightPt` 0 (`odf.js`'s own `resolveColumnWidthPt`/`readRowLayout`), and that explicit zero wins over the fallback. While `buildOdsPackage`'s own output was only ever a terminal deliverable (`pdfToOds`, or a caller's own `readOdsContent` round trip), this was cosmetic: a real app reopening it would use its own defaults instead of the source's. `xlsxToPdf` (`xlsxToOds` then `odsToPdf`) made it a real bug instead — the intermediate ods bytes get laid out again by `convertSpreadsheetToLayout`, and a zero-size grid collapses every cell onto the same physical position rather than merely losing precision. `setColumnWidth`/`setRowHeight` mint a fresh `style:style[family="table-column"|"table-row"]` per column/row and repoint its own `table:style-name`, the same append-only style-minting convention `writeSheetPrintSettings`/`src/edit/odg/style.ts` already establish; `buildOdsPackage` now calls both for every `ContentSheetColumn`/`ContentSheetRow` a source sheet carries. Column/row HIDDEN state and `ContentSheetImage`/formula `embeddedObjects` are no longer gaps either, closed in the same phase: `OdsSheet.setColumnHidden`/`setRowHidden` set or clear `table:visibility="collapse"` directly on the `table:table-column`/`table:table-row` element — a plain attribute, not a style property, so it never interacts with the width/height setters above — and `buildOdsPackage` calls one of these for every column/row whose `hidden` field is `true`. `OdsSheet.addImage` (`src/edit/ods/floating.ts`) writes a real floating `draw:frame`/`draw:image` into `table:shapes` (the ODF 1.3 content-model container for spreadsheet floating shapes, always `table:table`'s own first child in a package this editor builds), resolving a `ContentSheetImage`'s `anchorRow`/`anchorColumn` plus `offsetXPt`/`offsetYPt` to an absolute `svg:x`/`svg:y` by summing the real, currently-declared width/height of every column/row strictly before the anchor (header-wrapper-aware, hidden columns/rows contributing zero, falling back to the same default column/row size the layout engine assumes once the walk runs past what the sheet has declared) — reusing `addImageMedia` for the binary part and manifest entry, the same mechanism `src/edit/odp/image.ts` already uses for a slide. `OdsSheet.addEmbeddedObject` writes a real embedded ODF formula sub-document for `objectKind === 'formula'` (reusing `addFormulaObject`, the same mechanism `OdtBody.appendFormula` already uses); every other `objectKind` (`wordprocessing`/`presentation`/`spreadsheet`/`drawing`) is left unwritten, a documented, bounded gap mirroring `buildOdtPackage`'s identical narrowing for a `'drawing'` embedded object, since embedding one would mean writing that document's own package as a nested OLE sub-object, which no writer in this codebase implements. `buildOdsPackage` calls both for every sheet's images/embedded objects, after every column/row width/height/hidden call, so an image's own anchor resolves against the sheet's final, real column/row sizing. This is no longer write-only: `odf.js` 2.2.0's own `readOds` reads a sheet's floating shapes and embedded objects back (it previously hardcoded `images: []` and never set `embeddedObjects`), so a written image now verifies as a genuine `ContentDocument` re-read round trip — bytes, declared size, and anchor quartet — on top of the direct written-XML structural checks these tests already made.
632
+ - **`OdsSheet` now has a real column-width/row-height setter (`setColumnWidth`/`setRowHeight`, `src/edit/ods/column-row.ts`), closing a gap that escalated from cosmetic to a genuine correctness bug once `xlsxToPdf`/`pdfToXlsx` started composing through `buildOdsPackage` internally.** `OdsSheet.cell()`'s own column/row-materialisation (`address.ts`) creates a real, explicit `table:table-column`/`table:table-row` element for any position a caller ever addresses, but previously never gave it a width/height style. This is a genuinely different failure shape from a column/row with NO element at all: `sheets.ts`'s own `resolveAxis` only falls back to `DEFAULT_COLUMN_WIDTH_PT`/`DEFAULT_ROW_HEIGHT_PT` for an index with no `ContentSheetColumn`/`ContentSheetRow` entry whatsoever — an explicit-but-unstyled element reads back at `widthPt`/`heightPt` 0 (`odf.js`'s own `resolveColumnWidthPt`/`readRowLayout`), and that explicit zero wins over the fallback. While `buildOdsPackage`'s own output was only ever a terminal deliverable (`pdfToOds`, or a caller's own `readOdsContent` round trip), this was cosmetic: a real app reopening it would use its own defaults instead of the source's. `xlsxToPdf` (`xlsxToOds` then `odsToPdf`) made it a real bug instead — the intermediate ods bytes get laid out again by `convertSpreadsheetToLayout`, and a zero-size grid collapses every cell onto the same physical position rather than merely losing precision. `setColumnWidth`/`setRowHeight` mint a fresh `style:style[family="table-column"|"table-row"]` per column/row and repoint its own `table:style-name`, the same append-only style-minting convention `writeSheetPrintSettings`/`src/edit/odg/style.ts` already establish; `buildOdsPackage` now calls both for every `ContentSheetColumn`/`ContentSheetRow` a source sheet carries. Column/row HIDDEN state and `ContentSheetImage`/formula `embeddedObjects` are no longer gaps either, closed in the same phase: `OdsSheet.setColumnHidden`/`setRowHidden` set or clear `table:visibility="collapse"` directly on the `table:table-column`/`table:table-row` element — a plain attribute, not a style property, so it never interacts with the width/height setters above — and `buildOdsPackage` calls one of these for every column/row whose `hidden` field is `true`. **The "explicit but unstyled" hazard itself is now closed at its source, not just worked around by callers who remember to call `setColumnWidth`/`setRowHeight` explicitly.** `column-row.ts`'s `ensureColumnDefaultWidth`/`ensureRowDefaultHeight` stamp `DEFAULT_COLUMN_WIDTH_PT`/`DEFAULT_ROW_HEIGHT_PT` (matching `sheets.ts`'s own fallback values exactly) onto a column/row the moment any of `OdsSheet.cell()`, `mergeCells()`, `setColumnHidden()`, or `setRowHidden()` individuates it for the first time — but only when it has no width/height style at all yet, never overwriting a width a caller (or a real source document opened via `openOds`) already set. A plain `createOds()` sheet touched only through `.cell()` now reads back with a real, positive `widthPt`/`heightPt` on every column/row it addressed, never the ambiguous 0 that used to require an explicit `setColumnWidth`/`setRowHeight` call to avoid. One narrower instance of the identical hazard remains open, deliberately not folded into this fix: `print-settings.ts`'s `wrapRepeatRange` (the `repeatColumns`/`repeatRows` writer) still individuates its own boundary columns/rows via a bare, unstyled `replaceRun`, and — unlike `cell()`/`mergeCells()` — stamping every position that boundary individuation touches would mean materializing an unbounded range rather than the two boundary positions it is deliberately designed to touch, a genuinely different performance tradeoff this fix does not attempt to resolve. `OdsSheet.addImage` (`src/edit/ods/floating.ts`) writes a real floating `draw:frame`/`draw:image` into `table:shapes` (the ODF 1.3 content-model container for spreadsheet floating shapes, always `table:table`'s own first child in a package this editor builds), resolving a `ContentSheetImage`'s `anchorRow`/`anchorColumn` plus `offsetXPt`/`offsetYPt` to an absolute `svg:x`/`svg:y` by summing the real, currently-declared width/height of every column/row strictly before the anchor (header-wrapper-aware, hidden columns/rows contributing zero, falling back to the same default column/row size the layout engine assumes once the walk runs past what the sheet has declared) — reusing `addImageMedia` for the binary part and manifest entry, the same mechanism `src/edit/odp/image.ts` already uses for a slide. `OdsSheet.addEmbeddedObject` writes a real embedded ODF formula sub-document for `objectKind === 'formula'` (reusing `addFormulaObject`, the same mechanism `OdtBody.appendFormula` already uses); every other `objectKind` (`wordprocessing`/`presentation`/`spreadsheet`/`drawing`) is left unwritten, a documented, bounded gap mirroring `buildOdtPackage`'s identical narrowing for a `'drawing'` embedded object, since embedding one would mean writing that document's own package as a nested OLE sub-object, which no writer in this codebase implements. `buildOdsPackage` calls both for every sheet's images/embedded objects, after every column/row width/height/hidden call, so an image's own anchor resolves against the sheet's final, real column/row sizing. This is no longer write-only: `odf.js` 2.2.0's own `readOds` reads a sheet's floating shapes and embedded objects back (it previously hardcoded `images: []` and never set `embeddedObjects`), so a written image now verifies as a genuine `ContentDocument` re-read round trip — bytes, declared size, and anchor quartet — on top of the direct written-XML structural checks these tests already made.
633
633
  - **`reconstructDrawing` maps recovered geometry back onto ODF shapes near-1:1, with no clustering — and every vector kind in this package's own `.odg` fixture now survives the round trip, where a stroked rect, an ellipse, and a line used to collapse to a generic `path`.** Every painted `LayoutItem` maps onto a `ContentVector`/`ContentShape` directly, in the exact z-order it was recovered — `LayoutRect` → `rect`, `LayoutEllipse` → `ellipse`, `LayoutLine` → `line`, `LayoutPath` → `path`, `LayoutText`/`LayoutImage` → `ContentShape` — a fundamentally more tractable problem than `reconstructWordprocessing`/`reconstructPresentation`'s own paragraph/shape geometry clustering, since a drawing has no semantic structure to infer at all. How much *kind* information survives is decided upstream, by what `readPdf` can hand it: pdf-codec's own shape-pattern detection (see the gotcha above) now recovers a rect under any fill/stroke combination, a real ellipse from the four kappa-ratio cubics `writeEllipse` emits, and a real line, so `reconstructDrawing` receives — and therefore emits — the original kind in each case. What still narrows: a rotation that is not a multiple of 90° leaves no axis-aligned pattern to match, so a rect turned by 30° comes back as a `path` carrying its four rotated corners exactly. Position, size, and fill/stroke colour survive regardless of kind (within ordinary floating-point/string-formatting tolerance). A `path` vector's own reconstructed `frame` is a further, separate approximation: it is the *tight* bounding box of every recovered point, cubic control points included (a cubic curve is guaranteed to lie within their convex hull, so this never clips the curve) — which can legitimately be *larger* than whatever frame the original path's own author declared, if that frame didn't tightly bound its own control points to begin with (a real, valid ODF/SVG authoring pattern: a `viewBox`/frame is a declared coordinate window, not a guaranteed tight bounding box). A single original drawing text box that PDF's own greedy line-wrapper split across several lines does **not** reconstruct as one multi-line shape: `reconstructDrawing` maps each recovered `LayoutText` item to its own separate `ContentShape` (the same one-`LayoutItem`-to-one-shape rule every other kind follows), so a wrapped multi-line text box comes back as several small, independently-positioned text boxes, one per original line — confirmed visually against real LibreOffice (see the real-file verification note below); the full text content still survives, just redistributed. `buildOdgPackage` (`src/edit/odg/content.ts`) is `pdfToOdg`'s own package-building half, mirroring `buildOdtPackage`/`buildOdpPackage`'s role for `pdfToOdt`/`pdfToOdp`.
634
634
  - **Two real, confirmed-against-actual-LibreOffice-rendering fill bugs were fixed as part of building `reconstructDrawing`/`pdfToOdg`, not by it.** Both are pre-existing gaps in code that `reconstructDrawing`'s own real-file verification exposed, not something the reconstruction algorithm itself introduced, and both apply to every `.odg` this package writes, not only a reconstructed one: (1) `src/edit/odg/style.ts`'s `graphicPropertyAttrs` wrote `draw:fill-color` alone, with no accompanying `draw:fill="solid"` — real LibreOffice 26.2 fills a `draw:rect`/`draw:ellipse` that way fine, but silently renders a `draw:path` with the identical omission as unfilled, even with a fill colour declared. `draw:fill="solid"` is now written explicitly whenever a fill is set, for every vector kind. (2) `writeEllipse` (pdf-codec's own `content-write.ts`) never emitted a PDF closepath (`h`) operator, even though its four Bezier arcs already return exactly to their own starting point — PDF fill operators close every subpath implicitly regardless (ISO 32000-1 8.5.3.1), but `readPdf`'s own general path tracking only marks a subpath `closed: true` when it actually sees an explicit `h`, so a PDF-round-tripped ellipse came back with `closed: false`, which correctly-behaving ODF/SVG consumers then refuse to fill even with `draw:fill="solid"` set. `writeEllipse` now emits `h` before its paint operator, drawing no additional ink (the path was already geometrically closed) but recording that closure explicitly.
635
635
  - **A vector primitive's own fill/stroke needed a self-contained graphic-family style writer, not `odf.js`'s own `StyleRegistry`.** `'graphic'` is a recognised `StyleFamily` member (`odf.js`'s `src/styles/registry.ts`), but `StylePropertiesSchema`/`buildStylePropertyElements` (`properties.ts`/`serialize.ts`) only ever model text/paragraph formatting and never emit a `style:graphic-properties` element for any family — extending that shared package for one narrow, documents.js-local need (`draw:fill(-color)`/`draw:stroke` + `svg:stroke-color`/`svg:stroke-width`) would be scope creep into a foreign package for a two-attribute-group writer this package can express directly. `src/edit/odg/style.ts` is that writer: it still reuses `odf.js`'s general append-only style-editing invariant (a setter always mints a fresh `style:style` and repoints `draw:style-name`, never mutates an existing entry — verified by the same `assertAutomaticStylesOnlyAppended` helper `OdpEditor`'s own live-view fidelity test uses) and `src/edit/odt/automatic-styles.ts`'s `ensureAutomaticStyles`/`nextStyleName` (the "find-or-create `office:automatic-styles`, mint the next unused name" logic every other hand-rolled style helper in this package already shares), rather than a third reimplementation of either.
@@ -639,7 +639,7 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
639
639
  - **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.
640
640
  - **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.
641
641
  - **`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.
642
- - **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.
642
+ - **A cell's declared border now renders with its real `style` (`solid`/`dashed`/`dotted`/`double`) in PDF output.** `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 reaches the PDF byte output for real: `LayoutLineSchema`/`LayoutPathSchema` both carry that optional `style` enum (added in `document-schema.js` 2.1.0), `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`, and pdf-codec (this package pins `^1.11.8`; the dash rendering landed in 1.10.0) draws it a genuine PDF `d` dash-array operator for `'dashed'`, round-cap dots for `'dotted'`, and `'double'` as two hand-offset parallel strokes. The one honest caveat is `'double'`'s own inter-line offset: that distance is nowhere in the data model, so pdf-codec picks its own internal constant for it rather than reading a value the source never carried — every other style value is faithful.
643
643
  - **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).
644
644
  - **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.
645
645
  - **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.
@@ -681,13 +681,13 @@ To run a single test file: `pnpm vitest run src/path/to/file.test.ts`.
681
681
  - **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.
682
682
  - **`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.
683
683
  - **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.
684
- - **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.
684
+ - **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` now renders through the identical mechanism (`renderAnchoredImages`): a floating image carries the same anchor quartet a formula does, resolves against the same positioned axes, widens the print range to cover its anchor cell the same way, and is skipped for a hidden anchor row/column or an anchor outside an explicit print range on exactly the same terms the only difference is that an image is a real `LayoutItem` (`LayoutImage`) pushed into the page's own items, with its bytes registered in the document-wide image registry `registerImage` deduplicates into, rather than a separate out-array the way a formula's CID-font glyph runs are.
685
685
  - **`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.
686
- - **The formula-size heuristic (`formulaSizePtFromFrame`, `src/layout/shared.ts`) is now one shared function rather than three copies**, consumed identically by `engine.ts` (flow placement), `slides.ts` (shape placement), and `sheets.ts` (cell-anchored placement): half the embedded object's own declared frame height, floored at 8pt. Its documented limit is worth restating with a measured example, since the ods fixture exercises it directly: it assumes a roughly single-line formula (total height a little over twice the base font size), so a frame sized for a genuinely *stacked* formula over-estimates. `src/test-support/ods-formula.ts`'s real LibreOffice file declares a 4.5cm-tall frame for a fraction-plus-radical expression, which this heuristic renders at ~64ptvisually much larger than Calc itself draws it, and wide enough to run past the page's right edge. Position is correct; size is an approximation, exactly as the heuristic says. Replacing it with a two-pass fit (lay out once at a reference size, rescale by the frame's own width/height ratio, lay out again — `layoutFormula`'s output scales linearly in `sizePt`, so no iteration is needed) is a real, tractable improvement, but a cross-engine one that would change docx/odt/odp output too, so it is tracked rather than done here alongside the sheets work.
686
+ - **The formula-size fit (`formulaSizePtForFrame`, `src/layout/shared.ts`) is now one shared two-pass function rather than three copies of a height/2 heuristic**, consumed identically by `engine.ts` (flow placement), `slides.ts` (shape placement), and `sheets.ts` (cell-anchored placement): it lays the formula out once at a reference size to measure its natural width and height, then rescales by the frame's own declared width and height so the laid-out box fits both (whichever is the binding constraint), floored at 8pt. `layoutFormula`'s output scales linearly in `sizePt`, so a single rescale reaches the fit with no iterationthe height/2 heuristic this replaced overflowed a genuinely stacked formula (a fraction inside a radical is taller than twice its base font size), rendering it larger than the frame the source document drew it at. A docx OMML equation carries no geometry of its own (`equationFrame` synthesises `widthPt: 0`), so for docx the width contributes no constraint and height alone drives the size the old heuristic's intent while ODF-sourced formulas (which carry a real `draw:frame` width+height) get the full two-dimensional fit.
687
687
 
688
688
  - **Embedded-formula detection inside odt/odp is genuinely new work with no `odf.js`-side equivalent (`readDrawFrameContent` doesn't recognise a `draw:object`-bearing `draw:frame` at all yet — see the `src/odf/` architecture entry above), and each format's own placement is now derived from the exact walk `odf.js` itself used, rather than approximated.** For **odt** (`src/odf/odt/read.ts`): a formula frame is found wherever it actually is — a direct child of `office:text`, one nested inside a `draw:g` group, one anchored inline inside a paragraph's own run content (`text:anchor-type="as-char"`, the shape LibreOffice writes for a formula typed into a sentence), and one inside a list item's own paragraph. Each block lands at its **true position** among the paragraphs/tables `odf.js` already read, because this adapter mirrors `readOdt`'s own `readBlocks` walk to *count* how many `ContentBlock`s each `office:text` child contributes — the per-element bookkeeping that was previously missing and forced every formula to be appended at the end (a `text:list` unwraps into one `ContentParagraph` per item at every nesting level, so "one raw child = one block" does not hold, which is exactly why counting rather than indexing is required). Two bounded, honest details remain: an *inline* formula's block is placed immediately **after** the paragraph containing it rather than truly inside it (`ContentRun` is text-only, so `ContentBlock` has no inline slot for an embedded object, and splitting the paragraph around the formula would invent a boundary the source never had), and an inline frame carries `svg:width`/`svg:height` but no `svg:x` — so its recovered frame is the declared size at a zero origin the text flow replaces, which is all the wordprocessing layout engine reads from it anyway. For **odp** (`src/odf/odp/read.ts`): every formula on every slide is detected, groups included. `collectSlideFormulaFrames` replicates `odf.js`'s own `walkDrawShapes` traversal exactly — document order, recursing into a `draw:g`'s children with that group's own `draw:transform` composed, one shape per `draw:frame` whose geometry `readDrawFrame` resolves and none for any it cannot — so the shape index it counts *is* the index `readOdp` assigned. The previous "skip the whole slide if it contains any `draw:g`" narrowing existed only because the old correspondence was "Nth top-level frame = `shapes[N]`", which a group breaks by splicing its own frames into the same flat array; deriving the index from the same walk removes the ambiguity rather than working around it. **ods needs no detection pass of its own at all, unlike odt and odp**: `odf.js` 2.2.0's own `readOds` walks each `table:table-cell`'s children with a real `TableCursor` and classifies an embedded formula sub-document directly (`readOdfFormulaDocument`, alongside the wordprocessing/presentation/spreadsheet/drawing kinds its 2.1.0 classifier already recognised), so a cell-anchored formula arrives as an ordinary `ContentSheet.embeddedObjects` entry already carrying its own anchor. `src/layout/sheets.ts` consumes that directly — see the cell-anchored-formula gotcha below.
689
689
  - **A formula crossing a boundary that cannot typeset it degrades to its own plain-text stand-in — its StarMath annotation, or the literal `[formula]` — never to nothing. The docx bridges are no longer part of that list.** `buildDocxPackage` now writes a genuine OMML display equation (`m:oMathPara` > `m:oMath`, structurally translated by `src/omml/write.ts` — see the architecture entry above), so a formula crossing `odtToDocx`, or reaching a docx through any other `buildDocxPackage` caller, arrives as real, editable Word math rather than text. The stand-in survives there for exactly one case: a formula whose MathML produces no OMML content at all (an empty `mathml` array). An individual MathML construct with no OMML counterpart degrades on its own, *inside* the equation, as a literal-text run with an `unsupported-element` diagnostic reported through `buildDocxPackage`'s own `onMathDiagnostic` (threaded from `odtToDocx`/`markdownToDocx`'s `DocumentBridgeOptions`) — it never drags the whole formula down to text. `buildOdtPackage` is no longer on that list either: it writes a real embedded formula sub-document (a nested `Object N/content.xml` with its own `draw:frame`/`draw:object` reference and manifest entry — see the `src/odf-package/` architecture entry), with the identical single-case fallback, a formula carrying no MathML nodes at all. The markdown writer is the only genuinely stand-in-only path left, since CommonMark/GFM has no math construct whatsoever. **`odmToPdf` is not part of this list either**: a chapter's formula is an ordinary block inside that chapter's own `ContentDocument`, so it survives concatenation into the combined document exactly as a paragraph does and renders as genuine typeset MathML. That used to be a documented gap — the formulas travelled in a side-channel map keyed by `sourcePath`, and re-keying every entry against the combined document's own renumbered block indices was intractable — which moving a formula's content *into* the `ContentDocument` removed outright rather than solved.
690
- - **OMML is read as well as written, but the two directions are deliberately not symmetric in coverage.** `readDocxContent` recovers a docx equation as a real `ContentEmbeddedObjectBlock` carrying its own MathML — the identical shape `readOdtContent` produces for an ODF embedded formula — so `docxToPdf` typesets a Word-authored equation, and `odt → docx → odt` carries a formula through as a formula. The reader covers strictly more than the writer emits, because it has to read what Word wrote rather than only what this package wrote: `m:d`, `m:nary`, `m:acc`, `m:bar`, `m:func`, and `m:sPre` have exact MathML inverses and no writer counterpart at all (see the `src/omml/` architecture entry). What that asymmetry costs in practice: a `docx → odt → docx` round trip of a Word-authored `m:d` comes back as explicit `mo` fence tokens inside an `mrow` rather than as an auto-growing `m:d` delimiter again, an `m:nary` comes back as a scripted operator followed by its operand rather than as an `m:nary`, and an `m:sPre` degrades outright on the way back out, since `mmultiscripts` is one of the constructs `src/omml/write.ts` has no OMML expression for. The mathematics survives every one of those hops; only the specific OMML construct that expressed it does not. Three further real, tracked read-side boundaries: an equation inside a TABLE CELL is not recovered (a cell's paragraphs are blocks of a `ContentTableCell`, not top-level blocks, so they neither participate in the `w:p`-ordinal correspondence nor have a top-level position to splice into the same scope line `buildDocxPackage`'s own `appendCellBlock` draws on the write side); OMML records no geometry whatsoever, so a recovered block's `frame` is a stand-in whose only meaningful field is `heightPt`, taken from the equation's own `w:rPr/w:sz` when it states one and from Word's own 11pt body default otherwise, stated as the exact inverse of `src/layout/engine.ts`'s `frameHeightPt / 2` size estimate; and an `mtext` that carried an explicit `mathvariant` was written as an ordinary styled math run, which OMML gives no way to distinguish from a styled `mi`, so it reads back as `mi`/`mn`/`mo` rather than as `mtext`.
690
+ - **OMML is read as well as written, but the two directions are deliberately not symmetric in coverage.** `readDocxContent` recovers a docx equation as a real `ContentEmbeddedObjectBlock` carrying its own MathML — the identical shape `readOdtContent` produces for an ODF embedded formula — so `docxToPdf` typesets a Word-authored equation, and `odt → docx → odt` carries a formula through as a formula. The reader covers strictly more than the writer emits, because it has to read what Word wrote rather than only what this package wrote: `m:d`, `m:nary`, `m:acc`, `m:bar`, `m:func`, and `m:sPre` have exact MathML inverses and no writer counterpart at all (see the `src/omml/` architecture entry). What that asymmetry costs in practice: a `docx → odt → docx` round trip of a Word-authored `m:d` comes back as explicit `mo` fence tokens inside an `mrow` rather than as an auto-growing `m:d` delimiter again, an `m:nary` comes back as a scripted operator followed by its operand rather than as an `m:nary`, and an `m:sPre` degrades outright on the way back out, since `mmultiscripts` is one of the constructs `src/omml/write.ts` has no OMML expression for. The mathematics survives every one of those hops; only the specific OMML construct that expressed it does not. Three further real, tracked read-side boundaries: an equation inside a TABLE CELL is now recovered too — `spliceDocxEmbeddedObjects` descends into every table's cells (and any table nested in a cell, recursively), pairing each `ContentTableCell` with its own `w:tc` and splicing the formula into THAT cell's blocks rather than only walking top-level paragraphs; OMML records no geometry whatsoever, so a recovered block's `frame` is a stand-in whose only meaningful field is `heightPt`, taken from the equation's own `w:rPr/w:sz` when it states one and from Word's own 11pt body default otherwise, stated as the exact inverse of the frame fit `src/layout/shared.ts`'s `formulaSizePtForFrame` applies; and an `mtext` that carried an explicit `mathvariant` was written as an ordinary styled math run, which OMML gives no way to distinguish from a styled `mi`, so it reads back as `mi`/`mn`/`mo` rather than as `mtext`.
691
691
  - **The OMML translator covers exactly the construct set `src/mathml/layout.ts` typesets, no more — the two are kept aligned deliberately, not by accident.** `mrow`/`mstyle`/`semantics` flatten (every OMML argument slot already holds a sequence, so OMML has no row element of its own); `mi`/`mn`/`mo`/`mtext` become `m:r`/`m:t` runs, with `mtext` written as OMML normal text (`m:nor`) and every `mathvariant` mapped onto the `m:scr` script + `m:sty` style pair — a mapping with no residue, since OMML's two axes span MathML's fourteen values exactly. The honest limits: a stretchy fence is written as an ordinary operator run rather than as an auto-growing `m:d` delimiter — which now genuinely DIVERGES from the PDF path, where a fence does stretch to its content (see the stretchy-fence gotcha above): Word will render the docx fence at its base size where the PDF renders it assembled and full height. A tracked, bounded gap, not a silent one; closing it means emitting a real `m:d` with the fence characters as its `m:begChr`/`m:endChr`, which is a different write shape from the run-per-token one the rest of this translator uses. `munderover` becomes a nested `m:limUpp`/`m:limLow` pair rather than an `m:nary`, because `m:nary`'s own `m:e` slot is the *operand* being summed and MathML records no operand inside `munderover` at all (it sits outside as a following sibling, with nothing marking where it ends — choosing one would be guessing at operand scope), and `mspace` becomes a single literal space with an `approximated-element` diagnostic, since OMML has no width-parameterised spacer anywhere in its vocabulary. `mathvariant` is carried as markup only: the characters themselves stay in their base form rather than being rewritten into the Mathematical Alphanumeric Symbols block the way `applyMathVariant` does for glyph rendering, which would double-apply the style in Word. The `xmlns:m` declaration goes on the fragment's own root rather than on `w:document`, so an equation appended through `DocxParagraph.appendOfficeMath` stays valid inside a docx this package did not scaffold.
692
692
  - **`sourcePath` traces a `LayoutItem` back to the `ContentDocument` node it came from, but only within one read+layout pass.** `ooxml.js`'s `readDocx`/`readPptx` stamp every `ContentRun`/`ContentImageBlock`/`ContentTable`/`ContentShape` with a positional path (`sections[0].blocks[2].runs[1]`, `slides[1].shapes[3].blocks[0]`); `convertWordprocessingToLayout`/`convertPresentationToLayout` copy that same string onto whichever `LayoutText`/`LayoutImage`/`LayoutLink`/`LayoutRect` item(s) it produces, so a positioned PDF-side item can be traced back to its semantic origin. When line-wrapping splits one run's word across a run boundary, every resulting fragment gets its own run's path (not a shared or merged one); when a single run is emergency-split across several lines or pages, every resulting fragment keeps that same one run's path unchanged. A table cell's background `LayoutRect` is attributed to its containing table's own `sourcePath`, since `ContentTableCell` carries none of its own. This is **not** an edit-tracking or incremental-relayout mechanism — the path is only valid against the exact `ContentDocument`/`Package` it was assigned from in that one read; editing the document, re-reading it, or reordering its blocks invalidates every previously-captured path, and nothing here recomputes or diffs paths across two versions of a document.
693
693
  - **`readMarkdownContent` passes markdown-codec's `readMarkdown` return value straight through, unlike `readDocxContent`/`readOdtContent`/etc., which build a fresh `ContentDocument` envelope from a narrower, format-specific shape.** `markdown-codec`'s own `readMarkdown` already produces a full `document-schema.js` `ContentDocument` directly (`kind`/`formatVersion`/`metadata`/`sections`) — the identical `ContentDocument` type `documents.js` itself imports and re-exports from `document-schema.js`, with no local schema of its own to reconcile against — so, after narrowing to the `wordprocessing` variant, there is nothing left to rebuild.
@@ -732,7 +732,7 @@ The prose below is authoritative; this table is a quick-reference summary of it,
732
732
 
733
733
  **docx/pptx/odt/odp/ods/odg → PDF** is a genuine layout render: the docx/odt flow/pagination engine and the pptx/odp direct-placement engine both produce real positioned text, images, tables, and (for docx/odt) numbered/bulleted lists, styled through the full cascade (theme fonts/colours, `basedOn` chains, placeholder inheritance for docx/pptx; `style:default-style`/`style:parent-style-name` chains for odt/odp). `odg` renders its vector primitives (rect/ellipse/line/path, the last emitted as real PDF `m`/`l`/`c`/`h` content-stream operators, not a polygon approximation of any curve) and reuses the pptx/odp direct-placement engine's own shape conversion for whatever text it also carries. It is a faithful **visual approximation**, not a pixel- or line-identical reproduction of what Word/PowerPoint/Writer/Impress/Draw would themselves render — how close depends on which typeface the document asks for and whether it embedded one, see the font-resolution gotcha above.
734
734
 
735
- **odf → PDF (`odfToPdf`), and a formula embedded inside odt/odp/ods,** render **faithful mathematical typesetting**, not a static image or a plain-text placeholder: real box-model layout (script/limit positioning, fraction/radical geometry with correct rule thickness, table column alignment, `mathvariant` → Mathematical Alphanumeric Symbols mapping) through the embedded STIX Two Math font, with genuine per-glyph metrics (advance width, italic correction, top-accent attachment) and font-wide layout constants (axis height, fraction/radical rule thickness and gaps, script shift amounts) parsed directly from that font's own `MATH` table — not approximated or hand-tuned. A vertical fence around a tall construct genuinely stretches too, assembled from the font's own `MathVariants` pieces and sized to what it wraps, rather than drawn at a fixed base size — and an over/under-brace spanning its own `munder`/`mover`/`munderover` base stretches horizontally on the identical basis, sized to that base's own width. The honest limits: `msqrt`/`mroot` still draw a hand-built radical sign rather than the font's own stretched one (a structural reason, not because the font data is unavailable — see the Gotchas entries above); `mover`/`munder` centre geometrically rather than at the font's own declared accent-attachment point; and the operator dictionary and Greek `mathvariant` mapping each cover a deliberately bounded, common-case set rather than the full specification. For a formula anchored to a spreadsheet **cell**, position is genuinely resolved against that sheet's real column/row geometry (verified end to end against a real LibreOffice-authored `.ods`), but the rendered **size** comes from the same frame-height heuristic every engine uses, which over-estimates for a stacked formula — see the Gotchas entry on `formulaSizePtFromFrame`. **`pdfToOdf` (PDF → structured MathML) is not attempted, on either direction** — recovering a semantic operator tree (is this pair of glyphs a fraction, or a coincidentally stacked pair of ordinary characters? is a raised glyph a superscript, or just a smaller font size used for emphasis?) from nothing but positioned glyphs and paths is a categorically different, OCR-adjacent problem, with no geometry-reconstruction analogue anywhere else in this package: `reconstructWordprocessing`/`reconstructPresentation` recover paragraph/shape *structure* from geometry, never semantic *meaning* the way recognising a fraction would require.
735
+ **odf → PDF (`odfToPdf`), and a formula embedded inside odt/odp/ods,** render **faithful mathematical typesetting**, not a static image or a plain-text placeholder: real box-model layout (script/limit positioning, fraction/radical geometry with correct rule thickness, table column alignment, `mathvariant` → Mathematical Alphanumeric Symbols mapping) through the embedded STIX Two Math font, with genuine per-glyph metrics (advance width, italic correction, top-accent attachment) and font-wide layout constants (axis height, fraction/radical rule thickness and gaps, script shift amounts) parsed directly from that font's own `MATH` table — not approximated or hand-tuned. A vertical fence around a tall construct genuinely stretches too, assembled from the font's own `MathVariants` pieces and sized to what it wraps, rather than drawn at a fixed base size — and an over/under-brace spanning its own `munder`/`mover`/`munderover` base stretches horizontally on the identical basis, sized to that base's own width. The honest limits: `msqrt`/`mroot` still draw a hand-built radical sign rather than the font's own stretched one (a structural reason, not because the font data is unavailable — see the Gotchas entries above); `mover`/`munder` centre geometrically rather than at the font's own declared accent-attachment point; and the operator dictionary and Greek `mathvariant` mapping each cover a deliberately bounded, common-case set rather than the full specification. For a formula anchored to a spreadsheet **cell**, position is genuinely resolved against that sheet's real column/row geometry (verified end to end against a real LibreOffice-authored `.ods`), and the rendered **size** comes from the same two-pass frame fit every engine uses (`formulaSizePtForFrame`) — see the Gotchas entry on it for the docx-OMML width-less fallback. **`pdfToOdf` (PDF → structured MathML) is not attempted, on either direction** — recovering a semantic operator tree (is this pair of glyphs a fraction, or a coincidentally stacked pair of ordinary characters? is a raised glyph a superscript, or just a smaller font size used for emphasis?) from nothing but positioned glyphs and paths is a categorically different, OCR-adjacent problem, with no geometry-reconstruction analogue anywhere else in this package: `reconstructWordprocessing`/`reconstructPresentation` recover paragraph/shape *structure* from geometry, never semantic *meaning* the way recognising a fraction would require.
736
736
 
737
737
  **PDF → docx/pptx/odt/odp** is necessarily a **best-effort reconstruction** from geometry: a PDF page is just positioned glyphs and images, with no semantic paragraph or shape structure to recover. Reading order, bold/italic/colour/font-size, and page/slide count are preserved; paragraph and text-block boundaries are inferred from baseline spacing and left-margin indentation, not recovered exactly. Two further kinds of content are recovered on top of that text, each on its own explicit terms: a real `ContentTable`, but **only** where a genuine drawn gridline lattice is detected, never from text alignment (which would be inventing structure, not recovering it); and a page's vector primitives, into a nested drawing document that currently reaches the `ContentDocument` pivot but not the output bytes. Both are covered in full by their own [Gotchas](#gotchas-and-quirks) entries.
738
738
 
@@ -8,6 +8,8 @@ let odf_js = require("odf.js");
8
8
  //#region src/edit/ods/column-row.ts
9
9
  const VISIBILITY_ATTR = "table:visibility";
10
10
  const VISIBILITY_COLLAPSE = "collapse";
11
+ const DEFAULT_COLUMN_WIDTH_PT = 64;
12
+ const DEFAULT_ROW_HEIGHT_PT = 15;
11
13
  function currentColumnStyleProperties(pkg, columnElement) {
12
14
  const styleName = (0, ooxml_js.attr)(columnElement, "table:style-name");
13
15
  const styleElement = styleName === void 0 ? void 0 : (0, odf_js.findStyleElement)(styleName, "table-column", pkg);
@@ -67,24 +69,34 @@ function writeColumnWidth(pkg, tableElement, index, widthPt) {
67
69
  function writeRowHeight(pkg, tableElement, index, heightPt) {
68
70
  applyRowStyleProperties(pkg, tableElement, index, { heightPt });
69
71
  }
72
+ function ensureColumnDefaultWidth(pkg, tableElement, index) {
73
+ require_edit_ods_address.ensureColumnCoverage(tableElement, index + 1);
74
+ if (currentColumnStyleProperties(pkg, require_edit_ods_address.replaceRun(tableElement.children, require_edit_ods_address.isElementWithTag("table:table-column"), index, "table:number-columns-repeated", () => require_xml_fragment.el("table:table-column"), "table:table-header-columns")).widthPt === void 0) writeColumnWidth(pkg, tableElement, index, DEFAULT_COLUMN_WIDTH_PT);
75
+ }
76
+ function ensureRowDefaultHeight(pkg, tableElement, index) {
77
+ if (currentRowStyleProperties(pkg, require_edit_ods_address.replaceRun(tableElement.children, require_edit_ods_address.isElementWithTag("table:table-row"), index, "table:number-rows-repeated", () => require_xml_fragment.el("table:table-row"), "table:table-header-rows")).heightPt === void 0) writeRowHeight(pkg, tableElement, index, DEFAULT_ROW_HEIGHT_PT);
78
+ }
70
79
  function writeColumnManualBreak(pkg, tableElement, index) {
71
80
  applyColumnStyleProperties(pkg, tableElement, index, { manualBreak: true });
72
81
  }
73
82
  function writeRowManualBreak(pkg, tableElement, index) {
74
83
  applyRowStyleProperties(pkg, tableElement, index, { manualBreak: true });
75
84
  }
76
- function writeColumnHidden(tableElement, index, hidden) {
77
- require_edit_ods_address.ensureColumnCoverage(tableElement, index + 1);
85
+ function writeColumnHidden(pkg, tableElement, index, hidden) {
86
+ ensureColumnDefaultWidth(pkg, tableElement, index);
78
87
  const columnElement = require_edit_ods_address.replaceRun(tableElement.children, require_edit_ods_address.isElementWithTag(require_edit_ods_address.COLUMN_TAG), index, require_edit_ods_address.COLUMN_REPEAT_ATTR, () => require_xml_fragment.el(require_edit_ods_address.COLUMN_TAG), require_edit_ods_address.HEADER_COLUMNS_TAG);
79
88
  if (hidden) require_xml_edit.setAttr(columnElement, VISIBILITY_ATTR, VISIBILITY_COLLAPSE);
80
89
  else require_xml_edit.removeAttr(columnElement, VISIBILITY_ATTR);
81
90
  }
82
- function writeRowHidden(tableElement, index, hidden) {
91
+ function writeRowHidden(pkg, tableElement, index, hidden) {
92
+ ensureRowDefaultHeight(pkg, tableElement, index);
83
93
  const rowElement = require_edit_ods_address.replaceRun(tableElement.children, require_edit_ods_address.isElementWithTag(require_edit_ods_address.ROW_TAG), index, require_edit_ods_address.ROW_REPEAT_ATTR, () => require_xml_fragment.el(require_edit_ods_address.ROW_TAG), require_edit_ods_address.HEADER_ROWS_TAG);
84
94
  if (hidden) require_xml_edit.setAttr(rowElement, VISIBILITY_ATTR, VISIBILITY_COLLAPSE);
85
95
  else require_xml_edit.removeAttr(rowElement, VISIBILITY_ATTR);
86
96
  }
87
97
  //#endregion
98
+ exports.ensureColumnDefaultWidth = ensureColumnDefaultWidth;
99
+ exports.ensureRowDefaultHeight = ensureRowDefaultHeight;
88
100
  exports.writeColumnHidden = writeColumnHidden;
89
101
  exports.writeColumnManualBreak = writeColumnManualBreak;
90
102
  exports.writeColumnWidth = writeColumnWidth;
@@ -2,9 +2,11 @@ import { Package, XmlElement } from "odf.js";
2
2
  //#region src/edit/ods/column-row.d.ts
3
3
  declare function writeColumnWidth(pkg: Package, tableElement: XmlElement, index: number, widthPt: number): void;
4
4
  declare function writeRowHeight(pkg: Package, tableElement: XmlElement, index: number, heightPt: number): void;
5
+ declare function ensureColumnDefaultWidth(pkg: Package, tableElement: XmlElement, index: number): void;
6
+ declare function ensureRowDefaultHeight(pkg: Package, tableElement: XmlElement, index: number): void;
5
7
  declare function writeColumnManualBreak(pkg: Package, tableElement: XmlElement, index: number): void;
6
8
  declare function writeRowManualBreak(pkg: Package, tableElement: XmlElement, index: number): void;
7
- declare function writeColumnHidden(tableElement: XmlElement, index: number, hidden: boolean): void;
8
- declare function writeRowHidden(tableElement: XmlElement, index: number, hidden: boolean): void;
9
+ declare function writeColumnHidden(pkg: Package, tableElement: XmlElement, index: number, hidden: boolean): void;
10
+ declare function writeRowHidden(pkg: Package, tableElement: XmlElement, index: number, hidden: boolean): void;
9
11
  //#endregion
10
- export { writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
12
+ export { ensureColumnDefaultWidth, ensureRowDefaultHeight, writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
@@ -2,9 +2,11 @@ import { Package, XmlElement } from "odf.js";
2
2
  //#region src/edit/ods/column-row.d.ts
3
3
  declare function writeColumnWidth(pkg: Package, tableElement: XmlElement, index: number, widthPt: number): void;
4
4
  declare function writeRowHeight(pkg: Package, tableElement: XmlElement, index: number, heightPt: number): void;
5
+ declare function ensureColumnDefaultWidth(pkg: Package, tableElement: XmlElement, index: number): void;
6
+ declare function ensureRowDefaultHeight(pkg: Package, tableElement: XmlElement, index: number): void;
5
7
  declare function writeColumnManualBreak(pkg: Package, tableElement: XmlElement, index: number): void;
6
8
  declare function writeRowManualBreak(pkg: Package, tableElement: XmlElement, index: number): void;
7
- declare function writeColumnHidden(tableElement: XmlElement, index: number, hidden: boolean): void;
8
- declare function writeRowHidden(tableElement: XmlElement, index: number, hidden: boolean): void;
9
+ declare function writeColumnHidden(pkg: Package, tableElement: XmlElement, index: number, hidden: boolean): void;
10
+ declare function writeRowHidden(pkg: Package, tableElement: XmlElement, index: number, hidden: boolean): void;
9
11
  //#endregion
10
- export { writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
12
+ export { ensureColumnDefaultWidth, ensureRowDefaultHeight, writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
@@ -7,6 +7,8 @@ import { findStyleElement, formatOdfLength, parseOdfLength } from "odf.js";
7
7
  //#region src/edit/ods/column-row.ts
8
8
  const VISIBILITY_ATTR = "table:visibility";
9
9
  const VISIBILITY_COLLAPSE = "collapse";
10
+ const DEFAULT_COLUMN_WIDTH_PT = 64;
11
+ const DEFAULT_ROW_HEIGHT_PT = 15;
10
12
  function currentColumnStyleProperties(pkg, columnElement) {
11
13
  const styleName = attr(columnElement, "table:style-name");
12
14
  const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-column", pkg);
@@ -66,22 +68,30 @@ function writeColumnWidth(pkg, tableElement, index, widthPt) {
66
68
  function writeRowHeight(pkg, tableElement, index, heightPt) {
67
69
  applyRowStyleProperties(pkg, tableElement, index, { heightPt });
68
70
  }
71
+ function ensureColumnDefaultWidth(pkg, tableElement, index) {
72
+ ensureColumnCoverage(tableElement, index + 1);
73
+ if (currentColumnStyleProperties(pkg, replaceRun(tableElement.children, isElementWithTag("table:table-column"), index, "table:number-columns-repeated", () => el("table:table-column"), "table:table-header-columns")).widthPt === void 0) writeColumnWidth(pkg, tableElement, index, DEFAULT_COLUMN_WIDTH_PT);
74
+ }
75
+ function ensureRowDefaultHeight(pkg, tableElement, index) {
76
+ if (currentRowStyleProperties(pkg, replaceRun(tableElement.children, isElementWithTag("table:table-row"), index, "table:number-rows-repeated", () => el("table:table-row"), "table:table-header-rows")).heightPt === void 0) writeRowHeight(pkg, tableElement, index, DEFAULT_ROW_HEIGHT_PT);
77
+ }
69
78
  function writeColumnManualBreak(pkg, tableElement, index) {
70
79
  applyColumnStyleProperties(pkg, tableElement, index, { manualBreak: true });
71
80
  }
72
81
  function writeRowManualBreak(pkg, tableElement, index) {
73
82
  applyRowStyleProperties(pkg, tableElement, index, { manualBreak: true });
74
83
  }
75
- function writeColumnHidden(tableElement, index, hidden) {
76
- ensureColumnCoverage(tableElement, index + 1);
84
+ function writeColumnHidden(pkg, tableElement, index, hidden) {
85
+ ensureColumnDefaultWidth(pkg, tableElement, index);
77
86
  const columnElement = replaceRun(tableElement.children, isElementWithTag(COLUMN_TAG), index, COLUMN_REPEAT_ATTR, () => el(COLUMN_TAG), HEADER_COLUMNS_TAG);
78
87
  if (hidden) setAttr(columnElement, VISIBILITY_ATTR, VISIBILITY_COLLAPSE);
79
88
  else removeAttr(columnElement, VISIBILITY_ATTR);
80
89
  }
81
- function writeRowHidden(tableElement, index, hidden) {
90
+ function writeRowHidden(pkg, tableElement, index, hidden) {
91
+ ensureRowDefaultHeight(pkg, tableElement, index);
82
92
  const rowElement = replaceRun(tableElement.children, isElementWithTag(ROW_TAG), index, ROW_REPEAT_ATTR, () => el(ROW_TAG), HEADER_ROWS_TAG);
83
93
  if (hidden) setAttr(rowElement, VISIBILITY_ATTR, VISIBILITY_COLLAPSE);
84
94
  else removeAttr(rowElement, VISIBILITY_ATTR);
85
95
  }
86
96
  //#endregion
87
- export { writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
97
+ export { ensureColumnDefaultWidth, ensureRowDefaultHeight, writeColumnHidden, writeColumnManualBreak, writeColumnWidth, writeRowHeight, writeRowHidden, writeRowManualBreak };
@@ -44,10 +44,10 @@ var OdsSheet = class {
44
44
  require_edit_ods_column_row.writeRowHeight(this.pkg, this.live(), index, heightPt);
45
45
  }
46
46
  setColumnHidden(index, hidden) {
47
- require_edit_ods_column_row.writeColumnHidden(this.live(), index, hidden);
47
+ require_edit_ods_column_row.writeColumnHidden(this.pkg, this.live(), index, hidden);
48
48
  }
49
49
  setRowHidden(index, hidden) {
50
- require_edit_ods_column_row.writeRowHidden(this.live(), index, hidden);
50
+ require_edit_ods_column_row.writeRowHidden(this.pkg, this.live(), index, hidden);
51
51
  }
52
52
  addImage(image) {
53
53
  require_edit_ods_floating.insertSheetImage(this.pkg, this.live(), image);
@@ -56,8 +56,11 @@ var OdsSheet = class {
56
56
  require_edit_ods_floating.insertSheetEmbeddedObject(this.pkg, this.live(), object);
57
57
  }
58
58
  cell(row, column) {
59
- const node = require_edit_ods_address.resolveCellNode(this.live(), row, column);
59
+ const tableElement = this.live();
60
+ const node = require_edit_ods_address.resolveCellNode(tableElement, row, column);
60
61
  if (node.tag === "table:covered-table-cell") throw new Error(`cell (${row}, ${column}) is covered by a merged range -- address the merge's own anchor cell instead`);
62
+ require_edit_ods_column_row.ensureColumnDefaultWidth(this.pkg, tableElement, column);
63
+ require_edit_ods_column_row.ensureRowDefaultHeight(this.pkg, tableElement, row);
61
64
  return new require_edit_ods_cell.OdsCell(node, this.pkg);
62
65
  }
63
66
  cellAt(reference) {
@@ -70,6 +73,8 @@ var OdsSheet = class {
70
73
  const tableElement = this.live();
71
74
  const anchorNode = require_edit_ods_address.resolveCellNode(tableElement, startRow, startColumn);
72
75
  if (anchorNode.tag === "table:covered-table-cell") throw new Error(`mergeCells: (${startRow}, ${startColumn}) is already covered by another merged range`);
76
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) require_edit_ods_column_row.ensureRowDefaultHeight(this.pkg, tableElement, startRow + rowOffset);
77
+ for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) require_edit_ods_column_row.ensureColumnDefaultWidth(this.pkg, tableElement, startColumn + columnOffset);
73
78
  if (rowSpan > 1) require_xml_edit.setAttr(anchorNode, "table:number-rows-spanned", String(rowSpan));
74
79
  if (colSpan > 1) require_xml_edit.setAttr(anchorNode, "table:number-columns-spanned", String(colSpan));
75
80
  for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) {
@@ -1,7 +1,7 @@
1
1
  import { removeChild, setAttr } from "../../xml/edit.js";
2
2
  import { COVERED_CELL_TAG, resolveCellNode } from "./address.js";
3
3
  import { OdsCell } from "./cell.js";
4
- import { writeColumnHidden, writeColumnWidth, writeRowHeight, writeRowHidden } from "./column-row.js";
4
+ import { ensureColumnDefaultWidth, ensureRowDefaultHeight, writeColumnHidden, writeColumnWidth, writeRowHeight, writeRowHidden } from "./column-row.js";
5
5
  import { insertSheetEmbeddedObject, insertSheetImage } from "./floating.js";
6
6
  import { readSheetPrintSettings, writeSheetPrintSettings } from "./print-settings.js";
7
7
  import { attr } from "ooxml.js";
@@ -43,10 +43,10 @@ var OdsSheet = class {
43
43
  writeRowHeight(this.pkg, this.live(), index, heightPt);
44
44
  }
45
45
  setColumnHidden(index, hidden) {
46
- writeColumnHidden(this.live(), index, hidden);
46
+ writeColumnHidden(this.pkg, this.live(), index, hidden);
47
47
  }
48
48
  setRowHidden(index, hidden) {
49
- writeRowHidden(this.live(), index, hidden);
49
+ writeRowHidden(this.pkg, this.live(), index, hidden);
50
50
  }
51
51
  addImage(image) {
52
52
  insertSheetImage(this.pkg, this.live(), image);
@@ -55,8 +55,11 @@ var OdsSheet = class {
55
55
  insertSheetEmbeddedObject(this.pkg, this.live(), object);
56
56
  }
57
57
  cell(row, column) {
58
- const node = resolveCellNode(this.live(), row, column);
58
+ const tableElement = this.live();
59
+ const node = resolveCellNode(tableElement, row, column);
59
60
  if (node.tag === "table:covered-table-cell") throw new Error(`cell (${row}, ${column}) is covered by a merged range -- address the merge's own anchor cell instead`);
61
+ ensureColumnDefaultWidth(this.pkg, tableElement, column);
62
+ ensureRowDefaultHeight(this.pkg, tableElement, row);
60
63
  return new OdsCell(node, this.pkg);
61
64
  }
62
65
  cellAt(reference) {
@@ -69,6 +72,8 @@ var OdsSheet = class {
69
72
  const tableElement = this.live();
70
73
  const anchorNode = resolveCellNode(tableElement, startRow, startColumn);
71
74
  if (anchorNode.tag === "table:covered-table-cell") throw new Error(`mergeCells: (${startRow}, ${startColumn}) is already covered by another merged range`);
75
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) ensureRowDefaultHeight(this.pkg, tableElement, startRow + rowOffset);
76
+ for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) ensureColumnDefaultWidth(this.pkg, tableElement, startColumn + columnOffset);
72
77
  if (rowSpan > 1) setAttr(anchorNode, "table:number-rows-spanned", String(rowSpan));
73
78
  if (colSpan > 1) setAttr(anchorNode, "table:number-columns-spanned", String(colSpan));
74
79
  for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) {
@@ -6,7 +6,8 @@ type OmmlDiagnosticSink = (diagnostic: OmmlDiagnostic, context: {
6
6
  readonly sourcePath?: string;
7
7
  }) => void;
8
8
  declare function collectBodyParagraphs(nodes: readonly XmlNode[], out: XmlElement[]): void;
9
+ declare function collectBodyTables(nodes: readonly XmlNode[], out: XmlElement[]): void;
9
10
  declare const PARAGRAPH_NON_CONTENT_TAGS: ReadonlySet<string>;
10
11
  declare function equationFrame(equation: XmlElement): Box;
11
12
  //#endregion
12
- export { equationFrame as i, PARAGRAPH_NON_CONTENT_TAGS as n, collectBodyParagraphs as r, OmmlDiagnosticSink as t };
13
+ export { equationFrame as a, collectBodyTables as i, PARAGRAPH_NON_CONTENT_TAGS as n, collectBodyParagraphs as r, OmmlDiagnosticSink as t };
@@ -6,7 +6,8 @@ type OmmlDiagnosticSink = (diagnostic: OmmlDiagnostic, context: {
6
6
  readonly sourcePath?: string;
7
7
  }) => void;
8
8
  declare function collectBodyParagraphs(nodes: readonly XmlNode[], out: XmlElement[]): void;
9
+ declare function collectBodyTables(nodes: readonly XmlNode[], out: XmlElement[]): void;
9
10
  declare const PARAGRAPH_NON_CONTENT_TAGS: ReadonlySet<string>;
10
11
  declare function equationFrame(equation: XmlElement): Box;
11
12
  //#endregion
12
- export { equationFrame as i, PARAGRAPH_NON_CONTENT_TAGS as n, collectBodyParagraphs as r, OmmlDiagnosticSink as t };
13
+ export { equationFrame as a, collectBodyTables as i, PARAGRAPH_NON_CONTENT_TAGS as n, collectBodyParagraphs as r, OmmlDiagnosticSink as t };
@@ -175,7 +175,7 @@ function layoutFormulaFlow(block, section, pages, state, contentLeftXDown, conte
175
175
  layoutFormulaFallback(block, section, pages, state, contentLeftXDown, contentWidthPt, contentBottomYDown, measurer);
176
176
  return;
177
177
  }
178
- const sizePt = require_layout_shared.formulaSizePtFromFrame(block.frame.heightPt);
178
+ const sizePt = require_layout_shared.formulaSizePtForFrame(formula.mathml, block.frame);
179
179
  const metrics = (0, pdf_codec.loadMathFont)().metricsAt(sizePt);
180
180
  const { box } = require_mathml_layout.layoutFormula(formula.mathml, {
181
181
  metrics,
@@ -1,7 +1,7 @@
1
1
  import { flipY } from "../model/geometry.js";
2
2
  import { formulaOfBlock, formulaPlaceholderText } from "../model/formula.js";
3
3
  import { layoutFormula } from "../mathml/layout.js";
4
- import { alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtFromFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, sumColumnWidthsPt } from "./shared.js";
4
+ import { alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtForFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, sumColumnWidthsPt } from "./shared.js";
5
5
  import { COLOR_BLACK, LAYOUT_FORMAT_VERSION } from "document-schema.js";
6
6
  import { loadMathFont, wrapRunsToWidth } from "pdf-codec";
7
7
  //#region src/layout/engine.ts
@@ -174,7 +174,7 @@ function layoutFormulaFlow(block, section, pages, state, contentLeftXDown, conte
174
174
  layoutFormulaFallback(block, section, pages, state, contentLeftXDown, contentWidthPt, contentBottomYDown, measurer);
175
175
  return;
176
176
  }
177
- const sizePt = formulaSizePtFromFrame(block.frame.heightPt);
177
+ const sizePt = formulaSizePtForFrame(formula.mathml, block.frame);
178
178
  const metrics = loadMathFont().metricsAt(sizePt);
179
179
  const { box } = layoutFormula(formula.mathml, {
180
180
  metrics,
@@ -1,12 +1,22 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_mathml_layout = require("../mathml/layout.cjs");
2
3
  let ooxml_js = require("ooxml.js");
3
4
  let pdf_codec = require("pdf-codec");
4
5
  let document_schema_js = require("document-schema.js");
5
6
  //#region src/layout/shared.ts
6
7
  const NOMINAL_TEXT_SIZE_PT = 18;
7
8
  const MIN_FORMULA_SIZE_PT = 8;
8
- function formulaSizePtFromFrame(frameHeightPt) {
9
- return Math.max(MIN_FORMULA_SIZE_PT, frameHeightPt / 2);
9
+ const REFERENCE_FORMULA_SIZE_PT = 12;
10
+ function formulaSizePtForFrame(mathml, frame) {
11
+ const referenceMetrics = (0, pdf_codec.loadMathFont)().metricsAt(REFERENCE_FORMULA_SIZE_PT);
12
+ const { box } = require_mathml_layout.layoutFormula(mathml, {
13
+ metrics: referenceMetrics,
14
+ sizePt: REFERENCE_FORMULA_SIZE_PT,
15
+ color: document_schema_js.COLOR_BLACK
16
+ });
17
+ const heightScale = frame.heightPt / box.heightPt;
18
+ const widthScale = frame.widthPt > 0 ? frame.widthPt / box.widthPt : Number.POSITIVE_INFINITY;
19
+ return Math.max(MIN_FORMULA_SIZE_PT, REFERENCE_FORMULA_SIZE_PT * Math.min(heightScale, widthScale));
10
20
  }
11
21
  const FALLBACK_ROW_HEIGHT_PT = 20;
12
22
  function runFont(run) {
@@ -152,7 +162,7 @@ exports.NOMINAL_TEXT_SIZE_PT = NOMINAL_TEXT_SIZE_PT;
152
162
  exports.alignmentOffsetPt = alignmentOffsetPt;
153
163
  exports.effectiveStyledRuns = effectiveStyledRuns;
154
164
  exports.estimateRowHeightPt = estimateRowHeightPt;
155
- exports.formulaSizePtFromFrame = formulaSizePtFromFrame;
165
+ exports.formulaSizePtForFrame = formulaSizePtForFrame;
156
166
  exports.justifyLineGapsPt = justifyLineGapsPt;
157
167
  exports.lineNaturalHeightPt = lineNaturalHeightPt;
158
168
  exports.pushCellBorderLines = pushCellBorderLines;
@@ -1,9 +1,9 @@
1
1
  import { i as LayoutFont, t as Alignment } from "../style-DkamR3aY.cjs";
2
- import { Box, ContentCellBorders, ContentImageBlock, ContentRun, ContentTableRow, LayoutImageAsset, LayoutItem } from "document-schema.js";
2
+ import { Box, ContentCellBorders, ContentImageBlock, ContentRun, ContentTableRow, LayoutImageAsset, LayoutItem, MathMlNode } from "document-schema.js";
3
3
  import { StyledRun, TextMeasurer, WrappedLine } from "pdf-codec";
4
4
  //#region src/layout/shared.d.ts
5
5
  declare const NOMINAL_TEXT_SIZE_PT = 18;
6
- declare function formulaSizePtFromFrame(frameHeightPt: number): number;
6
+ declare function formulaSizePtForFrame(mathml: readonly MathMlNode[], frame: Box): number;
7
7
  declare function runFont(run: ContentRun): LayoutFont;
8
8
  declare function toStyledRuns(runs: readonly ContentRun[], fontScale?: number): StyledRun[];
9
9
  declare function effectiveStyledRuns(runs: readonly ContentRun[], fontScale?: number): StyledRun[];
@@ -15,4 +15,4 @@ declare function registerImage(block: ContentImageBlock, images: Record<string,
15
15
  declare function sumColumnWidthsPt(columnWidthsPt: readonly number[], startIndex: number, span: number): number;
16
16
  declare function estimateRowHeightPt(row: ContentTableRow, measurer: TextMeasurer, columnWidthsPt: readonly number[], scale: number): number;
17
17
  //#endregion
18
- export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtFromFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };
18
+ export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtForFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };
@@ -1,9 +1,9 @@
1
1
  import { i as LayoutFont, t as Alignment } from "../style-DkamR3aY.js";
2
- import { Box, ContentCellBorders, ContentImageBlock, ContentRun, ContentTableRow, LayoutImageAsset, LayoutItem } from "document-schema.js";
2
+ import { Box, ContentCellBorders, ContentImageBlock, ContentRun, ContentTableRow, LayoutImageAsset, LayoutItem, MathMlNode } from "document-schema.js";
3
3
  import { StyledRun, TextMeasurer, WrappedLine } from "pdf-codec";
4
4
  //#region src/layout/shared.d.ts
5
5
  declare const NOMINAL_TEXT_SIZE_PT = 18;
6
- declare function formulaSizePtFromFrame(frameHeightPt: number): number;
6
+ declare function formulaSizePtForFrame(mathml: readonly MathMlNode[], frame: Box): number;
7
7
  declare function runFont(run: ContentRun): LayoutFont;
8
8
  declare function toStyledRuns(runs: readonly ContentRun[], fontScale?: number): StyledRun[];
9
9
  declare function effectiveStyledRuns(runs: readonly ContentRun[], fontScale?: number): StyledRun[];
@@ -15,4 +15,4 @@ declare function registerImage(block: ContentImageBlock, images: Record<string,
15
15
  declare function sumColumnWidthsPt(columnWidthsPt: readonly number[], startIndex: number, span: number): number;
16
16
  declare function estimateRowHeightPt(row: ContentTableRow, measurer: TextMeasurer, columnWidthsPt: readonly number[], scale: number): number;
17
17
  //#endregion
18
- export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtFromFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };
18
+ export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtForFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };
@@ -1,12 +1,22 @@
1
1
  import { COLOR_BLACK } from "../model/color.js";
2
2
  import { DEFAULT_LAYOUT_FONT } from "../model/style.js";
3
+ import { layoutFormula } from "../mathml/layout.js";
3
4
  import { base64ToBytes } from "ooxml.js";
4
- import { crc32, decodePng, readJpegInfo, wrapRunsToWidth } from "pdf-codec";
5
+ import { crc32, decodePng, loadMathFont, readJpegInfo, wrapRunsToWidth } from "pdf-codec";
5
6
  //#region src/layout/shared.ts
6
7
  const NOMINAL_TEXT_SIZE_PT = 18;
7
8
  const MIN_FORMULA_SIZE_PT = 8;
8
- function formulaSizePtFromFrame(frameHeightPt) {
9
- return Math.max(MIN_FORMULA_SIZE_PT, frameHeightPt / 2);
9
+ const REFERENCE_FORMULA_SIZE_PT = 12;
10
+ function formulaSizePtForFrame(mathml, frame) {
11
+ const referenceMetrics = loadMathFont().metricsAt(REFERENCE_FORMULA_SIZE_PT);
12
+ const { box } = layoutFormula(mathml, {
13
+ metrics: referenceMetrics,
14
+ sizePt: REFERENCE_FORMULA_SIZE_PT,
15
+ color: COLOR_BLACK
16
+ });
17
+ const heightScale = frame.heightPt / box.heightPt;
18
+ const widthScale = frame.widthPt > 0 ? frame.widthPt / box.widthPt : Number.POSITIVE_INFINITY;
19
+ return Math.max(MIN_FORMULA_SIZE_PT, REFERENCE_FORMULA_SIZE_PT * Math.min(heightScale, widthScale));
10
20
  }
11
21
  const FALLBACK_ROW_HEIGHT_PT = 20;
12
22
  function runFont(run) {
@@ -148,4 +158,4 @@ function estimateRowHeightPt(row, measurer, columnWidthsPt, scale) {
148
158
  return max;
149
159
  }
150
160
  //#endregion
151
- export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtFromFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };
161
+ export { NOMINAL_TEXT_SIZE_PT, alignmentOffsetPt, effectiveStyledRuns, estimateRowHeightPt, formulaSizePtForFrame, justifyLineGapsPt, lineNaturalHeightPt, pushCellBorderLines, registerImage, runFont, sumColumnWidthsPt, toStyledRuns };