Bidirectional docx/pptx/odt/odp/ods/odg ⇄ PDF conversion, a resolver-driven odm (ODF master document) → PDF conversion for multi-chapter documents, six further cross-format bridges (odt⇄docx, odp⇄pptx, ods⇄xlsx) that bypass PDF entirely,
.odb(ODF database front-end) table extraction to xlsx/CSV from an embedded HSQLDB TEXT script (Tier 1), HSQLDB's own binary CACHED-table row-store format (Tier 2), and an embedded Firebird database's own gbak logical-backup format (Tier 3), a read-and-write live-view editor for docx/pptx/odt/odp/ods/odg content, a hand-written MathML presentation-layer typesetting engine with embedded-font PDF rendering (odf → PDF, plus formulas embedded inside odt/odp), and a fully hand-written PDF codec, built on ooxml.js and odf.js.
documents.js depends on ooxml.js for lossless docx/pptx/xlsx ⇄ JSON handling and extends it in two directions ooxml.js deliberately does not cover: full PDF support (parsing arbitrary real-world PDFs and generating new ones), and a read-and-write manipulation API for docx/pptx content — ooxml.js's own typed readers (readDocx/readPptx) are one-way and explicitly forbid write-back. PDF reading, writing, and the docx⇄PDF/pptx⇄PDF conversion pipeline are entirely hand-written: no external PDF library (pdf-lib, pdfjs-dist, mupdf, or any other) is a dependency. The one exception is fflate for raw DEFLATE/zlib compression underneath PDF's FlateDecode filter and PNG's IDAT chunks — the same dependency ooxml.js itself already relies on for ZIP handling. src/mathml/ (the MathML typesetting engine) and the OpenType/CFF font parsing this package's own PDF writer uses to embed a real math font (src/pdf/sfnt.ts/math-*.ts) are both hand-written too, for the same "no supply-chain surface beyond what's already declared" reason — the one bundled binary asset is the vendored STIX Two Math font itself (OFL-1.1, see Fidelity and assets/fonts/NOTICE.md), not a library.
Converting docx/pptx to PDF and back is usually solved by wrapping a mature third-party PDF library. This package takes the opposite approach: every layer of the PDF format — the object model, the cross-reference table, the content-stream operators, standard-font metrics, the parser's cross-reference/object-stream resolution and content-stream interpreter — is hand-written against the ISO 32000-1 specification. That is a genuinely large undertaking (the PDF codec is comparable in size to the rest of the package combined), and it comes with an honest trade-off spelled out in Fidelity below: this is not, and does not attempt to be, as robust against adversarial or badly malformed real-world PDFs as a library with 15+ years of hardening. What it buys instead is a dependency-free, fully auditable PDF implementation with no supply-chain surface beyond ooxml.js and fflate.
The read-and-write editor exists because ooxml.js's own typed readers are a deliberate one-way, lossy projection — reading is fine, but there is no way to add a paragraph, style a run, or insert an image and get a valid docx/pptx back out. documents.js's editors are live views directly over the XmlElement objects inside a decoded Package: a mutation edits that tree in place, and everything you don't touch round-trips byte-faithful, because it never stopped being the original XML.
Requires Node.js >=20 and pnpm 11.6.0 (pinned via packageManager in package.json).
pnpm installInstall as a dependency in another project:
pnpm add documents.js
# or
npm install documents.jsThe twelve round-trip ergonomic conversions (docx/pptx/odt/odp/ods/odg ⇄ PDF, all now round-trip both ways):
import { docxToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, pdfToDocx, pdfToOdg, pdfToOdp, pdfToOds, pdfToOdt, pptxToPdf, pdfToPptx } from 'documents.js';
const pdfBytes = docxToPdf(docxBytes);
const docxBytes2 = pdfToDocx(pdfBytes);
const pdfFromSlides = pptxToPdf(pptxBytes);
const pptxBytes2 = pdfToPptx(pdfFromSlides);
const pdfFromOdt = odtToPdf(odtBytes);
const odtBytes2 = pdfToOdt(pdfFromOdt);
const pdfFromOdp = odpToPdf(odpBytes);
const odpBytes2 = pdfToOdp(pdfFromOdp);
const pdfFromOdg = odgToPdf(odgBytes);
const odgBytes2 = pdfToOdg(pdfFromOdg);
const pdfFromOds = odsToPdf(odsBytes);
const odsBytes2 = pdfToOds(pdfFromOds); // recovers what was printed, not what was entered -- see FidelityEach accepts an optional signal (AbortSignal) and either a onSubstitution callback (docx/pptx/odt/odp/ods/odg → PDF, called once per character not representable in a standard-14 font) or a sink (PDF → docx/pptx/odt/odp/ods/odg, called once per recoverable parse diagnostic).
Six further conversions bypass PDF entirely: odtToDocx/docxToOdt, odpToPptx/pptxToOdp, and odsToXlsx/xlsxToOds each compose a direct readXContent → buildYPackage pivot copy, since both sides of each pair already read into and build from the identical ContentDocument variant — no layout engine, no font measurement, and no geometry-based reconstruction in between. See Fidelity for what that means in practice.
import { odtToDocx, docxToOdt } from 'documents.js';
const docxBytes = odtToDocx(odtBytes);
const odtBytes2 = docxToOdt(docxBytes);Each takes an optional { signal } — there is no onSubstitution/sink option here, since there is no font substitution or PDF-parse degradation to report; a wrong-kind ContentDocument throws outright rather than becoming a diagnostic.
The same conversions behind a swappable port, for a caller that wants to inject a different implementation later without changing call sites:
import { createLocalDocumentConverter } from 'documents.js';
const converter = createLocalDocumentConverter();
const { document, diagnostics } = await converter.convert(
{ source: { format: 'docx', bytes: docxBytes }, targetFormat: 'pdf' },
{ signal: new AbortController().signal },
);DocumentFormat includes xlsx alongside docx/pptx/odt/odp/ods/odg/pdf — not because xlsx has a PDF conversion of its own, but because createLocalDocumentConverter's { source, targetFormat } contract already generalises past "targetFormat always means pdf": odt→docx, docx→odt, odp→pptx, pptx→odp, ods→xlsx, and xlsx→ods are six further entries in the same conversions list, routed to the six bridge functions above with an empty diagnostics array.
Getting back the intermediate DocumentPackage (content + layout, from document-content-model) a conversion built internally, instead of only the target bytes — every ergonomic conversion function above accepts an onDocument callback for this, and the port surfaces the same value as package on its ConversionResult:
import { docxToPdf } from 'documents.js';
const pdfBytes = docxToPdf(docxBytes, {
onDocument: (pkg) => {
console.log(pkg.content.kind); // 'wordprocessing'
console.log(pkg.layout?.pages.length); // populated for every X-to-PDF/PDF-to-X conversion
},
});
// or via the port:
const { document, package: pkg } = await converter.convert(
{ source: { format: 'docx', bytes: docxBytes }, targetFormat: 'pdf' },
{ signal: new AbortController().signal },
);For the six PDF-bypassing bridges, pkg.layout is always undefined — a bridge never runs a layout engine, so there is nothing to populate it with; running one purely to fill this field would be wasted work no caller asked for.
Reading and editing docx/pptx content directly, without going through PDF at all:
import { openDocx, createDocx } from 'documents.js';
const editor = openDocx(existingDocxBytes);
const paragraph = editor.body.appendParagraph({ alignment: 'center' });
const run = paragraph.appendRun({ text: 'Hello' });
run.bold = true;
run.color = { r: 1, g: 0, b: 0 };
const bytes = editor.toBytes();
// or start from nothing:
const fresh = createDocx();
fresh.body.appendParagraph().appendRun({ text: 'New document' });openPptx/createPptx and PptxSlide/PptxShape are the pptx equivalent (slide.addTextBox, slide.addImage, shape.setParagraphs for multi-paragraph styled text).
openOdt/createOdt and OdtParagraph/OdtRun/OdtTable/OdtList are the odt equivalent, built on ODF's own style-name-referencing model (run.bold = true interns or reuses a named style:style in office:automatic-styles, rather than writing an inline attribute — see Conventions below). openOdp/createOdp and OdpSlide/OdpShape are the odp equivalent of PptxSlide/PptxShape (slide.addTextBox, slide.addImage, slide.notes), and reuse OdtParagraph/OdtRun/OdtList directly for a shape's own text content — a draw:frame's draw:text-box holds the identical text:p/text:span model office:text does, interned into the same content.xml style registry:
import { createOdp } from 'documents.js';
const editor = createOdp();
const slide = editor.addSlide();
const title = slide.addTextBox({ frame: { xPt: 40, yPt: 30, widthPt: 640, heightPt: 80 }, text: 'Title' });
title.rotationDeg = 15; // OdpShape has a genuine draw:transform rotation setter, unlike PptxShape's own documented gap below
const bullets = slide.addTextBox({ frame: { xPt: 40, yPt: 130, widthPt: 300, heightPt: 200 }, text: '' });
bullets.paragraphs()[0].remove();
bullets.addList().addItem().appendParagraph({ text: 'A real bulleted text:list' });
slide.notes = 'Speaker notes for this slide';
const bytes = editor.toBytes();createOds/openOds and OdsEditor/OdsSheet/OdsCell are the spreadsheet equivalent — cell addressing has no docx/pptx analogue at all, so this is the one editor family built from scratch rather than reusing OdtParagraph/OdtRun. Setting a cell far from the origin does not materialise every cell in between: the underlying table:number-columns-repeated/table:number-rows-repeated runs are split in place at exactly the target position, the same repeat-compression convention odf.js's own reader already reads. OdsSheet.printSettings is a genuine getter/setter too (src/edit/ods/print-settings.ts) — a set mints a fresh style:page-layout/style:master-page/style:style[family="table"] chain and repoints the sheet at it, rather than mutating whatever it was pointing at before, matching this package's own append-only style-editing convention throughout.
import { createOds } from 'documents.js';
const editor = createOds();
const sheet = editor.addSheet('Sheet1');
sheet.printSettings = { pageSize: { widthPt: 595, heightPt: 842 }, margins: { topPt: 20, rightPt: 20, bottomPt: 20, leftPt: 20 }, gridlines: true, headers: true, pageOrder: 'downThenOver' };
sheet.cell(0, 0).value = { kind: 'string', value: 'Total' }; // 0-based (row, column) -- there is no A1-string overload
sheet.cell(0, 1).value = { kind: 'currency', value: 42.5, currency: 'USD' };
sheet.cell(500, 50).value = { kind: 'boolean', value: true }; // does not materialise 500x50 empty cells
const bytes = editor.toBytes();createOdg/openOdg and OdgEditor/OdgPage are the drawing equivalent — a page-level container (draw:page), extended with the vector-primitive setters a drawing carries that a presentation typically doesn't. OdgPage.addTextBox/.addImage return real OdpShape instances (draw:frame's content model is byte-for-byte identical between odp and odg — see Architecture); addRect/addEllipse/addLine/addPath return OdgBoxVector/OdgLineVector/OdgPathVector, writing real draw:rect/draw:ellipse/draw:line/draw:path elements. A vector's own paint order is purely document order — the same convention real LibreOffice output already uses, so an earlier add* call paints behind a later one, with no draw:z-index attribute ever written.
import { createOdg } from 'documents.js';
const editor = createOdg();
const page = editor.addPage();
page.addRect({ frame: { xPt: 20, yPt: 20, widthPt: 100, heightPt: 60 }, fill: { r: 1, g: 0.5, b: 0 } });
page.addEllipse({ frame: { xPt: 140, yPt: 20, widthPt: 100, heightPt: 60 }, stroke: { color: { r: 0, g: 0, b: 0 }, widthPt: 1 } });
page.addPath({
frame: { xPt: 20, yPt: 100, widthPt: 80, heightPt: 80 },
subpaths: [{ start: { xPt: 0, yPt: 80 }, closed: true, segments: [{ kind: 'line', to: { xPt: 60, yPt: 80 } }, { kind: 'cubic', control1: { xPt: 80, yPt: 80 }, control2: { xPt: 80, yPt: 0 }, to: { xPt: 40, yPt: 0 } }] }],
fill: { r: 1, g: 1, b: 0 },
}); // a genuine Bezier curve -- writes a real svg:d/svg:viewBox pair, not a polygon approximation
page.addTextBox({ frame: { xPt: 20, yPt: 200, widthPt: 300, heightPt: 30 }, text: 'A label on top' });
const bytes = editor.toBytes();buildOdsPackage bridges a spreadsheet ContentDocument (either one from readOdsContent, or a best-effort one from reconstructSpreadsheet) to a fresh package built entirely through the same primitives — pdfToOds's own package-building half, mirroring buildOdtPackage/buildOdpPackage's role for pdfToOdt/pdfToOdp. buildOdgPackage bridges a drawing ContentDocument (either one from readOdgContent, or a best-effort one from reconstructDrawing) to a fresh package built entirely through the same primitives — pdfToOdg's own package-building half.
Reading and writing PDF bytes directly, without going through docx/pptx:
import { readPdf, writePdf } from 'documents.js';
const layout = readPdf(pdfBytes); // -> LayoutDocument: pages of positioned text/image/rect/link items
const bytes = writePdf(layout);The same seven round trips (PDF ⇄ LayoutDocument, docx ⇄ PDF, pptx ⇄ PDF, odt ⇄ PDF, odp ⇄ PDF, ods ⇄ PDF, odg ⇄ PDF) are each also available as a schema-validated z.codec() pair, mirroring ooxml.js's own packageCodec — z.decode/z.encode validate both the raw bytes (against the magic-byte schemas below) and the parsed value (against LayoutDocumentSchema) on every call, catching a malformed value that a bare function call wouldn't. This is the no-extra-options form: readPdf/writePdf/docxToPdf/etc. remain the entry points for cancellation (signal), diagnostics (sink), or substitution reporting (onSubstitution), none of which fit z.codec()'s fixed decode(input)/encode(output) signature.
import { z } from 'zod';
import { docxPdfCodec, pdfCodec, pptxPdfCodec } from 'documents.js';
const layout = z.decode(pdfCodec, pdfBytes); // throws a ZodError if pdfBytes has no %PDF- header
const pdfBytes2 = z.encode(pdfCodec, layout);
const pdfFromDocx = z.decode(docxPdfCodec, docxBytes);
const docxBack = z.encode(docxPdfCodec, pdfFromDocx);The six PDF-bypassing bridges above get the same treatment: odtDocxCodec, odpPptxCodec, and odsXlsxCodec (odt bytes ⇄ docx bytes, odp bytes ⇄ pptx bytes, ods bytes ⇄ xlsx bytes) — the no-options form again, odtToDocx/docxToOdt/etc. remain the entry points for signal.
readDocxContent/readPptxContent/readOdtContent/readOdpContent/readOdsContent/readOdgContent (docx/pptx/odt/odp/ods/odg → ContentDocument), convertWordprocessingToLayout/convertPresentationToLayout/convertSpreadsheetToLayout/convertDrawingToLayout (ContentDocument → LayoutDocument), and reconstructWordprocessing/reconstructPresentation/reconstructSpreadsheet/reconstructDrawing (LayoutDocument → ContentDocument) are each exported individually too, for a caller that wants one stage of the pipeline without the rest. readDocxContent and readOdtContent both produce the identical wordprocessing-variant ContentDocument shape from two completely unrelated package formats (OOXML and ODF), which is what lets odtToPdf feed convertWordprocessingToLayout without a single line of that engine changing; readPptxContent and readOdpContent do the same for the presentation variant and convertPresentationToLayout. readOdgContent/convertDrawingToLayout has no OOXML-side counterpart at all (no drawing-equivalent OOXML format this package reads); readOdsContent/convertSpreadsheetToLayout now does have one on the read side — ooxml.js's own readXlsxContent — but only for the PDF-bypassing odsToXlsx/xlsxToOds bridge below, not for the PDF pivot: xlsx has no PDF conversion of its own, so convertSpreadsheetToLayout still has no xlsx-layout counterpart to reuse or be reused by. Both convertSpreadsheetToLayout and convertDrawingToLayout are genuinely new layout algorithms, since a spreadsheet's addressed-grid-with-print-settings semantics and a drawing's vector-primitive vocabulary (rect/ellipse/line/path) have no flow/pagination or direct-placement analogue; convertDrawingToLayout does still reuse convertPresentationToLayout's own shape-conversion logic (convertShape, exported from src/layout/slides.ts) verbatim for whatever text/image/table content a drawing page also carries. reconstructDrawing is reconstructWordprocessing/reconstructPresentation's drawing-side counterpart, but does no baseline/paragraph clustering at all — a drawing has no semantic structure to recover, only a near-1:1 LayoutItem → ContentVector/ContentShape mapping to make, in the same paint order the items were recovered in. reconstructSpreadsheet is a genuinely different geometry-recovery problem from either: a real gridline lattice on the page (drawn by a printed sheet with gridlines enabled) is used DIRECTLY as cell boundaries when one is detected; absent one, text is clustered into a 2D grid from geometry alone. It recovers what was printed, not what was entered — every cell comes back a bare string, never re-parsed into a number/date/boolean or claimed as a formula (see Fidelity).
One further conversion, odmToPdf, is shaped differently from every conversion above: a .odm (ODF master document, a "book" of chapters) never carries its own chapters' content — each text:section is a bare external reference to a standalone .odt file, confirmed against real LibreOffice output (see Gotchas below) — so producing a PDF needs a caller-supplied resolveSubDocument callback to hand back each chapter's own bytes given that section's href. Every chapter's own ContentSection[] is concatenated in text:section document order into one combined document, with an explicit page break marking each chapter boundary, and fed through the same convertWordprocessingToLayout engine every wordprocessing-variant conversion above already uses unmodified:
import { readFileSync } from 'node:fs';
import { odmToPdf, OdmUnresolvedSectionError } from 'documents.js';
const chapterBytes = new Map([
['../chapter1.odt', new Uint8Array(readFileSync('chapter1.odt'))],
['../chapter2.odt', new Uint8Array(readFileSync('chapter2.odt'))],
]);
try {
const pdfBytes = odmToPdf(odmBytes, {
resolveSubDocument: (href) => chapterBytes.get(href),
});
} catch (error) {
if (error instanceof OdmUnresolvedSectionError) {
console.error('missing chapters:', error.hrefs); // every unresolved href, not just the first
}
}odmToPdf is not one of the twelve round-trip conversions or the six bridges above, has no z.codec() pair, and is not wired into the DocumentConverter port below — see Gotchas for why.
.odb (ODF database front-end) support: readOdbTables extracts every table an embedded database declares, and odbToXlsx/odbToCsv turn that straight into xlsx or CSV bytes. Three embedded storage shapes are supported, dispatched automatically from the package's own connection URL and, for HSQLDB, its own per-table storage shape: a MEMORY/TEXT table's rows inline in database/script as ordinary TEXT-format SQL (Tier 1, src/hsqldb/script.ts), a CACHED table's rows in a separate binary page-cache file, database/data (Tier 2, src/hsqldb/cache.ts/rowformat.ts — LibreOffice's own embedded-HSQLDB default, see Architecture/Gotchas for the exact scope and version pinning), and a Firebird database's own database/firebird.fbk part — LibreOffice's modern default embedded engine since 4.1, a genuine gbak logical-backup stream rather than a raw on-disk database file (Tier 3; see the Gotchas entry below for the empirical finding this rests on). A caller never needs to know which shape or engine a given .odb used. HSQLDB's own whole-script BINARY/COMPRESSED serialisation is the one embedded shape still out of scope (see Gotchas/Fidelity):
import { decodePackage } from 'odf.js';
import { odbToCsv, odbToXlsx, readOdbTables } from 'documents.js';
const xlsxBytes = odbToXlsx(odbBytes); // one xlsx sheet per table, a header row of column names then one row per record
const csvBytes = odbToCsv(odbBytes, { table: 'CUSTOMERS' }); // exactly one named table as CSV -- required whenever the .odb has more than one table
const tables = readOdbTables(decodePackage(odbBytes)); // Package -> HsqldbTable[], for a caller that wants the raw table/column/row data without going through xlsx or CSV -- the identical shape whether the .odb is HSQLDB- or Firebird-backedreadOdbTables takes a decoded Package (matching readOdtContent/readOdsContent/etc.'s own convention), while odbToXlsx/odbToCsv take raw bytes and decode them internally, matching every other ergonomic conversion in this package. .odb has no PDF conversion, no reverse (xlsx/CSV → .odb) direction, and — like odmToPdf — is not wired into the DocumentConverter port below, since Reports require live SQL execution to render (categorically out of scope) and the write direction would need a real embedded SQL engine this package deliberately does not implement.
readFirebirdBackup (src/firebird/backup.ts) is also exported individually, for a caller that has already extracted a Firebird-backed .odb's own database/firebird.fbk bytes and wants to decode them directly without going through a Package at all:
import { readFirebirdBackup } from 'documents.js';
const { summary, tables } = readFirebirdBackup(firebirdBackupBytes); // summary: backupFormatVersion/transportable/compressed/pageSizeBytes; tables: the same HsqldbTable[] shapeA standalone .odf (an ODF formula document) converts to PDF via odfToPdf, rendering the formula's own real MathML through a hand-written typesetting engine (src/mathml/) and the embedded STIX Two Math font, not a static image or a StarMath-text placeholder:
import { odfToPdf } from 'documents.js';
const pdfBytes = odfToPdf(odfBytes); // a single formula (or small formula document), faithfully typeset -- see FidelityodfToPdf is not one of the twelve round-trip conversions above either: there is no pdfToOdf (recovering structured MathML from rendered glyphs is a categorically different, OCR-adjacent problem, not a geometry-reconstruction one — see Fidelity), no z.codec() pair, and — unlike odmToPdf — it is wired into the DocumentConverter port below, as a DocumentFormat: 'odf' source with only a 'pdf' target.
Standalone .odf files are rare in practice; a formula embedded inside an odt paragraph or an odp slide is the far more common real-world case, and odtToPdf/odpToPdf already render one automatically wherever readOdtContent/readOdpContent find a draw:frame referencing an embedded formula sub-object — no extra code needed at the call site:
import { odtToPdf } from 'documents.js';
// odtBytes contains an ordinary paragraph followed by an embedded formula object (LibreOffice: Insert > Object > Formula) --
// the formula renders as real typeset MathML in the output PDF, at the position and approximate size of its own source frame.
const pdfBytes = odtToPdf(odtBytes);This works by threading the formula's own raw MathML alongside the ContentDocument readOdtContent/readOdpContent already produce (see OdtContentResult/OdpContentResult's own formulas field) rather than through ContentDocument itself, since document-content-model's own ContentEmbeddedObject.document field has no MathML-shaped variant to carry it in:
import { convertWordprocessingToLayout, readOdtContent } from 'documents.js';
const { document, formulas } = readOdtContent(pkg); // formulas: ReadonlyMap<sourcePath, EmbeddedFormula>
const { document: layout, formulas: positioned } = convertWordprocessingToLayout(document, { measurer, formulas });
const pdfBytes = writePdf(layout, { formulas: positioned }); // writePdf's own formula-aware option -- see ArchitecturelayoutFormula (the typesetting engine's own entry point) and loadMathFont (the embedded STIX Two Math font, parsed and cached once per process) are each exported individually too, for a caller that wants to lay out a formula directly:
import { layoutFormula, loadMathFont } from 'documents.js';
const { metricsAt } = loadMathFont();
const { box, diagnostics } = layoutFormula(mathml, { metrics: metricsAt(12), sizePt: 12, color: { r: 0, g: 0, b: 0 } });
// box: a MathBox -- positioned glyph runs, fraction/radical rules, and radical-hook strokes, ready for src/pdf's own math-content-write.ts
// diagnostics: a 'missing-glyph' or 'unsupported-element' entry for anything this engine couldn't render faithfully -- see FidelityThe package is layered from generic primitives outward to the two conversion directions:
src/model/— thin, documents.js-specific additions on top of the siblingdocument-content-modelpackage, which now owns the two pivot models themselves:LayoutDocument(the PDF-side pivot: pages of positioned text/image/rect/line/ellipse/path/link items, PDF-native coordinates and units —LayoutPathis a general vector path, one or more subpaths of line/cubic segments sharing one fill/fillRule/stroke, the item kindwritePath, src/pdf/content-write.ts, turns into PDFm/l/c/hcontent-stream operators) andContentDocument(the semantic pivot: a discriminated union ofwordprocessing,presentation,spreadsheet, anddrawingvariants sharing paragraph/run/table/image building blocks,drawing's ownContentVectorvocabulary — rect/ellipse/line/path — being the vector-primitive counterpart to the sharedContentShape) are both imported, not defined here —document-content-modelexists specifically soooxml.js,odf.js, anddocuments.jsshare one schema instead of each maintaining an independent, drift-prone copy. What remains local:bytes.ts(magic-byte-validatedUint8Arrayschemas for docx/pptx/PDF, plusOdt/Ods/Odp/OdgBytesSchema, which check the package's actual declared media type againstodf.js'sODF_MEDIA_TYPEStable rather than only the generic ZIP signature the OOXML schemas are limited to),units.ts(OOXML EMU/twip/point/half-point conversions), andgeometry.ts/color.ts/style.ts, each now mostly a thin re-export ofdocument-content-model'sBox/Margins/PageSize/Color/Alignment/LayoutFont— the one genuinely PDF-specific piece each still adds locally isgeometry.ts'sflipY(the top-left/y-down ↔ bottom-left/y-up space conversion between OOXML/ODF and PDF coordinates);LayoutFont/DEFAULT_LAYOUT_FONTmoved todocument-content-modeltoo (sinceLayoutText, part of the pivot, needs the field), leaving only the standard-14 font resolution logic that consumes it (src/pdf/fonts.ts/font-read.ts) as PDF-specific and local.content.tsholds the one genuinely local piece of theContentDocumentenvelope (CONTENT_FORMAT_VERSION), andformula.tsdefinesEmbeddedFormula/PositionedFormula— the side-channel formula shapes threaded alongside aContentDocument/LayoutDocumentrather than inside them (see the Usage section above) — the one file insrc/model/with a local dependency of its own, a type-only import ofMathBox/MathMlNodefrommathml(see the dependency-direction note below).src/bytes/andsrc/image/— generic byte and image-container primitives with zero PDF or OOXML knowledge: a chunked byte writer, a backtracking byte reader, CRC32, and a hand-written PNG decoder/encoder (palette/gray/RGB/alpha, multi-IDATfiles, all five scanline filters) plus JPEG marker scanning for dimensions only — JPEG's compressed bytes pass through completely unchanged in both directions.src/bytes/flate.tsis the only file that importsfflate, mirroring howooxml.js's ownsrc/zip.tswraps it for ZIP handling.src/ports/— the two injectable ports this package's own "identity, clock, and observability are first-class ports" convention calls for:abort.ts'sthrowIfAborted(a signal-check helper called at page/slide/row loop boundaries throughoutsrc/pdf/write.ts/read.tsandsrc/layout/sheets.ts/reconstruct.ts— the codebase has noawaitpoint for cancellation to hook into implicitly, since the local pipeline is synchronous end to end, so every long-running loop checks explicitly instead) andclock.ts'sClockPort/systemClock/fixedClock(an injectable "now", for deterministic PDF output in tests).ClockPortis exported and tested in isolation but not yet consumed by any conversion path —writePdf's own/CreationDate//ModDatecome directly fromLayoutDocument.metadata.createdIso/modifiedIsowhen present, with nothing in this package's own write path callingnew Date()to fill in a missing one, so there is currently no real call site forClockPortto inject into. A real, tracked gap in wiring, not a documentation gap: a future default-timestamp write path should consume it rather than reaching fornew Date()directly.src/xml/andsrc/opc/— parent-aware XML query/mutation and OPC package mechanics (relationship IDs, content-type entries, atomic media-part insertion) built overooxml.js'sPackage/XmlNode, needed becauseooxml.js's own XML nodes have no parent pointers andooxml.jsnever writes new parts into an existing package.src/xml/odf-text.tsis the one ODF-specific module in this directory:encodeOdfText/decodeOdfTextconvert between a plain string and ODF's own whitespace-run element sequence (text:sfor a run of two or more literal spaces,text:tab,text:line-break— all three occupy real character positions in an ODF paragraph but are ELEMENTS, not text-node characters, unlike docx's flatw:trun text) — see the Gotchas entry below on why every ODF text getter in this codebase must calldecodeOdfText, neverooxml.js's own plain-text-nodetextContent().src/odf-package/— the ODF-side counterpart tosrc/opc/:manifest.tsis a pure re-export ofodf.js's own manifest read/build/write/sync/validate functions (odf.jsalready ownsMETA-INF/manifest.xmlend to end — reading, deriving, writing, syncing, and validating it — unlikeooxml.js's read-only OPC relationship handling), andmedia.ts'saddImageMediainserts a binary image part underPictures/(the real-world LibreOffice/OASIS convention, confirmed againstodf.js's own round-trip/manifest fixtures) and re-syncs the manifest via that samesyncManifestre-export — one step simpler than OOXML's ownaddImageMedia(src/opc/media.ts) since ODF references a media part directly by its package path (xlink:href) rather than through a relationship-ID indirection.OdpSlide.addImage/OdpShape(src/edit/odp/image.ts) isaddImageMedia's real caller — and, throughsrc/edit/odg/*'s wholesale reuse ofOdpShape(see thesrc/edit/entry below),OdgPage.addImagetoo;src/odb/read.tsalso reusesmanifest.ts'sreadManifestdirectly, to checkdatabase/script's own manifest-declared media type before treating it as an HSQLDB script part.src/edit/— the read-and-write editable model: live-view classes (DocxEditor/DocxParagraph/DocxRun/DocxTable,PptxEditor/PptxSlide/PptxShape,OdtEditor/OdtParagraph/OdtRun/OdtTable/OdtList,OdpEditor/OdpSlide/OdpShape,OdsEditor/OdsSheet/OdsCell,OdgEditor/OdgPage/OdgBoxVector/OdgLineVector/OdgPathVector) wrapping the actualXmlElementobjects inside a decodedPackage, plusbuildDocxPackage/buildPptxPackage/buildOdtPackage/buildOdpPackage/buildOdsPackage/buildOdgPackagebridging aContentDocumentto a fresh package built entirely through those same primitives —pdfToOdt/pdfToOdp/pdfToOds/pdfToOdgeach call the matching one.src/edit/odp/*reusessrc/edit/odt/*'s own paragraph/run/list/style-interning classes WHOLESALE rather than reimplementing them for presentations: adraw:frame'sdraw:text-boxholds the identicaltext:p/text:spancontent modeloffice:textdoes, interned into the identicalcontent.xmloffice:automatic-stylesregistry (src/edit/odt/props.ts'sapplyStyleChange) —OdpShape.appendParagraph/.paragraphs()/.addList()return realOdtParagraph/OdtListinstances, not odp-specific lookalikes. The genuinely new odp-specific work isdraw:page/draw:framemechanics (a slide is adraw:page, a shape's geometry is explicitsvg:x/svg:y/svg:width/svg:heightrather than pptx's placeholder-inheritance-heavy model) and rotation:OdpShape.rotationDegis a genuinedraw:transformsetter built onodf.js's ownapplyOdfTransform/resolveOdfShapeGeometry(typed/shared/transform.ts) — the write-side inverse of the exact function odf.js's own reader uses — unlikePptxShape, which has no rotation setter yet (see Gotchas below).src/edit/ods/*has no docx/pptx/odt/odp analogue to reuse for its core concern (cell addressing) but still reusessrc/edit/odt/*'s style interning andsrc/edit/odt/content.ts'spopulateParagraphfor cell text content —src/edit/ods/address.tsis the write-side counterpart toodf.js's own read-sidetable:number-*-repeated-aware cursor: setting a distant cell's value splits the covering repeated run in place at that one position rather than materialising every cell in between, exactly mirroring the read-side hazardodf.js's owntyped/shared/a1.tsalready solved.src/edit/ods/print-settings.tsis the newest addition:OdsSheet.printSettings's own getter/setter, miningstyles.xml'soffice:automatic-styles/office:master-stylesdirectly (a part no othersrc/edit/ods/*module needed to touch before) rather thancontent.xmlalone, reusingodf.js's own exportedfindStyleElement/resolvePageLayoutProperties/parsePageSize/parseMarginsfor the read half andsrc/edit/odt/automatic-styles.ts'snextStyleName(already generic over whichoffice:automatic-styleselement it scans) for the write half's own fresh-name minting.src/edit/odg/*reusesOdpShape/buildTextBoxFrame/insertImageFrameMediaWHOLESALE fordraw:frametext/image content (a drawing page'sdraw:framecontent model and geometry resolution — rotation included — are byte-for-byte identical to a presentation's, both resolved throughodf.js's own sharedreadDrawFrame), so there is no separateOdgShapeclass at all; the genuinely new work is the vector-primitive classes (no rotation, a per-kind attribute vocabulary:svg:x/y/width/heightfor rect/ellipse/path,svg:x1/y1/x2/y2for a line) and their own fill/stroke, which needed a small, self-contained graphic-family style writer (src/edit/odg/style.ts) sinceodf.js's ownStyleRegistryrecognises'graphic'as a style family but itsStylePropertiesSchemaonly ever models text/paragraph formatting — it has no fill/stroke fields and never emits astyle:graphic-propertieselement. A path vector's ownsvg:dis generated bysrc/edit/odg/svg-path.ts, the write-side inverse ofodf.js's owntyped/shared/path.tsparser — always absolute, always space-separated commands, anchoringsvg:viewBoxat"0 0 {widthPt} {heightPt}"so the written numbers are the exact sourceContentPathPointvalues with no rescaling arithmetic either way (see Gotchas below for the cross-check against that exact parser).src/mathml/— a MathML presentation-layer typesetting engine, comparable in scope to the standard-14 text-layout half ofsrc/pdf/— genuinely self-contained: no import frommodel,pdf, orodf.jsat all (not evendocument-content-model), matchingsrc/layout/'s own "pure conversion algorithm" isolation one tier further down.nodes.tsdefinesMathMlNode/MathMlElementas a local, structurally-compatible mirror ofodf.js's ownXmlNode(the same "mirror the shape, don't import the package" tricksrc/interop.test.tsalready proves holds betweenooxml.jsandodf.js), soodf.js'sreadOdfFormula's real return value type-checks against it with zero cast.variant.tsmapsmathvariantto the Unicode Mathematical Alphanumeric Symbols block (Latin/Greek/digits, including the block's own well-known Letterlike-Symbols hole-fillers — italic small h, eleven Script/Fraktur/Double-struck capitals — generated directly from Unicode's ownUnicodeData.txt, not transcribed by hand).operators.tsis a deliberately bounded operator dictionary (lspace/rspace/stretchy/largeop/movablelimits per operator), not the MathML3 spec's own multi-thousand-entry table.layout.tsis the recursive box-model engine itself (mrow/mi/mn/mo/mtext/mspace/msub/msup/msubsup/munder/mover/munderover/mfrac/msqrt/mroot/mtable/mtr/mtd/mstyle/semantics, plus a text-content fallback with a diagnostic for anything else), driven entirely by the injectedMathFontMetricsport (metrics.ts) rather than any font-parsing code of its own —src/pdf/math-font.tsis the real implementation.compose.ts/radical.ts/length.tsare its own small geometry helpers (baseline-offset box placement, a hand-drawn hooked radical sign built from line segments rather than a bare glyph substitute, MathML length-unit parsing). Output is a flatMathBox(positioned glyph runs, rules, and strokes, box-local top-left/y-down coordinates) —src/pdf/math-content-write.tsis the one consumer that turns it into PDF content-stream bytes.src/pdf/— the hand-written PDF codec, importingmodel/bytes/image/mathml(no OOXML knowledge at all):- Write:
objects.ts(thePdfObjectdiscriminated union),afm-widths.ts/encoding.ts/winansi.ts/fonts.ts(standard-14 metrics, WinAnsi encoding, family resolution),measure.ts/text-layout.ts(greedy line-wrapping),matrix.ts,content-write.ts(LayoutItem[]→ content-stream operators),write.ts(the full object graph, classic cross-reference table, trailer, and — whenWritePdfOptions.formulasis non-empty — one embedded math composite font group, allocated once and shared across pages). - Embedded math font:
sfnt.ts(a generic sfnt table-directory reader),math-cmap.ts/math-hmtx.ts/math-table.ts(Unicode → glyph ID, per-glyph advance widths, and the OpenTypeMATHtable's constants/glyph-info subtables — every offset cross-checked against the real vendored font while these were built, not transcribed from the spec alone),math-font.ts(parses and caches the vendored STIX Two Math font once per process, exposing a size-specificMathFontMetricsimplementation),math-font-write.ts(builds the/Type0//CIDFontType0//FontDescriptor//FontFile3/ToUnicode object group),math-content-write.ts(aPositionedFormula[]→ PDF content-stream bytes, Identity-H 2-byte CIDs for text-showing,re/m/loperators for rules and the radical hook). See Fidelity for the CFF-full-embed (not glyph-subsetted) simplification this makes. - Read:
lexer.ts/parse.ts(byte tokenizer and tokens →PdfObject),filters.ts/predictors.ts(Flate/LZW/ASCII85/ASCIIHex/RunLength, TIFF/PNG predictors),xref.ts/document.ts(classic and cross-reference-stream resolution, object streams,/Prevchains, linear-scan recovery, the page tree with attribute inheritance),content-read.ts/interpret.ts(the content-stream tokenizer and graphics/text state machine, including form-XObject recursion),cmap.ts/font-style.ts/font-read.ts(/ToUnicodeCMaps, font-dictionary resolution),images-read.ts(Image XObjects → PNG/JPEG bytes),read.ts(readPdf, assembling all of the above into aLayoutDocument). codec.ts—pdfCodec, az.codec()pair overreadPdf/writePdf(PDF bytes ⇄LayoutDocument).
- Write:
src/ooxml/— resolves aPackageinto aContentDocument:docx/read.tsandpptx/read.tsare now thin adapters overooxml.js's ownreadDocx/readPptx, wrapping their{ metadata, sections }/{ metadata, slides }result intoContentDocument'swordprocessing/presentationshape. The docx style cascade (docDefaults→ named-stylebasedOnchains → paragraph-mark run properties → character styles → direct formatting), the pptx placeholder → layout → master → theme inheritance cascade, and DrawingML geometry/colour resolution all now live upstream inooxml.jsitself, not in this package.src/odf/— the ODF-side counterpart tosrc/ooxml/, resolving anodf.jsPackageinto aContentDocument:odt/read.ts'sreadOdtContentis a thin adapter overodf.js's ownreadOdt, wrapping its{ metadata, sections }result into the identicalwordprocessingshapereadDocxContentproduces — the concrete proof that odt and docx genuinely share one pivot and one layout engine.odp/read.ts'sreadOdpContentis the same adapter overodf.js's ownreadOdp, wrapping{ metadata, slides }into the identicalpresentationshapereadPptxContentproduces.ods/read.ts'sreadOdsContentwrapsodf.js'sreadOds's{ metadata, sheets }into thespreadsheetContentDocumentvariant, andodg/read.ts'sreadOdgContentwrapsodf.js'sreadOdg's{ metadata, pages }into thedrawingvariant —odgstill has no OOXML-side sibling adapter at all (no drawing-equivalent OOXML format this package reads);odsnow does,ooxml.js's ownreadXlsxContent/buildXlsxPackage, consumed directly bysrc/convert/convert.ts'sodsToXlsx/xlsxToOdsbridge (see below) but deliberately not re-exported from this package's own public surface, mirroring thereadDocx/readPptxnon-re-export choice above.buildOdtPackage/buildOdpPackage/buildOdsPackage/buildOdgPackage(src/edit/{odt,odp,ods,odg}/content.ts) each bridge aContentDocumentback to a fresh package built on that format's own live-view editor, closing the PDF → odt/odp/ods/odg direction (pdfToOdt/pdfToOdp/pdfToOds/pdfToOdgeach call the matching one) — see thepdfToOdsgotcha below forbuildOdsPackage's own printSettings-writing addition.formula/read.ts'sreadOdfFormulaContent/readOdfEmbeddedFormulaare the same thin-adapter pattern overodf.js's ownreadOdfFormula, for a standalone.odfand an embedded sub-object respectively (the latter reading the sub-object's owncontent.xml/meta.xmldirectly out of the outer package's flatPackage.partsrecord, no separate unzip step needed);formula/detect.ts'sdetectEmbeddedFormulaFramesis genuinely new work with noodf.js-side equivalent at all —odf.js's ownreadDrawFrameContentdoesn't recognise adraw:object-bearingdraw:frameyet, soodt/read.tsandodp/read.tseach run this as a second pass over the same package's rawcontent.xmlto find and inject a formula's own placeholder block (see the Gotchas entry below for the exact scope and positioning caveats this second pass carries).src/layout/— the pure conversion algorithms, importingmodel, (for formula placement)mathml, and — for line-wrapping/pagination itself — several genuinely PDF-sourced primitives: the injectedTextMeasurerport andwrapRunsToWidth(src/pdf/measure.ts/text-layout.ts, since deciding where a line breaks needs to know how wide text renders in a PDF standard-14 font, regardless of which format the content came from),loadMathFont(src/pdf/math-font.ts, for formula placement),src/pdf/matrix.ts'srotatePointAboutCenter(slides.ts's own shape-rotation placement), andsrc/pdf/afm-widths.ts/fonts.ts'sSTANDARD_METRICS/resolveStandardFont(reconstruct.ts's own font-matching when reconstructing from aLayoutDocument) — the one dependency arrow in this package that runs opposite the general "generic primitives outward" ordering below, since text layout is inherently coupled to the one font model (pdf's own standard-14 resolution) every conversion direction ultimately renders through:engine.ts(ContentDocumentwordprocessing →LayoutDocument: flow, line-breaking, pagination — fed identically by docx- and odt-sourced content; also returnsWordprocessingLayoutResult.formulas, every embedded formula block it laid out viasrc/mathml'slayoutFormula, positioned in PDF page space — see the Gotchas entry below on why a formula can't become an ordinaryLayoutItem),slides.ts(ContentDocumentpresentation →LayoutDocument: direct EMU-to-point placement, no pagination needed — fed identically by pptx- and odp-sourced content; also exportsconvertShape, the single-ContentShape-to-LayoutItem[]conversiondrawing.tsbelow reuses verbatim, now optionally formula-aware via its own trailingformulaContextparameter sodrawing.ts's existing call site keeps compiling unchanged),sheets.ts(ContentDocumentspreadsheet →LayoutDocument: resolve the print range, build cumulative column/row offsets skipping hidden ones, reserve header/repeat-row-column space, resolve an explicit or non-iterative fit-to-page scale, partition into column/row bands honouring manual breaks with the same "an oversized item gets its own band and overflows rather than looping" guaranteeengine.ts'sensureRoomdocuments, emit pages indownThenOver/overThenDownorder, then per page paint backgrounds/gridlines/headers/cell text with default alignment by value kind and###/spill-then-truncate overflow handling — the first layout algorithm in this package that accepts anAbortSignal, since a 50k-cell sheet needs cancellation where a docx/pptx page count never did),drawing.ts(ContentDocumentdrawing →LayoutDocument: oneContentDrawPageper PDF page, direct placement likeslides.ts, with one new emission path — aContentVectorrect/ellipse/linemaps onto the pre-existingLayoutRect/LayoutEllipse/LayoutLinekinds, and apathvector's local, viewBox-relative subpath points are resolved through the vector's own frame offset then a single page-space flip into aLayoutPathvalue; vectors paint before shapes, a documented, bounded choice — see this module's own top-of-file note — sinceContentDrawPageSchemakeepsshapesandvectorsas two independently paint-ordered arrays with no field recording their relative order when the two genuinely overlap),reconstruct.ts(LayoutDocument→ContentDocument:reconstructWordprocessing/reconstructPresentationdo baseline-proximity line clustering, then paragraph/text-block clustering from geometry — PDF has no semantic paragraph or shape structure to recover, only positioned glyphs;reconstructDrawingdoes no clustering at all, since a drawing has no such structure to infer in the first place — everyLayoutItemmaps close to 1:1 back onto aContentVectorrect/ellipse/line/pathor aContentShape, in the exact z-order it was painted, bucketed intoContentDrawPageSchema's own two independently-orderedshapes/vectorsarrays the same waydrawing.tsproduced them;reconstructSpreadsheettries a real gridline lattice first — scanning the page'sLayoutLine/stroked-single-segment-LayoutPathitems for enough parallel horizontal and vertical lines at consistent positions to call it a printed grid, using those line positions directly as cell boundaries when found — and falls back to text-position clustering otherwise, reusing this same module'sclusterIntoLinesfor rows and a parallel recurring-x-position generalisation ofclusterIntoParagraphs's owndominantLeftXfor columns; every recovered cell is a bare string, column widths/row heights are genuinely measured from whichever geometry was used, and no print range/scale/repeat-rows/repeat-columns/manual-breaks are ever inferred).src/hsqldb/— the.odbdecoders, in two tiers over two genuinely different on-disk storage shapes a HSQLDB table can use.script.ts(Tier 1): a small, bounded HSQLDB TEXT-script-format (hsqldb.script_format=0) DDL/DML text parser, not a database engine —parseHsqldbScript(bytes)extractsCREATE TABLE's own column names/types andINSERT INTO's own row values intoHsqldbTable[], tolerating (skipping) every other statement kind real HSQLDB output emits that this package has no use for (users, grants, sequences, indexes, views), and throwingHsqldbScriptParseErrorfor anything matching neither list.rowformat.ts/cache.ts(Tier 2): a CACHED table's own binary row-store format — LibreOffice's embedded-HSQLDB default (database.isStoredFileAccess()switcheshsqldb.default_table_typetocachedspecifically for storage-backed access, confirmed against the decompiled engine source) — a CACHED table's DDL still lives indatabase/scriptas ordinary TEXT (Tier 1 parses it unmodified) but its row data lives in a separate binary page-cache file,database/data.rowformat.tsdecodes one column's own binary field at a time (HsqldbDataCursor, a big-endianDataViewcursor;readHsqldbColumnValue, one branch per SQL type code);cache.tswalks a table's own AVL row-position tree (readHsqldbCachedTableRows, following each row's persistediLeft/iRightchild positions recursively, needing no key-comparison or free-list logic at all — a deleted row is already unlinked from the tree before its space can be reused, so a traversal rooted at the tree's current root only ever reaches live rows), rooted at the positionparseHsqldbIndexRootsrecovers from each table's ownSET TABLE ... INDEX'...'script line, usingparseHsqldbProperties's reading ofdatabase/properties(cache-file scale, engine version) to resolve byte offsets;decodeHsqldbCachedTablesis the orchestrationsrc/odb/read.tscalls, splicing real rows into every table with an index-root line and leaving every other table (MEMORY/TEXT, or a genuinely empty CACHED table — HSQLDB never writes an index-root line for one) exactly as Tier 1 already produced it. Both tiers mirrorsrc/pdf/'s own isolation discipline:script.tsimports onlydocument-content-model'sContentCellValuetype;rowformat.tsimports the same plus nothing else;cache.tsimports only those two andscript.ts's own types — no odf.jsPackage/XmlElementknowledge anywhere insrc/hsqldb/— the caller is responsible for handing every function its raw bytes/text already extracted from a real.odbpackage.HsqldbTable/HsqldbColumnare also the shared pivot shapesrc/firebird/'s own Tier 3 decoder below produces. See Gotchas for Tier 2's own version scope and verification account.src/firebird/— the Tier 3.odbdecoder: a reader for Firebird's own gbak logical-backup format (database/firebird.fbk), the artifact a real Firebird-embedded.odbactually contains — see the README's own Gotchas entry below for the empirical finding that this is NOT a raw on-disk ODS page dump, the single largest correction this subsystem's own design went through.reader.tsholds the two distinct byte-level primitives the format mixes (FirebirdBackupReader, the generic little-endian tag+length+value attribute framing everyrec_*/att_*record uses, plus its own RLE/"PackBits"-style decompression foratt_data_datawhen the backup is compressed;XdrReader, the big-endian, 4-byte-aligned RFC 1832 XDR decoding a row's own field values use once compression is peeled off).blr-types.tsmaps a field's own BLR type opcode (att_field_type) onto its physical storage representation, sourced directly from Firebird's ownblr.h/align.h.date.tsrestates Firebird's own MJD-epoch DATE and 1/10000-second-tick TIME encoding, taken fromNoThrowTimeStamp.cpp.schema.tswalksrec_relation/rec_field(column definitions gbak has ALREADY resolved from the live engine's system tables at backup time — see the Gotchas entry).data.tswalksrec_relation_data/rec_data(a relation's own rows, addressed by name), decoding each row's XDR-and-possibly-RLE-compressed field-value sequence intoContentCellValue[].backup.ts'sreadFirebirdBackupis the top-level entry point, producing the identicalHsqldbTable[]shapeparseHsqldbScriptdoes.src/odb/— the decoder-selection and pivot-mapping layer sitting between odf.js's.odbsupport andsrc/hsqldb//src/firebird/:read.ts'sreadOdbTables(pkg)calls odf.js's ownreadOdbInventoryto classify the package's connection (throwingOdbNoEmbeddedDataSourceErrorfor an external-only datasource) and its embedded engine, then routes a genuine HSQLDB TEXT script toparseHsqldbScript, then — whenever adatabase/datapart is present — hands Tier 1's own result tosrc/hsqldb/cache.ts'sdecodeHsqldbCachedTablesto splice in every CACHED table's real rows (a.odbwith no CACHED table at all, the common case, never even looks fordatabase/data, leaving Tier 1's own result untouched), or routes a Firebirddatabase/firebird.fbkpart toreadFirebirdBackup— throwingOdbUnsupportedFormatError(naming HSQLDB's own whole-script binary/compressed serialisation explicitly, the one embedded shape still unimplemented) for anything none of these cover.spreadsheet.ts'sodbTablesToSpreadsheetDocumentmapsHsqldbTable[]onto the sameContentSheet-basedContentDocumentspreadsheet variantreadOdsContent/buildOdsPackagealready produce and consume, feedingodbToXlsx's call intobuildXlsxPackagedirectly.csv.ts'sbuildOdbTableCsvwrites exactly one named table as CSV bytes, with noContentSheet/xlsx machinery involved at all, throwingOdbTableNotSpecifiedError/OdbTableNotFoundError(naming every available table) when the caller's owntableoption doesn't resolve to exactly one table.src/convert/—convert.ts(the twelve PDF-pivot round-trip ergonomic wrappers, a dedicated "Six cross-format bridges" section:odtToDocx/docxToOdt,odpToPptx/pptxToOdp,odsToXlsx/xlsxToOds, each a directreadXContent→buildYPackagecomposition bypassing PDF entirely — see Fidelity —odmToPdf, the one further conversion shaped around a caller-suppliedresolveSubDocumentcallback rather than being purely bytes-in/bytes-out, since a.odmmaster document's own chapters are external references odf.js'sreadOdmnever inlines — see Gotchas —odbToXlsx/odbToCsv, thin compositions overreadOdbTablesandsrc/odb/'s own pivot/CSV mapping, andodfToPdf, a standalone.odfformula document → PDF viareadOdfFormulaContent→src/mathml'slayoutFormula→writePdf's own formula-aware option, with no reversepdfToOdfat all),codec.ts(docxPdfCodec/pptxPdfCodec/odtPdfCodec/odpPdfCodec/odsPdfCodec/odgPdfCodecplusodtDocxCodec/odpPptxCodec/odsXlsxCodec, az.codec()pair over each —odmToPdf/odbToXlsx/odbToCsv/odfToPdfhave no codec of their own, for the same fixed-signature/one-directional reasons each has no port entry, or a one-way port entry, below),port.ts/local.ts(the swappableDocumentConvertercontract and its synchronous local implementation, coveringdocx/pptx/odt/odp/ods/odg/odf→pdf,pdf→docx/pptx/odt/odp/ods/odg, and the six bridge pairs —DocumentFormatincludesxlsxfor exactly this reason, even though xlsx has no PDF conversion of its own;odmandodbare deliberately notDocumentFormatmembers, since neitherodmToPdfnorodbToXlsx/odbToCsvis wired into this port at all;odfIS a member, but with only the oneodf → pdfentry — nopdf → odf). Every conversion function that builds aContentDocument/LayoutDocumentinternally (the twelve PDF-pivot conversions and the six bridges;odfToPdfaccepts but never invokes it) also accepts anonDocumentcallback, andConversionResultcarries the same value through the port as an optionalpackagefield — the fullDocumentPackage(content + layout, fromdocument-content-model) that conversion built, not just its target bytes.
Dependency direction is downward and checkable, with one deliberate exception (layout, noted below): bytes/mathml/ports import nothing local (mathml is fully self-contained — no dependency on model, document-content-model, or any ODF/PDF package, since it consumes only its own locally-mirrored MathMlNode input and its own injected MathFontMetrics port); model imports nothing local at the value level, but formula.ts carries one type-only import of MathBox/MathMlNode from mathml (erased entirely at runtime, and not a cycle — mathml itself imports nothing from model); image imports bytes only; pdf imports model+bytes+image+mathml+ports; ooxml/* imports model only (now a thin adapter over ooxml.js's own readDocx/readPptx — see the src/ooxml/ entry above — with no xml/* dependency of its own left; no PDF knowledge either); odf/* imports model only (no PDF knowledge, no xml/* — odf.js already owns its own XML query helpers); hsqldb imports document-content-model only (no odf.js knowledge); firebird imports document-content-model (its own row/schema decoding, ContentCellValue only) and hsqldb (HsqldbTable/HsqldbColumn, a type-only import for its own output shape — the deliberate pivot-sharing point between Tier 1 and Tier 3) but no odf.js knowledge at all; layout imports model+mathml+ports, plus, genuinely upward, several text-measurement/font-metric/matrix primitives from pdf itself (measure.ts/text-layout.ts/math-font.ts/matrix.ts/afm-widths.ts/fonts.ts — see the src/layout/ entry above for exactly which); odf-package imports odf.js only (no local dependency, mirroring opc's relationship to ooxml.js); odb imports hsqldb+firebird+model+odf-package+odf.js only; convert composes everything else. No PdfObject/PdfDict/PdfStream type appears outside src/pdf/.
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts)
pnpm typecheck # tsc --noEmit
pnpm lint # eslint . --max-warnings 0
pnpm test # vitest run --project unit
pnpm test:watch # vitest --project unit
pnpm test:smoke # rebuilds dist/, then verifies ESM/CJS parity, a real docxToPdf/pdfToDocx round trip, real odtToPdf/odpToPdf/odsToPdf/odgToPdf conversions (odgToPdf's own fixture carries a real curved path, proving writePath reaches the built dist/ bundle), a real createOdp/odpToPdf/pdfToOdp round trip, a real odsToPdf/pdfToOds round trip plus a separate createOds/printSettings/buildOdsPackage exercise, a real createOdg/odgToPdf/pdfToOdg round trip (a curved path, a filled rect, and text, built entirely through the odg live-view editor, converted to PDF and reconstructed back to odg via reconstructDrawing), and a real odfToPdf conversion (a fraction, rendered via the embedded STIX Two Math font -- checked by confirming the built PDF contains a real /Type0/Identity-H/CIDFontType0C font resource, proving the base64-embedded font asset itself survived the tsdown build), from the built CJS bundle
pnpm test:corpus # optional real-world PDF conformance checks against a local, gitignored test/corpus/ (see Fidelity)To run a single test file: pnpm vitest run src/path/to/file.test.ts.
- Zod-first schema/type/guard, matching
ooxml.js: every model type is inferred from its Zod schema, never hand-written.ContentBlock(recursive, mirroringooxml.js's ownXmlNodetreatment) uses a hand-written structural guard +z.custom, notz.lazy, which collapses tounknownfor recursive element-children in the pinned Zod version. z.codec()for every schema-to-schema round trip, matchingooxml.js'spackageCodec/xmlCodec:pdfCodec(PDF bytes ⇄LayoutDocument),docxPdfCodec/pptxPdfCodec/odtPdfCodec/odpPdfCodec/odsPdfCodec/odgPdfCodec(docx/pptx/odt/odp/ods/odg bytes ⇄ PDF bytes), andodtDocxCodec/odpPptxCodec/odsXlsxCodec(odt/odp/ods bytes ⇄ docx/pptx/xlsx bytes, the PDF-bypassing bridges) each wrap an already-independently-tested function pair, adding automatic two-way schema validation. These are deliberately the no-options form —readPdf/writePdf/docxToPdf/pdfToDocx/pptxToPdf/pdfToPptx/odtToPdf/pdfToOdt/odpToPdf/pdfToOdp/odsToPdf/pdfToOds/odgToPdf/pdfToOdg/odtToDocx/docxToOdt/odpToPptx/pptxToOdp/odsToXlsx/xlsxToOdsremain the primary entry points wherever a caller needs anAbortSignal, aPdfDiagnosticSink, or anonSubstitutioncallback, sincez.codec()'s fixeddecode(input)/encode(output)signature has no room for side-channel options.PdfObjecthas no Zod schema at all, deliberately: it never crosses a public boundary or round-trips through JSON, and is constructed exclusively by this package's own parser — validating it would just be validating our own output. It narrows natively on its ownkinddiscriminant instead, the same reasoningooxml.jsapplies when it picks a hand-writtenisXmlNodeguard overz.lazy.- No type assertions anywhere. Every third-party or loosely-typed value is narrowed through a type guard or a Zod parse at the boundary.
- Live views, not flatten-and-regenerate.
src/edit/*'s editor classes hold a reference directly into the realPackage/XmlElementobjects; saving isencodePackage(pkg), nothing more. This is what makes "everything you didn't touch stays byte-faithful" a structural guarantee rather than a best effort. - A three-tier PDF-read failure policy, applied consistently across every
src/pdf/*read module: throw a typedPdfParseError/PdfEncryptedErrorfor a file that cannot be meaningfully processed at all; recover with aPdfDiagnostic(severity: 'warning') for something malformed but salvageable (a badstartxref, a wrong stream/Length); degrade with a diagnostic for an individual unsupported feature (an unimplemented filter, an unrecognised colour space) while the rest of the document still reads. - Conventional commits, enforced via commitlint + husky, matching
ooxml.js.
ooxml.js's typed readers (readDocx/readPptx) are now the actual basis for conversion —readDocxContent/readPptxContentare thin wrappers around them, not an independent walk ofword/document.xml/ppt/slides/slideN.xml. They are still deliberately not re-exported from this package's own public surface:readDocx/readPptxalso carrycomments/footnotes/headers/footers(docx) thatContentDocumentdoesn't model, so exposing both the wrapper and the thing it wraps would invite a caller to reach for the wrong one rather than genuinely offering two competing models.- ODF paragraph/heading text content is not a plain string the way a docx run's
w:tis, and reading it wrong fails silently. Real whitespace collapses HTML-style when an ODF consumer renders XML text-node content, so the format represents a run of two or more literal spaces as<text:s text:c="N"/>(an ELEMENT, not a text node), a tab as<text:tab/>, and a hard line break as<text:line-break/>— all three occupy real character positions in a paragraph's flat content model but carry no text-node value at all. Every ODF text getter in this codebase's editor layer (src/edit/odt/*,src/edit/odp/*,src/edit/ods/*) MUST calldecodeOdfText(src/xml/odf-text.ts) — neverooxml.js's owntextContent(), a plain text-node concatenation with no ideatext:s/text:tab/text:line-breakexist.textContent()would silently DROP every one of them: the file still parses as valid XML, so this produces no error and no warning, just silently shorter text.decodeOdfTextdelegates the real work entirely toodf.js's owndecodeOdfText(wrapped in a synthetic container element, sinceodf.js's version operates on a wholeXmlElement's children rather than a bare node array); the encode direction,encodeOdfText(plain string → the same element sequence, coalescing adjacent literal characters into as few text nodes as practical), is local to this package, sinceodf.jsis a read-and-manifest package with no write-side text builder of its own. - The docx⇄PDF and pptx⇄PDF conversions are explicitly not round-trip-lossless — in deliberate contrast to
ooxml.js's ownpackageCodec, which is byte/part-faithful by design. See Fidelity. The six cross-format bridges below (odtToDocx/docxToOdt,odpToPptx/pptxToOdp,odsToXlsx/xlsxToOds) are a genuinely different case — see the Fidelity section's own paragraph on them. - A
DocumentPackagereturned viaonDocument/ConversionResult.packageis a snapshot from that one conversion pass, not a live view — itslayoutcorrelates with itscontentonly as of the exact read+layout that produced it (document-content-model's ownDocumentPackageSchemadoc comment), so if a caller mutates the returnedcontentafterwards, thelayoutsitting alongside it silently goes stale; nothing in this package (ordocument-content-model) detects or rejects that. - Building the six cross-format bridges surfaced two real, previously-undiscovered gaps in existing
populateParagraphwrite paths, both now fixed.buildDocxPackage'spopulateParagraph(src/edit/docx/content.ts) never wrote a paragraph's ownlistmembership back (ContentParagraph.list, docx's flatnumId/levelmodel) — only read, never written, since no existing caller had ever round-tripped a list-bearing paragraph through it.buildOdtPackage'spopulateParagraph(src/edit/odt/content.ts) never wrote a paragraph's ownstyleIdback at all (readOdtContent/readOdfParagraphinodf.jsreads it unconditionally fromtext:style-name, but nothing on the write side ever set that attribute). Both are now fixed:DocxParagraph.listis set unconditionally alongsidestyleId/alignment, matching that function's own existing pattern;OdtParagraph.styleIdis set conditionally alongsidealignment, matching odt's own local convention.buildOdtPackageadditionally gainedappendBlocks/appendListRun(src/edit/odt/content.ts) — ODF has no flat per-paragraph list property to set the way docx does, so a run of consecutiveContentParagraphs sharinglist.numIdis grouped and written as a real, potentially multi-leveltext:list/text:list-itemtree viaOdtList/OdtListItem, the structural inverse ofodf.js's own list-reading (a freshtext:listpernumIdchange, one level of nesting perlist.levelstep, descending only one level at a time since ODF can only open a nested list from inside an existing item). Both gaps were invisible before this task specifically because nothing had previously round-tripped a list-bearing paragraph or a styled paragraph throughdocx ⇄ odtat all — the PDF-pivot conversions never exercisedbuildDocxPackage/buildOdtPackageon content read back from the OTHER format. - A table shape inside an odp slide does not survive
odpToPptx.buildPptxPackage'sappendShape(src/edit/pptx/content.ts) silently drops any non-paragraph block found inside a shape's own text-box loop — a scope choice whose own comment ("PDF-reconstructed shapes never mix kinds") assumed its only caller was the PDF-reconstruction path, where that is true.odpToPptxis a second, non-PDF-reconstructed caller for which it is not: a real odpdraw:framecontaining atable:tabledirectly (not inside a text box) reads as aContentShapewith a'table'block, and that block is silently dropped, leaving an empty pptx text box where the table was. Everything else on the same slide — a rotated shape, grouped shapes, an image, speaker notes — survives correctly (seesrc/convert/bridges.test.ts's own dedicated fidelity-gap test, which proves both halves against the existingminimalOdpBytes()fixture). A real, tracked, bounded gap, not a silent one: closing it means teachingbuildPptxPackage/buildOdpPackageto write a real table into a slide shape, a materially larger feature than this bridge's own scope. - The
ods ⇄ xlsxbridge inherits several real, format-boundary fidelity limits fromooxml.js's brand-newreadXlsxContent/buildXlsxPackage, on top of its own pivot-copy design. xlsx has nopercentage/currencycell type of its own (both are a plain numeric cell plus a number-format style neither this reader nor this writer interprets) — an odspercentage/currencycell survives theodsToXlsxhop with its numeric value intact but downgrades to a plainnumberkind, permanently (currency's own currency code is dropped outright). xlsx also has only one raret="d"cell type covering BOTH date and time — an odstimecell survives as adate-kind cell carrying its original value string verbatim, but mislabelled; an odsdatecell is unaffected (it was already the kind xlsx's ownt="d"maps onto). A formula (table:formula/<f>) is carried completely verbatim in both directions — never parsed, translated, or evaluated by either this package's own reader or writer — but a REAL spreadsheet application does evaluate a workbook's own<f>/table:formulaon open: confirmed against genuine LibreOffice 26.2, an ods formula authored in OpenFormula syntax (of:=[.B2]*2) becomes a formula ERROR (Err:510) when the bridged xlsx is opened in real Calc, even though the formula's own cached value is still present and correctly readable viareadXlsxContent— going the other way is less fragile in practice only because a genuine xlsx formula (bare Excel A1 syntax, e.g.B2*2) happens to still parse under LibreOffice's own more lenient, backward-compatible ODF formula grammar, not because of anything this bridge does differently in either direction. Column widths survive theodsToXlsxhop within roughly a pixel of rounding tolerance (seesrc/convert/bridges.test.ts's ownCOLUMN_WIDTH_TOLERANCE_PT) but are then dropped entirely on the returnxlsxToOdshop — not a character-width-unit rounding loss, butbuildOdsPackagenot writingContentSheetColumn.widthPtat all, a pre-existing, already-documented gap in that file's own module comment, unrelated to and unfixed by this bridge. A boolean cell written bybuildXlsxPackagerenders as a raw1/0rather thanTRUE/FALSEwhen opened in real Excel/Calc, since that writer's own genuinely-minimalxl/styles.xml(one default cell format, no boolean-specific number format) has nothing else to apply — the underlying{ kind: 'boolean', value: true }is still read back correctly byreadXlsxContentregardless; this is a real-application display gap, not a data-fidelity one.readXlsxContent's own cell.value.kind never produces'error'from an odf.js-sourced document at all, for a structural reason rather than a bug: ODF'soffice:value-typeenumeration has noerrormember, soOdsCell.value's own write-side choice for akind: 'error'cell is to write it as a genuine, non-emptyoffice:string-valuecarrying the error's own text — anxlsxToOds→odsToXlsxround trip of a genuine xlsxt="e"error cell therefore turns it into a plainstringcell carrying the identical text; the message survives, theerrorsemantic does not. odpToPdf/pdfToOdpneeded zero new layout code.readOdpContent(src/odf/odp/read.ts) produces the identicalpresentationContentDocumentshapereadPptxContentdoes, so it feedsconvertPresentationToLayoutunmodified — including the existing hidden-annotation speaker-notes mechanism below, which carries odp'spresentation:notesthrough to the PDF with no new notes-handling code at all;pdfToOdpreusesreconstructPresentationunmodified too, the same architectural betpdfToOdtalready proved forreconstructWordprocessing. The genuinely new work for the reverse direction was the live-view editor itself (src/edit/odp/*) — see Architecture above.OdpShape.rotationDegwrites a realdraw:transform, built onodf.js's own transform machinery. It is the write-side inverse ofodf.js'sresolveOdfShapeGeometry(typed/shared/transform.ts), built on that module's own exportedapplyOdfTransformrather than a hand-rolled rotation matrix, so it inherits that module's own empirically-verified rotate/translate composition order and sign convention by construction. UnlikePptxShape(see thecolSpan/rowSpangotcha below, which pptx still has and odp does not),buildOdpPackagewrites a rotated shape's rotation back correctly — verified both by this package's own tests and by opening a fresh, editor-built.odpin actual LibreOffice.src/pdf/interpret.tstracks general vector paths, not just axis-alignedrerectangles.m/l/c/v/y/h(andreitself, per its own ISO 32000-1 definition as a 4-point rectangle subpath) accumulate real subpaths — CTM-transformed line/cubic segments, open or closed — and any paint operator (f/F/f*/S/s/B/B*/b/b*) emits aLayoutPathitem when the path isn't reducible to the simple single-re-on-an-axis-aligned-CTM case (which still takes the original, unchangedLayoutRectfast path). Verified both by dedicated tests and by a genuinewritePath→writePdf→readPdfround trip recovering the originalLayoutPathvalue exactly. This is the shared infrastructure bothpdfToOdsandreconstructDrawingneed; both now use it. A direct, practical consequence forpdfToOds:readPdfnever reconstructs a'line'kind item at all (see thereconstructDrawinggotcha below), so a gridline written bysheets.ts's ownrenderGridlinesalways comes back from a real PDF round trip as a generic, single-subpath, single-line-segment, stroke-onlyLayoutPath—reconstructSpreadsheet's own gridline-lattice detection accepts both shapes (a genuineLayoutLineitem and this stroked-single-segmentLayoutPathshape) for exactly this reason.pdfToOdsrecovers what was printed, not what was entered.reconstructSpreadsheet(src/layout/reconstruct.ts) tries a real gridline lattice first: it scans the page'sLayoutLine/stroked-single-segment-LayoutPathitems (see theinterpret.tsgotcha above) for enough parallel horizontal and vertical lines at consistent positions to call it a printed grid (MIN_GRIDLINE_COUNT_PER_AXIS = 3per 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 reuseclusterIntoLinesverbatim (a spreadsheet cell's own text is never wrapped across lines, so a text line already IS a row), and columns generaliseclusterIntoParagraphs's own singledominantLeftXto several recurring x-position anchors, first merging directly-adjacent same-line fragments (splitLineByLargeGaps, the same >2em-gap signalreconstructPresentation's own block clustering uses) so a cell whose text arrived as several run-level-splitLayoutTextitems 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 is a bare string carrying only its own extracteddisplayText— never re-parsed into a number/date/boolean, and never claimed as a formula, even when the text looks numeric or date-shaped; see Fidelity for the full framing.buildOdsPackage(src/edit/ods/content.ts) ispdfToOds's own package-building half, mirroringbuildOdtPackage/buildOdpPackage/buildOdgPackage's role forpdfToOdt/pdfToOdp/pdfToOdg.buildOdsPackagenow writesprintSettingsfor real, via a newOdsSheet.printSettingsgetter/setter (src/edit/ods/print-settings.ts) — discovered as a genuine blocker while buildingpdfToOds's own round-trip verification, not a pre-planned feature.OdsEditor/OdsSheetpreviously had no width/height/print-settings API at all, sobuildOdsPackagesilently droppedContentSheetPrintSettingsentirely; that made a reconstructed sheet's own recoveredgridlines/headers/pageSizeunverifiable by any real write-then-reread round trip, which is exactly whatpdfToOds's own test needed to prove. The setter mints a freshstyle:page-layout(styles.xml/office:automatic-styles) +style:master-page(styles.xml/office:master-styles) +style:style[family="table"](content.xml/office:automatic-styles) triple and repoints the sheet's owntable:style-nameto it on every call, rather than mutating whatever it was pointing at before — the same append-only style-editing conventionsrc/edit/odg/style.tsalready documents. Scoped to the five fieldsContentSheetPrintSettingsSchemaalways carries (pageSize/margins/gridlines/headers/pageOrder);printRange/scale/fitToPages/repeatRows/repeatColumns/manualBreaks(all optional, and never set byreconstructSpreadsheet) are still not read or written — resolving them needs the same table-wide repeated-column/row cursor trackingodf.js's ownreadTabledoes before ever calling its ownreadPrintSettings, a genuinely separate, larger undertaking than this getter/setter's own scope.OdsSheet/OdsEditorstill have no column-width or row-height setter at all — discovered the same way as theprintSettingsgap above, while buildingpdfToOds's own smoke-test coverage.OdsSheet.cell()'s own column/row-materialisation (address.ts) creates a real, explicittable:table-column/table:table-rowelement for any position a caller ever addresses, but never gives it a width/height style. This is a genuinely different failure shape from a column/row with NO element at all:sheets.ts's ownresolveAxisonly falls back toDEFAULT_COLUMN_WIDTH_PT/DEFAULT_ROW_HEIGHT_PTfor an index with noContentSheetColumn/ContentSheetRowentry whatsoever — an explicit-but-unstyled element reads back atwidthPt/heightPt0 (odf.js's ownresolveColumnWidthPt/readRowLayout), and that explicit zero wins over the fallback. A sheet built purely throughcreateOds()/OdsSheet.cell()therefore renders with a zero-size grid — real content needs an explicit column-width/row-height style, exactly as a real LibreOffice-authored file always has one (seesrc/test-support/ods.ts's own fixtures, which set one deliberately).buildOdsPackage(src/edit/ods/content.ts) documents this as a tracked, bounded gap alongsideContentSheetImage/embeddedObjects, mirroringbuildOdtPackage's own identical image/colSpan-write gaps.reconstructDrawingmaps recovered geometry back onto ODF shapes near-1:1, with no clustering — but PDF's own content-stream operators still force severalContentVectorkinds to collapse to a genericpathon the way through. Every paintedLayoutItemmaps onto aContentVector/ContentShapedirectly, in the exact z-order it was recovered —LayoutRect→rect,LayoutEllipse→ellipse,LayoutLine→line,LayoutPath→path,LayoutText/LayoutImage→ContentShape— a fundamentally more tractable problem thanreconstructWordprocessing/reconstructPresentation's own paragraph/shape geometry clustering, since a drawing has no semantic structure to infer at all. The catch is upstream ofreconstructDrawingitself, in whatreadPdfcan even hand it:src/pdf/interpret.ts'sLayoutRectfast path only fires for a fill-only rectangle under a non-rotated CTM (see theinterpret.tsgotcha above),writeEllipsealways emits an ellipse as four cubic Beziers with no PDF-level marker that it started life as an ellipse, andreadPdfnever reconstructs a'line'kind item at all — so a stroked-and-filled rect, any ellipse, and any line each come back from a PDF as a genericLayoutPath, andreconstructDrawingcorrectly maps that to aContentVector'path', not the shape's original kind. Position, size, and fill/stroke colour still survive (within ordinary floating-point/string-formatting tolerance); only the vector's own discriminant kind narrows to whatever PDF's content-stream operators actually distinguish. Apathvector's own reconstructedframeis 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: aviewBox/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:reconstructDrawingmaps each recoveredLayoutTextitem to its own separateContentShape(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) ispdfToOdg's own package-building half, mirroringbuildOdtPackage/buildOdpPackage's role forpdfToOdt/pdfToOdp.- 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 thatreconstructDrawing's own real-file verification exposed, not something the reconstruction algorithm itself introduced, and both apply to every.odgthis package writes, not only a reconstructed one: (1)src/edit/odg/style.ts'sgraphicPropertyAttrswrotedraw:fill-coloralone, with no accompanyingdraw:fill="solid"— real LibreOffice 26.2 fills adraw:rect/draw:ellipsethat way fine, but silently renders adraw:pathwith 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(src/pdf/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), butreadPdf's own general path tracking only marks a subpathclosed: truewhen it actually sees an explicith, so a PDF-round-tripped ellipse came back withclosed: false, which correctly-behaving ODF/SVG consumers then refuse to fill even withdraw:fill="solid"set.writeEllipsenow emitshbefore its paint operator, drawing no additional ink (the path was already geometrically closed) but recording that closure explicitly. - A vector primitive's own fill/stroke needed a self-contained graphic-family style writer, not
odf.js's ownStyleRegistry.'graphic'is a recognisedStyleFamilymember (odf.js'ssrc/styles/registry.ts), butStylePropertiesSchema/buildStylePropertyElements(properties.ts/serialize.ts) only ever model text/paragraph formatting and never emit astyle:graphic-propertieselement 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.tsis that writer: it still reusesodf.js's general append-only style-editing invariant (a setter always mints a freshstyle:styleand repointsdraw:style-name, never mutates an existing entry — verified by the sameassertAutomaticStylesOnlyAppendedhelperOdpEditor's own live-view fidelity test uses) andsrc/edit/odt/automatic-styles.ts'sensureAutomaticStyles/nextStyleName(the "find-or-createoffice: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. - A path vector's own
svg:dis cross-checked againstodf.js's real parser, not merely asserted to "look plausible".src/edit/odg/svg-path.ts'sbuildSvgPathDatais the write-side inverse ofodf.js'sparseOdfPathData;OdgPathVector.subpathsre-derives its value by reparsing the actual writtensvg:viewBox/svg:dthrough that exact function (plusparseOdfViewBox/buildOdfSubpaths) on every read, rather than echoing back whateverContentSubpath[]the caller originally passed toaddPath— so every read is itself a live round-trip proof, and this module's own test suite additionally feedsbuildSvgPathData's output straight intoparseOdfPathDatato confirm point-for-point recovery. - A newly added vector/shape's paint order is expressed purely as document order, with no
draw:z-indexever written. This matchesodf.js's own reader-side convention exactly (typed/draw/shapes.ts'spaintOrderKey: honour an explicitdraw:z-indexwhen present, otherwise fall back to document order — and real LibreOffice output never emits one, it reorders elements instead), soOdgPage.addRect/addEllipse/addLine/addPath/addTextBox/addImagesimply append todraw:page's own children in call order and nothing more is needed for a lateradd*call to paint in front of an earlier one. LayoutPathSchema(document-content-model) has no quadratic or elliptical-arc segment kind, deliberately — not a scope gap that happens to be unfilled.writePath(src/pdf/content-write.ts) therefore has no quadratic-to-cubic elevation and no SVG-arc-to-cubic endpoint-to-centre parameterization anywhere in it:odf.js's own real-LibreOffice-output-verifiedsvg:dparser (typed/shared/path.ts) recognisesS/s/Q/q/T/t/A/aas command letters (so its own token stream stays in sync) but produces no segment for any of them — real LibreOffice output for rectangles, ellipses, freeform curves, and basic custom-shape presets never emits a quadratic or an arc in the first place, onlyM/L/H/V/C/Z. Building unused quadratic/arc conversion code against a segment kind that can never occur would be speculative, not root-cause work.- A drawing page's
shapesandvectorspaint in two independently-ordered arrays, with no field recording their relative order.ContentDrawPageSchema(document-content-model) keeps text/image/table content (shapes) and vector primitives (vectors) as two separate arrays, each correctly paint-ordered on its own byodf.js's own reader (honouring a realdraw:z-indexwhen present, falling back to document order otherwise) — but there is no shared ordering field between the two arrays at all, a real, tracked gap in the shared schema, not somethingconvertDrawingToLayoutcan reconstruct after the fact.convertDrawingToLayoutresolves it with one fixed, documented choice: every vector paints before every shape (vectors are the common "diagram" content in a real.odg; shapes are far more often text labels layered on top of them than the reverse). A page that genuinely interleaves the two mid-stack will not paint in true document z-order until the schema itself grows a shared field.reconstructDrawingresolves the identical gap in reverse the same way: it buckets each recoveredLayoutItemintovectorsorshapesby kind while walking the page once in overall paint order, so each array keeps its own items' relative order — which reproduces aconvertDrawingToLayout-produced page's original paint order exactly (vectors-then-shapes, by construction), and is still the best either array's own shape is able to express for aLayoutDocumentfrom any other producer. - A vector primitive's own rotation is never read at all. None of
ContentVectorSchema's variants carry a rotation field, unlikeContentShapeSchema—readOdgContent's underlyingodf.jsreader deliberately discards adraw:rect/draw:ellipse/draw:custom-shape's own rotation, so it reads (andconvertDrawingToLayoutplaces) at its unrotated bounding frame. A real, tracked model limitation inherited fromodf.js, not something this package's own layout code introduces. ContentVector'spathvariant'sfillRuleis never populated by the reader — alwaysundefined, whichwritePathtreats as nonzero.odf.js'sreadDrawPathVectordoes not currently resolve an evenodd fill rule from real ODF output, so every path this pipeline reads paints with PDF's default nonzero winding rule.LayoutPathSchema/writePathfully supportfillRule: 'evenodd'regardless — a caller constructing aLayoutPath(or a futureContentVectorproducer) directly can still set it; it just never arrives viaodgToPdftoday.ContentSheetCellSchema(document-content-model) models no per-cell border or background, and no per-cell alignment override — unlikeContentTableCellSchema.background.sheets.ts's cell-background and cell-border z-order steps are consequently skipped entirely (no dead placeholder code), and cell text alignment always falls back to the value-kind default (numeric right, boolean/error centre, string left) since there is nothing to override it with. A tracked, documented gap, not a silent one.- Ordinary text in PDF output uses the standard 14 fonts only — no font embedding. Helvetica/Times-Roman are genuinely metric-compatible substitutes for Arial/Times New Roman, but Word's actual current defaults (Calibri, Aptos) are not, so line wrapping and pagination will drift slightly from what Word itself would produce. Expect a faithful visual approximation, not a line-identical reproduction. The one exception is MathML formula rendering (
odfToPdf, and formulas embedded inside odt/odp): those genuinely embed the real STIX Two Math font — see the CFF-embedding gotcha below for the exact scope of that embedding (the wholeCFFtable, not glyph-subsetted). - Justified paragraphs render left-aligned, not justified.
Alignment(document-content-model) has a real'justify'member, and it survives reading a docx/odt paragraph's own alignment correctly, butsrc/layout/shared.ts'salignmentOffsetPt— the one function every layout engine (engine.ts/slides.ts/sheets.ts) consults to position a line — has no branch for it at all, falling through to the same0offset'left'gets; there is no inter-word spacing stretch anywhere in this package's line-wrapping code either. A documented narrowing, not a silent approximation:alignmentOffsetPt's own comment states "not implemented for v1" explicitly. Every other alignment value (center/right) works correctly. - Reading arbitrary real-world PDFs is the single largest risk surface in this package, and the parser is honest about its design target: cleanly-generated output from mainstream producers (Word, PowerPoint, Chrome, LibreOffice, Acrobat), recovering from the malformations those producers and their downstream tooling actually create, and failing loudly and specifically on anything else — not matching a mature library's robustness against adversarial input.
- Encrypted PDFs are unsupported.
/Encryptpresent in the trailer throwsPdfEncryptedError, even for the common empty-user-password case. CCITTFaxDecode/JBIG2Decode/JPXDecodePDF images are unsupported (scanned-fax and JPEG2000 formats) — the image is skipped with a diagnostic, the rest of the page still reads. JPEG images (DCTDecode) pass through completely losslessly in both directions; PNG-sourced images go through a real, narrowly-scoped hand-written codec.- PDF → docx/pptx reconstruction has no table or vector-shape recovery. A PDF has no semantic table structure to recover — a wide horizontal gap on a line becomes a tab character, not a reconstructed grid. General vector paths, curves, gradients, and shadings are not recovered either. This is a genuinely different scope boundary from
pdfToOds's own grid recovery (see thepdfToOdsgotcha above): there, the grid itself IS the deliverable, so a real gridline lattice or text-position clustering builds one deliberately, within its own honestly-scoped limits (a bare string per cell, never a typed value). - Table cell
colSpan/rowSpanand pptx shape rotation are read from aContentDocumentbut not yet written back bybuildDocxPackage/buildOdtPackage/buildPptxPackage— a merged cell round-trips as an ordinary unmerged one, and a rotated pptx shape round-trips unrotated (buildOdpPackagedoes not share the rotation half of this gap — see theOdpShape.rotationDeggotcha above). Both are bounded, tracked gaps (the cell's own text content and the shape's own position are still correct), not silent ones. - docx headers/footers, live
PAGE/NUMPAGESfield substitution, and inline images are not read byreadDocxContent— a deliberate, tracked scope narrowing from the original design, not an oversight. - pptx speaker notes survive
pptxToPdf/pdfToPptx, but not through any real PDF feature. PDF has no native concept of hidden presenter notes, soconvertPresentationToLayoutcarriesContentSlide.notesas a hidden/Subtype /Textannotation on the page (the same construct Acrobat's own sticky-note tool uses, marked with theHiddenannotation flag so it never renders or prints), andreconstructPresentationreads it back via a/Tmarker that distinguishes this package's own notes annotation from a genuine third-party sticky note. This is a round-trip mechanism specific to this package's own writer/reader pair — a PDF produced by anything else will never carry it, and a PDF consumer other than this package's ownreadPdfwill never see it as anything but an invisible, empty sticky note. odmToPdfis the one conversion in this package that is not purely bytes-in/bytes-out. A.odm(ODF master document) never carries its own chapters' content — eachtext:sectionis a bare external reference (text:section-source'sxlink:href+text:filter-name) to a standalone.odtfile, confirmed against real, unmodified LibreOffice 26.2 output while buildingodf.js's ownreadOdm: a self-closingtext:section-sourcewith noxlink:show/xlink:type, no manifest entry for the linked part, and no chapter text anywhere in the master document's owncontent.xml. There is consequently no way forodmToPdfto read a chapter's content from the.odmbytes alone — it takes anoptions.resolveSubDocumentcallback, called once per section with that section's ownhref, to hand back the chapter's own.odtbytes. Every section left unresolved (no callback given, or the callback returnsundefinedfor thathref) is collected across the whole document before anything throws, and reported together in oneOdmUnresolvedSectionErrornaming every unresolvedhref— not just whichever section the read loop happened to reach first.odmToPdfis consequently not one of the twelve round-trip conversions or six bridges above, and is deliberately not wired into theDocumentConverterport either: that port'sconvert(request, options)contract is a fixed single-bytes-in/bytes-out shape, and widening it with a resolver parameter for this one format would leak an odm-specific concern into every other conversion's own request shape — a caller wantingodmToPdfbehind the port can wrap it in their own adapter.OdmSection.inlineContent(declared byodf.js's ownreadOdmfor schema-completeness, covering a producer that caches a chapter's content inline rather than only linking it) is handled too, via the samereadOdfParagraph/readOdfTableprimitivesodf.js's ownreadOdtcalls internally — but the installedodf.js1.10.0 never actually populates it for any real documentreadOdmwas tested against, so this branch is exercised only by a directly-constructedOdmSectionin this package's own test suite, not by any.odmfixture..odbnever gets PDF conversion, and never will. A.odb's own Reports are live SQL-backed layouts — rendering one faithfully means actually executing its query against a real database engine, categorically out of scope for a hand-written codec that never runs SQL.readOdbTables/odbToXlsx/odbToCsvextract table data, not the database's own forms/reports/queries (whose namesodf.js'sreadOdbInventorysurfaces, but never their content — seeodf.js's own implementation notes).- All three
.odbdecoder tiers are implemented: HSQLDB TEXT-script rows (MEMORY/TEXT tables, Tier 1), HSQLDB's own binary CACHED-table row-store rows (Tier 2), and a Firebird-backed embedded database's own gbak logical-backup format (Tier 3, see the dedicated Tier 3 entries below).readOdbTablesstill detects and names one further shape it does not implement, rather than silently returning wrong or empty tables: HSQLDB's own whole-script BINARY (hsqldb.script_format=1) and COMPRESSED (hsqldb.script_format=3) serialisation — each throwsOdbUnsupportedFormatErrorwith aformatfield naming exactly which. BINARY and COMPRESSED are a single, materially different, larger undertaking from CACHED-table row-store decoding, not a close sibling of it: both are the entire script's own DDL/DML statements re-encoded as a length-prefixed,COMMAND-tagged binary stream (confirmed by generating a realhsqldb.script_format=1file and reading its ownCOMMAND/SYSTEM_SCRIPTtokens directly), andhsqldb.script_format=3is that identical binary stream wrapped in ordinary zlibDEFLATE(RFC 1950, confirmed against a real generated file's own0x78 0x9cheader) — genuinely not a compressed TEXT script, despite an earlier, unverified assumption to the contrary;readOdbTables' ownclassifyScriptBytesdetects the real zlib header rather than gzip's, which a real HSQLDB-produced COMPRESSED file never carries. An external-only connection (no embedded engine at all — MySQL/PostgreSQL/JDBC/ODBC) is a second, permanent scope boundary, not a missing tier:readOdbTablesthrowsOdbNoEmbeddedDataSourceErrorrather than attempting anything network-facing. - The CACHED-table row-store decoder (Tier 2,
src/hsqldb/cache.ts/rowformat.ts) is scoped to the specific HSQLDB 1.8.x-branch on-disk layout LibreOffice's embedded driver actually ships, not "any HSQLDB version ever" — the same bounding principle the PDF codec applies to "mainstream producer output" rather than every PDF ever created. There is no ISO/ratified specification for this binary format at all (unlike ODF or OOXML); ground truth is the actual HSQLDB 1.8.0.10 engine source, decompiled from the realhsqldb.jarLibreOffice 26.2 bundles (Specification-Version: 1.8.0.10in that jar's ownMETA-INF/MANIFEST.MF— the exact engine version LibreOffice's embedded HSQLDB JDBC driver loads), cross-checked against a real database that exact jar produced: created, populated, and checkpointed viajava.sqldirectly against the bundled jar, then read back — as this decoder's own ground-truth oracle — by a second, independent Java program using the identical jar. Every field of every row of all four CACHED tables in the checked-in fixture (src/test-support/odb.ts'sembeddedHsqldbCachedOdbBytes) matched that oracle exactly;parseHsqldbPropertiesthrows for ahsqldb.compatible_versionoutside the1.7.x/1.8.xfamily rather than guessing at an unverified layout. A genuine attempt was also made to cross-check the same fixture against actual LibreOffice itself via a headless UNO Basic macro driving its own SDBC API — this task's own strictest verification bar — but headlesssofficemacro invocation hung indefinitely in this sandbox regardless of profile isolation, macro-security configuration, or a five-minute timeout budget, independently corroborated by a concurrent, unrelated agent's own headless-LibreOffice attempt stalling identically in the same session; the JDBC oracle above is a materially stronger substitute than a fallback of convenience, though, since LibreOffice's own SDBC-to-HSQLDB path is itself a thin wrapper around calling this exact same bundled jar's own JDBC driver methods. - The CACHED-table decoder supports only a single-index table (the primary key, or HSQLDB's own internal row-position index for a table with none declared) — a table also carrying an explicit
CREATE INDEXor aUNIQUEconstraint's own auto-generated index throws rather than mis-decoding.org.hsqldb.Table.getIndexRoots()'s ownSET TABLE ... INDEX'...'script line carries one root row-position token per table index, but aUNIQUEconstraint's own index has no matchingCREATE INDEXDDL statement at all (its name is aTable.getIndexCount()-only trait), so this decoder'sindexCountcannot be reliably recovered from the script's own DDL text alone;parseHsqldbIndexRootsthrows rather than guessing which root token belongs to which index. The row-store's own AVL tree is walked purely by following each row's persisted child positions, never by comparing key values, so this decoder never needed HSQLDB's own free-block list at all: a deleted row is unlinked from its table's tree before its space is ever added to that list, so a traversal rooted at the tree's current root only ever reaches genuinely live rows. - DATE/TIME/TIMESTAMP columns decoded from a CACHED table's binary row store are only correct when read in the same timezone the database was written in — a genuine, inherent limitation of HSQLDB 1.8's own on-disk format, not an implementation shortcut.
org.hsqldb.HsqlDateTimeresolves every date/time value through ajava.util.Calendarcarrying no explicitTimeZone(i.e. the writing JVM's own default), and the row store's own encoding is a bare epoch-millisecondlongwith no timezone or offset recorded anywhere alongside it — confirmed empirically: the checked-in fixture's ownDATEvalues straddle both GMT and BST, and decoding via UTC (rather than local-timezone)Datemethods recovers the wrong calendar day for every summer date.src/hsqldb/rowformat.tsreconstructs every DATE/TIME/TIMESTAMP value using the reading process's own local timezone — correct whenever a.odbis read on the same machine/region that created it, the overwhelmingly common case, but not guaranteed for one moved to a host in a materially different timezone.src/hsqldb/cache.test.tspinsprocess.env.TZto'Europe/London'for exactly this reason, matching the fixture's own real generation environment. - A BIGINT value decoded from a CACHED table loses precision beyond
Number.MAX_SAFE_INTEGER, the same class of limitation every'number'-kindContentCellValuein this package already has (DECIMAL/NUMERIC included).ContentCellValuehas no arbitrary-precision integer kind to offer instead;readHsqldbColumnValueconverts a decoded BIGINT through abigintand only casts to a JSnumberat the very end viaNumber(), matching howsrc/hsqldb/script.ts's own Tier 1 numeric-literal parsing already stores every SQL numeric value as a plain JS number. A real, documented format-boundary gap, not a silent guess: the checked-in fixture's own BIGINT test values are deliberately kept within the safe range so the test suite demonstrates clean, exact round-tripping rather than exercising this already-understood, orthogonal ceiling. .odbTier 3 (Firebird) is the single subsystem in this whole package with no ratified spec foundation at all — not ISO 32000-1 (PDF), not the OASIS ODF 1.3 RelaxNG schema, nothing. Firebird's own on-disk page format (ODS) has no public specification; the only ground truth is Firebird's own open-source engine implementation (the firebirdsql/firebird repository) and real fixtures generated and cross-verified by hand. Building this reader surfaced a genuine, load-bearing correction to the design plan it was built against, discovered only by extracting and hex-inspecting a real LibreOffice-generated fixture: a Firebird-embedded.odb's owndatabase/firebird.fbkpart is a gbak logical BACKUP stream, not a raw ODS page dump. LibreOffice's embedded-Firebird SDBC driver backs up the live database (via the identical mechanism the standalonegbakcommand-line tool uses) into the.odbpackage on save, and restores it into a throwaway temp.fdbfile only when a document is actually opened for live editing — confirmed directly from the backup stream's own embedded temp-file path attribute (att_backup_file), which names a.../lu*.tmp/firebird.fdbpath under LibreOffice's own temp directory, never the.odb's own location. This means the page-level reader (header page, Page Inventory Page, Pointer Page → Data Page chains, RLE-style record compression, MVCC back-pointer chains) the original design plan called for has no real file to ever operate on — no.odbthis reader was tested against, or could plausibly be tested against, ever contains one.src/firebird/is consequently a gbak-backup-format reader instead, built against the exact same "no ratified spec, read the engine's own source, verify against real fixtures" discipline, just aimed at a different (and, as it turns out, more tractable) real artifact:src/burp/burp.h/backup.epp/restore.epp/canonical.cpp/mvol.cppandsrc/common/xdr.cpp/src/common/classes/NoThrowTimeStamp.cppin the Firebird engine repository, cross-checked line-for-line against real fixture bytes throughout construction (several real off-by-one attribute-index errors and one real high/low-word ordering bug in the initial pass were caught exactly this way, not by inspection alone). One genuine, welcome simplification falls out of this finding for free: because gbak's own backup process ALREADY resolves table/column definitions from the live engine'sRDB$RELATIONS/RDB$RELATION_FIELDS/RDB$FIELDSsystem tables before writing anything,src/firebird/schema.tsnever bootstraps those system tables itself — a realrec_relation/rec_fieldrecord pair, already fully resolved, is simply there in the stream for every user table.- The exact Firebird gbak backup format version this reader targets, and how that was determined: format version 10, per a real fixture's own
att_backup_formatattribute — burp.h's own version-history comment identifies format 10 as "FB2.5 → FB3.0" output.readFirebirdBackupchecks this explicitly and throwsFirebirdBackupFormatErrornaming the actual version found for anything else, rather than guessing at a different version's own attribute/record shape. Two real, LibreOffice 26.2-generated fixtures (src/test-support/firebird.ts) both report this same format version and both setatt_backup_compress=true(gbak's own default, not something either fixture-generation session opted into) — a genuine surprise this reader's own construction caught only by testing against real bytes: the naive assumption that a.odb's own embedded backup would be uncompressed was wrong on the very first real file tested, andsrc/firebird/reader.ts'sreadCompressedPayload(a signed-run-length/"PackBits"-style codec, restated frombackup.epp's owncompress/restore.epp's owndecompress) exists specifically because of that correction. - Two genuine real fixtures back this reader's own tests, both generated via a headless LibreOffice 26.2 UNO automation session and never hand-edited afterward (
src/test-support/firebird.tsdocuments each in full): a richer one built in this task's own session (two tables, varied column types —INTEGER/VARCHAR/DOUBLE PRECISION/DATE/BOOLEAN/DECIMAL/NUMERIC— four and three rows respectively, including deliberateNULLs in every nullable column, an apostrophe-escaped string, and a zero value distinct fromNULL), and theExaDev/odf.jsrepository's own pre-existing fixture (two empty tables, no row data, a second independently-generated real data point proving the schema-only path). The richer fixture's own construction surfaced a genuine UNO API ordering requirement, not obvious from the API surface alone:getConnection()on a freshlycreateInstance()'dDatabaseContextentry fails withSQLException: No storage or URL was givenunless.DatabaseDocument.storeAsURL()is called FIRST to give the embedded engine real backing storage to connect to — setting.URLalone is not enough. - Cross-verified field-by-field against real LibreOffice itself, not merely against this reader's own output. A second headless UNO macro (
VerifyFirebirdFixture— seesrc/test-support/firebird.ts's own doc comment) reopens the saved fixture completely fresh from disk (a genuine new document load, not the same in-memory session that created it), reconnects viagetConnection, and runs a realSELECT * FROM <table> ORDER BY <pk>through LibreOffice's own SDBC API — every value it returned matched this reader's own decoded output exactly, row for row, field for field, across both tables. - Headless LibreOffice command-line macro dispatch (
soffice {file} {macro:///Library.Module.Name}) needed two real, non-obvious environment fixes to run at all in this sandbox, beyond the ones already documented for HSQLDB/odm fixture generation. (1) A prior session's forcefully-killedsofficeprocess leaves macOS's own native "reopen windows after a crash" alert showing on every subsequent launch — invisible in headless/--invisiblemode (no window to click), sosofficehangs indefinitely in-[NSAlert runModal]waiting for a response that can never arrive;defaults write org.libreoffice.script ApplePersistenceIgnoreState -bool true(plus removing~/Library/Saved Application State/org.libreoffice.script.savedState) disables it. (2)soffice "macro:///Library.Module.Name"with no document argument silently does nothing at all — persoffice --help's own usage text, the{file}argument is not optional ({file} {macro:///Library.Module.MacroName}); a session invoking a macro with no real work to do on a document still needs a real (even trivial) file argument for the macro to actually dispatch. - A Firebird gbak backup stream's own wire format mixes two genuinely different byte-level encodings, confirmed only by testing against real bytes, not solely from reading the engine's source. Every
rec_*/att_*tag-and-attribute structure is little-endian ("VAX order",isc_vax_integer), one length-prefix byte per value; a row's own field-value sequence (once any RLE compression is peeled off) is standard RFC 1832 XDR — big-endian, every value (even a nominally 16-bitSSHORT) widened to a 4-byte-aligned unit, opaque byte runs zero-padded to the next 4-byte boundary. A genuine 64-bit-integer word-order bug (high 32 bits transmitted first, not low-first asxdr_hyper's own in-memorytemp_longarray layout suggests on first reading) was caught exactly this way: aDECIMAL(10,2)column decoded to a nonsense value on the first real-fixture test run, not from a source-reading mistake that was obvious in advance. - STIX Two Math (the embedded formula font) is a CFF-flavoured OpenType font (an
OTTOsfnt wrapping aCFFtable), not TrueType/glyf — confirmed by inspecting the vendored font's own sfnt table directory whilesrc/pdf/math-font.tswas built; the design plan this feature was built against assumed glyf and asked to confirm which and handle accordingly. Genuine Type2-charstring glyph subsetting (re-encoding charstrings, rebuilding the CFFINDEXstructures with a renumbered, minimal glyph set) is a substantially larger undertaking than TrueType glyf/loca subsetting, and is out of scope for this pass: the entireCFFtable is embedded verbatim, unmodified, as a single/FontFile3/Subtype /CIDFontType0Cstream — a real, correct, working embedded font, just not glyph-subsetted. Everything else genuinely IS built from a targeted parse of only what's used:cmapresolves exactly the Unicode code points a document's formulas actually reference to glyph IDs, and the emitted/Wwidths array and ToUnicode CMap only ever cover those same glyph IDs, not the font's full ~5,500-glyph repertoire. A CID-keyed composite font built this way needs no/CIDToGIDMapat all (that key exists only for/CIDFontType2): per ISO 32000-1 9.7.4.2, a/CIDFontType0whose/FontFile3is a "bare" (non-CID-keyed) CFF program is read with CID treated as directly indexing the CFF's ownCharStringsINDEX by glyph order — i.e. CID == GID — exactly the numberingcmap-derived glyph IDs already use, so Identity-H text-showing needs no further remapping anywhere in the write path. - The OpenType
MATHtable'sMathVariantssubtable (stretchy glyph assembly — building a tall parenthesis or brace from reusable top/middle/bottom/extender pieces) is deliberately not parsed. A stretchy fence ((/)/[/]/{/}/|/‖) or a stretchy<mo>wrapping a tall construct (a large fraction, a tall matrix) always renders at its own base glyph's fixed size, not dynamically resized to its content's own height — a documented, honest fallback the task's own design plan explicitly allowed for when the full variants subtable proves too large a sub-scope. TheMathConstantssubtable (every fraction/radical/script-positioning constant this package's own layout engine actually uses) and theMathGlyphInfosubtable (italics correction, top-accent attachment) ARE both genuinely parsed in full — seesrc/pdf/math-table.ts. - A token element's (
mi/mn/mo/mtext) own box height comes from the font's nominal design ascent/descent (hhea's ownascender/descender), not a tight per-glyph ink bounding box.src/mathml/never parses glyph outlines (noglyf/CFF charstring geometry extraction anywhere in this package — see the CFF-embedding gotcha above), so every token run shares one uniform vertical extent regardless of which characters it actually contains. Accurate enough for box-model layout (spacing, baseline alignment, page placement) but not pixel-tight around an unusually tall or shallow glyph. - 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. mover/munder/munderovercentre an over/under-script geometrically over the wider of the two boxes, not at the base glyph's own font-declared accent-attachment point (MathTopAccentAttachment, which the embedded font'sMathGlyphInfosubtable DOES carry and this package DOES parse — see the CFF-embedding gotcha above — just not consumed here). Visually correct for the common case of a single-character base (geometric centre ≈ optical centre for a roughly symmetric glyph); measurably different only for a multi-character or asymmetric base under a genuineaccent="true"mark. A real, bounded simplification, not a data gap — the metric this would need is already being parsed for a different purpose.- Greek
mathvariantmapping covers the plain alphabet plus nabla (∇) and partial differential (∂), not the OpenType/Unicode Greek "symbol variant" set (epsilon/theta/kappa/phi/rho/pi symbol glyphs —ϵ/ϑ/ϰ/ϕ/ϱ/ϖstyled to e.g. bold). Latin letters, digits, and the two named symbols above are fully covered, generated directly from Unicode's ownUnicodeData.txt(seesrc/mathml/variant.ts's own generation note) rather than transcribed by hand. - Embedded-formula detection inside odt/odp is genuinely new work with no
odf.js-side equivalent (readDrawFrameContentdoesn't recognise adraw:object-bearingdraw:frameat all yet — see thesrc/odf/architecture entry above), and each format's own detection carries its own real, bounded scope narrowing. For odt (src/odf/odt/read.ts): only adraw:framethat is a direct child ofoffice:textis detected — a formula anchored inline inside a paragraph's own run content, or nested inside adraw:ggroup, is not. Detected formulas are appended to the end of the section's ownblocksarray, in the order their frames appear in the document, not interleaved at their true original position among the paragraphs/tablesodf.js's own reader already produced — true positional interleaving would need per-element block-count bookkeeping this adapter doesn't have (atext:list, for instance, unwraps into manyContentParagraphblocks from one raw XML element, so "one raw child = one block" doesn't hold in general). For odp (src/odf/odp/read.ts): only a top-leveldraw:frameon adraw:pagewith nodraw:gsibling at all is detected (a slide containing any group is skipped entirely for formula detection, to avoid mismatching a formula onto the wrong shape) — but where it IS detected, position is exact, not appended:odf.js's ownwalkDrawShapesalready produces exactly oneContentShapeper top-leveldraw:framein document order, so the Nth frame maps precisely ontoshapes[N]. ods embedded-formula detection is not implemented at all —odf.js'sreadOdshas no existing floating-drawing/anchor-resolution mechanism (ContentSheetImage/ContentSheet.embeddedObjectsare both already-known, pre-existing unpopulated gaps this task does not newly create — see theContentSheetCellSchemagotcha above for the sibling gap on the write side), so there is noreadDrawFrame-equivalent entry point to hook a formula-frame scan onto the way odt/odp have;src/layout/sheets.tsaccordingly has no formula-handling branch at all, with a comment marking why. - A formula that isn't rendered as real MathML (an odm chapter's own embedded formula, or any formula crossing the
odtToDocx/docxToOdt/odpToPptx/pptxToOdpbridges) survives only as its own plain-text placeholder — the formula's StarMath annotation if it had one, or the literal[formula]otherwise (seesrc/odf/formula/placeholder.ts).odmToPdf's own per-chapterreadOdtContentcall discards that chapter's ownformulasmap entirely (re-keying every formula'ssourcePathagainst the final combined document's own renumbered block indices is a materially larger undertaking than this task's own scope, and.odmhas no confirmed real-world test fixture to validate it against regardless — see theodmToPdfgotcha below).buildDocxPackagehas no MathML-writing path of its own (OOXML's own math markup, OMML, is a different vocabulary this package does not write), so the six cross-format bridges never consult a formula's real MathML either, even when bridging between two formats that both, individually, support real formula rendering elsewhere in this package. sourcePathtraces aLayoutItemback to theContentDocumentnode it came from, but only within one read+layout pass.ooxml.js'sreadDocx/readPptxstamp everyContentRun/ContentImageBlock/ContentTable/ContentShapewith a positional path (sections[0].blocks[2].runs[1],slides[1].shapes[3].blocks[0]);convertWordprocessingToLayout/convertPresentationToLayoutcopy that same string onto whicheverLayoutText/LayoutImage/LayoutLink/LayoutRectitem(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 backgroundLayoutRectis attributed to its containing table's ownsourcePath, sinceContentTableCellcarries none of its own. This is not an edit-tracking or incremental-relayout mechanism — the path is only valid against the exactContentDocument/Packageit 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.
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 — see the standard-14 font substitution gotcha above.
odf → PDF (odfToPdf), and a formula embedded inside odt/odp, 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. The honest limits: stretchy delimiters render at a fixed size rather than dynamically assembling to their content's own height (the MathVariants subtable isn't parsed), a token's own box height comes from the font's nominal ascent/descent rather than a tight per-glyph ink bound, 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 — see the Gotchas entries above for the exact boundary of each. 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.
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.
PDF → odg (reconstructDrawing) is a best-effort reconstruction too, but for the opposite reason: not because a drawing's structure is hard to infer, but because a drawing has no semantic structure to infer at all, so there is no clustering step to get right or wrong in the first place. Every recovered LayoutItem maps close to 1:1 onto a ContentVector/ContentShape, in the exact order it was painted. What is genuinely lossy is upstream of reconstructDrawing, in what a PDF's own content-stream operators can even preserve: position, size, and fill/stroke colour survive within ordinary floating-point tolerance regardless of vector kind, but a filled-and-stroked rect, any ellipse, and any line each come back as a generic path vector rather than their original kind, since PDF has no native rect/ellipse/line primitive beyond one narrow fast-path case — see the reconstructDrawing gotcha above for the exact boundary. The one place this genuinely reorganises content rather than just approximating it: a single wrapped multi-line text box comes back as several separate single-line text boxes, one per line PDF's own line-wrapper produced, since reconstructDrawing maps one LayoutText item to one shape with no clustering — the text survives, its original grouping into one box does not. Verified against real LibreOffice 26.2, not merely against this package's own reader: a richly-varied .odg (overlapping rects, a filled-and-stroked ellipse, a stroked line, a filled-and-stroked Bezier curve, a wrapped text label) round-tripped through odgToPdf then pdfToOdg opens as a valid drawing with correct position, colour, and z-order throughout, the curve genuinely curved rather than polygon-approximated, and only the vector-kind-narrowing and text-splitting above visibly distinguishing it from the source.
PDF → ods (pdfToOds, reconstructSpreadsheet) recovers what was printed, not what was entered. This is a harder, categorically different limit than any other reconstruction direction above, not merely a looser version of the same one: docx/pptx/odp/odg reconstruction can at least recover real text, formatting, and (for odg) exact vector geometry from a PDF's own positioned glyphs and paths. A spreadsheet cell's own value — a formula, a real typed number, a date serial, a currency code — never exists anywhere in a rendered PDF at all; a PDF only ever carries the rendered string Calc or Excel chose to print for that cell. reconstructSpreadsheet is honest about this rather than pretending otherwise: every recovered cell is a bare { kind: 'string', value: displayText }, never re-parsed back into a number/date/boolean, and never claimed as a formula, even when the recovered text looks numeric or date-shaped ("42.5", "2024-01-15") — see the pdfToOds gotcha above for the full detection algorithm (a real gridline lattice used directly as cell boundaries when a printed sheet had gridlines enabled, text-position clustering otherwise). Column widths, row heights, and page size are genuinely measured from whichever geometry was used; no print range, scale, fit-to-page, repeat-rows/columns, or manual breaks are ever inferred, since a rendered page carries no trace of print intent, only what was visually printed. Verified against real LibreOffice 26.2, not merely against this package's own reader: a genuine, gridline-and-headers-enabled four-column, four-row employee-record .ods (mixed string/date/boolean-looking cell content) round-tripped through odsToPdf then pdfToOds opens as a valid spreadsheet with every cell's text recovered in its correct row/column position, via the gridline-lattice path specifically (confirmed by inspecting the recovered printSettings.gridlines), and every recovered cell an honest string.
Neither direction is round-trip-lossless, and no conversion is the exact inverse of its own reverse direction — pdfToDocx(docxToPdf(x)) will not reproduce x exactly, and neither will pdfToOdg(odgToPdf(x)) or pdfToOds(odsToPdf(x)); neither is intended to. This is a deliberate, permanent contrast with ooxml.js's own packageCodec, which genuinely is a lossless round trip. docxPdfCodec/pptxPdfCodec/odtPdfCodec/odpPdfCodec/odsPdfCodec/odgPdfCodec/pdfCodec share packageCodec's mechanism (z.codec(), schema-validated both ways) but not its guarantee — wrapping a lossy conversion in z.codec() validates the shape of what comes out, not its fidelity to what went in.
The six cross-format bridges (odtToDocx/docxToOdt, odpToPptx/pptxToOdp, odsToXlsx/xlsxToOds) are a categorically different case from every conversion above: they bypass the PDF pivot entirely, so the "not round-trip-lossless" caveat that applies to every PDF-pivot conversion in this section does not carry over to them. There is no layout engine (no flow, no line-wrapping, no pagination) and no geometry-based reconstruction (no baseline clustering, no gridline-lattice detection) anywhere in a bridge's own call path — each is nothing more than buildYPackage(readXContent(decodePackage(bytes))), composing the identical reader/builder pair the PDF-pivot conversions on either side of the bridge already use, because both formats in each pair read into and build from the exact same ContentDocument variant. Concretely, for odt ⇄ docx and odp ⇄ pptx: text, run styling (bold/italic/underline/colour/font/size), paragraph styleId, list membership and nesting level, table structure and cell content, and (for odp ⇄ pptx) speaker notes all survive completely — proven by src/convert/bridges.test.ts's own dedicated round-trip suite, exercised in both directions from both starting formats, and cross-checked by opening genuinely LibreOffice-produced source files and their bridged output in real LibreOffice (see that test file and this repo's own verification notes). The one confirmed gap is a table shape nested inside an odp/pptx slide shape, not the document/presentation structure itself — see the gotcha above. ods ⇄ xlsx preserves cell values, formulas (verbatim), and merged ranges completely, and column widths within a roughly one-pixel rounding tolerance on the odsToXlsx hop — but, being built on ooxml.js's brand-new readXlsxContent/buildXlsxPackage, carries several real, honestly-documented format-boundary limits of its own (percentage/currency downgrading to a plain number, time collapsing into date, xlsx column widths not surviving the xlsxToOds return hop, and a formula written in one dialect showing as a genuine formula error in a REAL spreadsheet application expecting the other) — see the ods ⇄ xlsx gotcha above for the full, specific list. None of this is layout drift or reconstruction guesswork; every gap listed is a genuine format-boundary limit (a cell type, a value kind, or a write-side omission that exists independently of this bridge), not an approximation introduced by the bridge itself.
.odb table extraction (readOdbTables, all three tiers) is a genuine, verified data extraction, not an approximation — but it recovers only what a .odb's own embedded database storage actually carries, which differs by tier. Tier 1 (HSQLDB TEXT script) parses real DDL/DML text, so a table's own declared column types survive as the literal SQL clause they were declared with, and row values are the literal INSERT statement literals. Tier 2 (HSQLDB CACHED-table binary row store) shares Tier 1's own DDL-derived column types — a CACHED table's DDL still lives in database/script as ordinary TEXT — but decodes its actual row values from a separate binary page-cache file, database/data, cross-verified field-by-field against a real HSQLDB JDBC oracle on the identical fixture (see the Gotchas entry above). Tier 3 (Firebird) decodes a real gbak backup stream — every cell value, NULL, and column name is genuinely read from the file, cross-verified field-by-field against real LibreOffice's own SDBC query on the identical fixture (see the Gotchas entry above for the full verification transcript) — but a column's own HsqldbColumn.type label is synthesised from the field's binary metadata (BLR type + length + scale), not lifted from source SQL text the way Tier 1/2's is, since a gbak backup carries no DDL text at all. No tier recovers a database's own forms, reports, or queries (names only, never content — see the gotcha above), and none has a reverse (xlsx/CSV → .odb) direction. Tier 3 additionally has three real, bounded, honestly-scoped gaps, all documented in code comments at the exact spot each applies: no BLOB column content (the blob's own reference is consumed for stream alignment, but its column value is always recorded empty); no FB4+-only types (INT128/DECFLOAT, i.e. a NUMERIC/DECIMAL column wider than 18 digits of precision — this reader's own real fixtures are Firebird 3.0-era output, which has no such types to begin with); and a blob-VALUED metadata attribute (a relation/field/index/trigger's own description, default value, or BLR body) uses a different, compound wire encoding this reader's generic attribute-skip does not yet handle — never encountered by either real fixture this reader was verified against, but a real gap on a .odb whose tables carry comments or computed columns.
Optional real-world corpus. test/corpus/ (gitignored, never committed) holds a pnpm test:corpus vitest project for manual conformance checking against real PDFs a hand-built fixture can't fully stand in for — a Word "Save as PDF", a PowerPoint "Save as PDF", a Chrome "Print to PDF", a LibreOffice export. It is not part of pnpm test and does not gate CI; drop files in locally before a significant parser change.
.github/workflows/ci.yml runs commitlint, lint, typecheck, the unit suite, and the smoke test on every push and pull request. On a push to main where those all pass, release.config.ts drives semantic-release: commit history since the last tag decides the version bump, CHANGELOG.md and package.json are committed back to main, a GitHub Release is cut, and the package publishes to npmjs.org — via npm's OIDC trusted publishing, so no NPM_TOKEN exists anywhere in the pipeline.
Whether that release actually published a new version is detected by diffing package.json's version before and after the release step, not by trusting a third-party action's own detection. Two further jobs gate on that: one republishes the same build under the scoped @exadev/documents.js alias to GitHub Packages (which has no OIDC exchange of its own, so it authenticates with GITHUB_TOKEN instead), and one packs the release into its own directory, generates an SPDX SBOM (pnpm sbom), and signs both an SBOM and a build-provenance attestation against that exact tarball — verifiable independently of the registry, and still present if the package is later unpublished.
Commits follow Conventional Commits (feat:, fix:, test:, chore:, …), enforced by commitlint (commitlint.config.ts) via a husky commit-msg hook and a CI commitlint job — semantic-release's version bump depends on these being well-formed, not just style. A husky pre-commit hook runs lint-staged (eslint --fix on staged *.ts files) and pre-push runs the test suite. There is a single main branch and no open pull request workflow established so far.
- ooxml.js — the sibling package this depends on for all docx/pptx/xlsx ⇄ JSON handling and cascade-resolved typed reading, including its own
readXlsxContent/buildXlsxPackage(aContentDocument-shaped xlsx reader/writer pair), consumed directly bysrc/convert/convert.ts'sodsToXlsx/xlsxToOdsbridge but not re-exported from this package's own public surface. - document-content-model — the sibling package that owns
ContentDocument/LayoutDocumentthemselves; bothooxml.jsanddocuments.jsimport from it rather than each maintaining an independent copy. - odf.js — a sibling package doing the equivalent lossless-codec job for the OpenDocument Format (odt/ods/odp/odg/…), also built on
document-content-model. A dependency ofdocuments.jsfor: this package'sOdt/Ods/Odp/OdgBytesSchema(src/model/bytes.ts), which validate against itsODF_MEDIA_TYPEStable;src/interop.test.ts, a type-level guard thatooxml.js's andodf.js's rawXmlElement/XmlNode/Attribute/Packagecontainer types stay structurally compatible;src/odf/odt/read.ts'sreadOdtContent, a thin adapter overodf.js's ownreadOdt, feedingodtToPdf/pdfToOdt(src/convert/convert.ts);src/odf/odp/read.ts'sreadOdpContent, the same adapter overodf.js'sreadOdp, feedingodpToPdf/pdfToOdp;src/odf/ods/read.ts'sreadOdsContent, the same adapter overodf.js'sreadOds, feedingodsToPdf/pdfToOds, and reused directly bysrc/edit/ods/print-settings.ts's ownreadSheetPrintSettings(findStyleElement/resolvePageLayoutProperties/parsePageSize/parseMargins, the same style-chain-resolution primitivesreadOds's ownreadPrintSettingsis built on);src/odf/odg/read.ts'sreadOdgContent, the same adapter overodf.js'sreadOdg— including its owntyped/shared/path.ts, the real-LibreOffice-output-verifiedsvg:d/draw:pointsparser this package'swritePathcontent is ultimately sourced from, and whichsrc/edit/odg/svg-path.ts'sbuildSvgPathData(the write-side inverse) also cross-checks its own output against directly — feedingodgToPdf/pdfToOdg(the latter re-reading a rebuilt package's own real geometry through this samereadOdg, not just writing one);src/odf/formula/read.ts'sreadOdfFormulaContent/readOdfEmbeddedFormula, thin adapters overodf.js's ownreadOdfFormula, feedingodfToPdfand the odt/odp embedded-formula paths respectively;src/edit/odt/*'sStyleRegistry/resolveStyle(style interning),src/edit/odp/shape.ts'sapplyOdfTransform/resolveOdfShapeGeometry(rotation), andsrc/edit/odt/automatic-styles.ts'sensureAutomaticStyles/nextStyleName(reused bysrc/edit/odg/style.ts's own graphic-family style writer andsrc/edit/ods/print-settings.ts's own page-layout/master-page/table-style minting), all consumed directly rather than reimplemented. odt, odp, ods, and odg →ContentDocumentreading and PDF conversion are now all integrated both ways. - STIX Two Math — the embedded math font
src/pdf/math-font.tsparses andodfToPdf(and the odt/odp embedded-formula paths) render through, vendored atassets/fonts/STIXTwoMath-Regular.otfand embedded intodist/as a base64 string (src/mathml/assets/stix-two-math-font.ts, generated byscripts/generate-math-font-asset.mjs) rather than read from disk at runtime. Copyright 2001-2021 The STIX Fonts Project Authors, licensed OFL-1.1 — seeassets/fonts/NOTICE.mdfor the exact source commit and version this was vendored from. - firebirdsql/firebird — the ground truth
src/firebird/is built against, since Firebird's own gbak backup format has no ratified public specification:src/burp/burp.h(therec_type/att_typeenumerations and their own per-block numbering, and the backup-format version history),src/burp/backup.epp/restore.epp(the write/read reference implementationsrc/firebird/reader.ts's attribute framing and RLE decompression are restated from),src/burp/canonical.cpp(the per-SQL-type XDR shape a row's own field values use),src/burp/mvol.cpp(the backup-header attributes and their own presence-means-true encoding),src/common/xdr.cpp(the underlying big-endian XDR primitive encodings, including thexdr_hyperhigh-word-first ordering this reader's own construction initially got backwards),src/jrd/align.h/src/include/firebird/impl/blr.h(the BLR-type-opcode-to-physical-storage-type mapping), andsrc/common/classes/NoThrowTimeStamp.cpp(the DATE/TIME encoding algorithms). Not a dependency of this package at build or runtime — read and cited as source material only, per commit state at the timesrc/firebird/was built.
MIT