From 069ebef8fb548440917ec4dd638ee65b14a0e5a6 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 19 Aug 2026 07:11:11 +0100 Subject: [PATCH 1/2] docs: state why the public API has no DocumentPackage read/write pair Every other codec in this family reads its format into the flat ContentDocument, so each can layer a tree-native entry point on top of its own flat one -- decompose/assemblePackage outward, flattenPackage back, now that document-schema.js 4.3.0 exports all four. PDF cannot follow that pattern in either direction. A PDF states positions, not structure: readPdf yields layout cheaply because positioned glyphs and paths are all the format actually carries, and semantic content comes only from a separate, lossy reconstruction pass that infers paragraphs, headings, tables, and shapes back out of geometry. The write direction is asymmetric for the same reason -- a DocumentPackage reaches PDF bytes only through a font-measuring, line-breaking layout engine, and writePdf takes the positioned LayoutDocument that engine produces. Both passes are semantic policy rather than codec business and live in documents.js, which also means a wrapper here would invert the dependency onto the package that already depends on this one. Records that in the barrel header, next to the readPdf/writePdf exports a reader would otherwise expect a package-native pair beside, and in the README's usage section, naming convertDocument's onDocument callback and ConversionResult.package as where a caller gets a PDF as a tree. --- README.md | 2 ++ src/index.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ffca7d2..cbc7280 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ const bytes = writePdf(layout); `LayoutDocument` and its whole item family — every item/page/image-asset type and schema, plus `LAYOUT_FORMAT_VERSION` — are this package's own exports, ported from `document-schema.js` (which dropped them) so a codec's native model lives in the codec, the same family pattern as `ooxml.js`'s `Package`/`XmlElement` and `markdown-codec`'s AST. `readPdf`/`writePdf` keep their signatures; callers see the same names from a new home. `documents.js` re-exports the family onward from its own barrel — those re-exports now source from `pdf-codec` rather than `document-schema.js`, same names, new source. +There is deliberately **no `DocumentPackage`-returning read or `DocumentPackage`-accepting write here**, and `readPdf`/`writePdf` are this package's primary API precisely because of that. Every other codec in the family reads its format into `document-schema.js`'s flat `ContentDocument`, so each can offer a tree-native entry point on top of its own flat one — `decompose`/`assemblePackage` outward, `flattenPackage` back. PDF is the mirror image: it yields *layout* cheaply on read, because positioned glyphs and paths are all the format actually states, and semantic content only through a separate, expensive, lossy reconstruction pass that infers paragraphs, headings, tables, and shapes back out of geometry. That inference is semantic policy rather than codec business, so it lives in `documents.js` — a caller wanting a PDF as a `DocumentPackage` goes through `convertDocument` there and reads the tree off its `onDocument` callback or `ConversionResult.package`. The write direction is asymmetric for the same reason: turning a `DocumentPackage` into PDF bytes needs a full layout engine (font measurement, line breaking, page filling), which is also `documents.js`'s, and `writePdf` takes the already-positioned `LayoutDocument` that engine produces. Keeping both edges out of this package is what makes the item layer an honest record of what a file says, separate from what any consumer thinks it means. + An encrypted PDF that opens without a password decrypts transparently — no extra option, no password parameter; one that genuinely needs a user password throws `PdfPasswordRequiredError`. See [Gotchas](#gotchas-and-quirks) for exactly which encryption is supported. Both accept an optional `signal` (`AbortSignal`); `readPdf` additionally takes a `sink` (`PdfDiagnosticSink`, called once per recoverable parse diagnostic — see the three-tier failure policy under [Conventions](#conventions)), and `writePdf` an `onSubstitution` callback (called once per character not representable in a standard-14 font — see [Fidelity](#fidelity)). diff --git a/src/index.ts b/src/index.ts index 7d8f809..499f911 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // pdf-codec's public surface: a curated barrel export, no subpath exports, matching document-schema.js/odf.js/ooxml.js's own precedent. What's exported here is every symbol a real external consumer needs -- headline read/write/codec entry points, the Layout item family this package owns outright as its native document model (src/layout.ts, exported wholesale below), the formula/math port documents.js's own MathML layout engine passes real values through, the text-layout and font-resolution helpers every layout engine built on this codec needs, and the full bytes/image surface (this package owns src/bytes/ and src/image/ outright; nothing duplicates them upstream). Internal plumbing (objects.ts, serialize.ts, lexer.ts, parse.ts, xref.ts, document.ts, interpret.ts, content-read.ts, content-write.ts, filters.ts, predictors.ts, images-read.ts, cmap.ts, font-read.ts, font-style.ts, and the cmap-table/hmtx-table/font-tables/glyf/sfnt/sfnt-subset/cff/cff-probe/cff-bounds/tounicode/ot-layout-common/gpos-table/math-font-write/math-content-write/embedded-font-write font-parsing, font-subsetting, and font-embedding internals) stays unexported -- math-table.ts is a partial exception, exporting its MathVariants types alone (see below), and glyph-bounds.ts another, exporting the GlyphInkBounds shape those outline readers report through -- nothing outside this package's own src/ consumes it today. embedded-font.ts is the one partial exception: its EmbeddedFace is the type ResolvedFace's own 'embedded' variant carries, and its EmbeddedFaceSubstitution is what WritePdfOptions.onMissingGlyph reports, so both must be nameable by an external caller even though nothing else in that module is exported. -// Headline: read/write/diagnostics/codec. +// Headline: read/write/diagnostics/codec. readPdf/writePdf speak LayoutDocument and there is deliberately no DocumentPackage-native pair beside them: every other codec in this family reads its format into document-schema.js's flat ContentDocument and can therefore layer a tree-native entry point on top (decompose/assemblePackage outward, flattenPackage back), but PDF states positions, not structure -- it yields layout cheaply on read and semantic content only through a separate, lossy reconstruction pass, and it is written from an already-positioned layout a font-measuring, line-breaking engine produced. Both of those passes are semantic policy rather than codec business (ExaDev/pdf-codec#65) and live in documents.js, which is where a caller wanting PDF <-> DocumentPackage goes; adding a wrapper here would mean this package owning either a reconstruction heuristic or a layout engine, and would make it depend on the package that depends on it. export type { ReadPdfOptions } from './read'; export { readPdf } from './read'; export type { WritePdfOptions } from './write'; From 662df3ad6f356cf3c0256719989ab1c0fb199c7c Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 19 Aug 2026 07:23:29 +0100 Subject: [PATCH 2/2] docs: correct the DocumentPackage-boundary rationale for the public API The write-direction claim was wrong: turning a DocumentPackage into PDF bytes does not need a font-measuring, line-breaking layout engine. documents.js's buildDocumentBytes reaches PDF only for a package that already carries frames from a prior layout pass, and layoutDocumentFromPackage is a mechanical walk of those existing positions, not a layout pass of its own; a frameless package (a bridge conversion's own dump) cannot reach PDF at all. Restate the real asymmetry: read needs a lossy geometry-to-structure reconstruction, write needs frames a layout pass already stamped, and both the stamping and the walk-back live in documents.js. Also fix three adjacent inaccuracies in the same passage: convertDocument returns bytes only and never exposes ConversionResult.package, which is populated separately by createLocalDocumentConverter()'s DocumentConverter port; the ExaDev/pdf-codec#65 citation quoted only the reconstruction-stays- in-documents.js bullet while omitting that the frames-mapping half it scoped to this package was never implemented here; and "every other codec reads into ContentDocument" doesn't hold at the codec level -- ooxml.js's readDocx/readPptx and odf.js's readOdt/readOds return their own native models, and it's documents.js's own readXContent wrapper layer that projects each into the flat ContentDocument. --- README.md | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbc7280..b114275 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ const bytes = writePdf(layout); `LayoutDocument` and its whole item family — every item/page/image-asset type and schema, plus `LAYOUT_FORMAT_VERSION` — are this package's own exports, ported from `document-schema.js` (which dropped them) so a codec's native model lives in the codec, the same family pattern as `ooxml.js`'s `Package`/`XmlElement` and `markdown-codec`'s AST. `readPdf`/`writePdf` keep their signatures; callers see the same names from a new home. `documents.js` re-exports the family onward from its own barrel — those re-exports now source from `pdf-codec` rather than `document-schema.js`, same names, new source. -There is deliberately **no `DocumentPackage`-returning read or `DocumentPackage`-accepting write here**, and `readPdf`/`writePdf` are this package's primary API precisely because of that. Every other codec in the family reads its format into `document-schema.js`'s flat `ContentDocument`, so each can offer a tree-native entry point on top of its own flat one — `decompose`/`assemblePackage` outward, `flattenPackage` back. PDF is the mirror image: it yields *layout* cheaply on read, because positioned glyphs and paths are all the format actually states, and semantic content only through a separate, expensive, lossy reconstruction pass that infers paragraphs, headings, tables, and shapes back out of geometry. That inference is semantic policy rather than codec business, so it lives in `documents.js` — a caller wanting a PDF as a `DocumentPackage` goes through `convertDocument` there and reads the tree off its `onDocument` callback or `ConversionResult.package`. The write direction is asymmetric for the same reason: turning a `DocumentPackage` into PDF bytes needs a full layout engine (font measurement, line breaking, page filling), which is also `documents.js`'s, and `writePdf` takes the already-positioned `LayoutDocument` that engine produces. Keeping both edges out of this package is what makes the item layer an honest record of what a file says, separate from what any consumer thinks it means. +There is deliberately **no `DocumentPackage`-returning read or `DocumentPackage`-accepting write here**, and `readPdf`/`writePdf` are this package's primary API precisely because of that. Only `markdown-codec`'s own read entry point returns `document-schema.js`'s flat `ContentDocument` directly — `ooxml.js`'s `readDocx`/`readPptx` return `DocxDocument`/`PptxDocument` and `odf.js`'s `readOdt`/`readOds` return `OdtDocument`/`OdsDocument`, each codec's own native model, with only `ooxml.js`'s separate `readXlsxContent` producing a `ContentDocument` outright. What lets `documents.js` offer one tree-native entry point uniformly across those formats (`decompose`/`assemblePackage` outward, `flattenPackage` back) is its own `readXContent` wrapper layer (`readDocxContent`, `readOdtContent`, and siblings), projecting each codec's native model into the flat `ContentDocument` — a step the codecs themselves don't take. PDF has no such wrapper to project through: `readPdf` yields *layout* cheaply, because positioned glyphs and paths are all the format actually states, and semantic content only through a separate, expensive, lossy reconstruction pass that infers paragraphs, headings, tables, and shapes back out of geometry. That inference is semantic policy rather than codec business, so it lives in `documents.js` — a caller wanting a PDF as a `DocumentPackage` passes `onDocument` to a named conversion function or to `convertDocument` itself and reads the tree off that callback (`convertDocument` on its own returns only bytes), or uses `createLocalDocumentConverter()`'s `DocumentConverter` port, whose `ConversionResult.package` is populated by wiring that same callback internally. The write direction is asymmetric for a different reason than it might look: turning a `DocumentPackage` into PDF bytes is not itself a layout pass — `documents.js`'s `layoutDocumentFromPackage` is a mechanical inverse that walks the positions a *prior* layout pass already stamped onto the package's own content nodes as `frames`, and it only works at all when those frames exist (a bridge conversion's own dump, e.g. `odt-to-docx`, carries no `pages` and cannot reach PDF this way). The actual font-measuring, line-breaking engine runs earlier, wherever the package first passed through an X-to-PDF or PDF-to-X conversion — both the frame-stamping and the frame-walking are `documents.js`'s. Keeping both edges out of this package is still what makes the item layer an honest record of what a file says, separate from what any consumer thinks it means. An encrypted PDF that opens without a password decrypts transparently — no extra option, no password parameter; one that genuinely needs a user password throws `PdfPasswordRequiredError`. See [Gotchas](#gotchas-and-quirks) for exactly which encryption is supported. diff --git a/src/index.ts b/src/index.ts index 499f911..73ef1ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // pdf-codec's public surface: a curated barrel export, no subpath exports, matching document-schema.js/odf.js/ooxml.js's own precedent. What's exported here is every symbol a real external consumer needs -- headline read/write/codec entry points, the Layout item family this package owns outright as its native document model (src/layout.ts, exported wholesale below), the formula/math port documents.js's own MathML layout engine passes real values through, the text-layout and font-resolution helpers every layout engine built on this codec needs, and the full bytes/image surface (this package owns src/bytes/ and src/image/ outright; nothing duplicates them upstream). Internal plumbing (objects.ts, serialize.ts, lexer.ts, parse.ts, xref.ts, document.ts, interpret.ts, content-read.ts, content-write.ts, filters.ts, predictors.ts, images-read.ts, cmap.ts, font-read.ts, font-style.ts, and the cmap-table/hmtx-table/font-tables/glyf/sfnt/sfnt-subset/cff/cff-probe/cff-bounds/tounicode/ot-layout-common/gpos-table/math-font-write/math-content-write/embedded-font-write font-parsing, font-subsetting, and font-embedding internals) stays unexported -- math-table.ts is a partial exception, exporting its MathVariants types alone (see below), and glyph-bounds.ts another, exporting the GlyphInkBounds shape those outline readers report through -- nothing outside this package's own src/ consumes it today. embedded-font.ts is the one partial exception: its EmbeddedFace is the type ResolvedFace's own 'embedded' variant carries, and its EmbeddedFaceSubstitution is what WritePdfOptions.onMissingGlyph reports, so both must be nameable by an external caller even though nothing else in that module is exported. -// Headline: read/write/diagnostics/codec. readPdf/writePdf speak LayoutDocument and there is deliberately no DocumentPackage-native pair beside them: every other codec in this family reads its format into document-schema.js's flat ContentDocument and can therefore layer a tree-native entry point on top (decompose/assemblePackage outward, flattenPackage back), but PDF states positions, not structure -- it yields layout cheaply on read and semantic content only through a separate, lossy reconstruction pass, and it is written from an already-positioned layout a font-measuring, line-breaking engine produced. Both of those passes are semantic policy rather than codec business (ExaDev/pdf-codec#65) and live in documents.js, which is where a caller wanting PDF <-> DocumentPackage goes; adding a wrapper here would mean this package owning either a reconstruction heuristic or a layout engine, and would make it depend on the package that depends on it. +// Headline: read/write/diagnostics/codec. readPdf/writePdf speak LayoutDocument and there is deliberately no DocumentPackage-native pair beside them. On read, PDF states positions, not structure -- readPdf yields layout cheaply, and semantic content (paragraphs, headings, tables) only through a separate, lossy reconstruction pass that infers structure from geometry, genuine semantic policy that lives in documents.js (src/layout/reconstruct.ts). On write, a DocumentPackage reaches PDF bytes only once a prior layout pass has already stamped frames onto it (a package with no pages, e.g. a bridge conversion's own odt-to-docx dump, cannot reach PDF at all): documents.js's layoutDocumentFromPackage walks those already-stamped frames back into a LayoutDocument, a mechanical inverse rather than a font-measuring, line-breaking pass of its own -- the actual layout engine runs earlier, wherever the package first passed through an X-to-PDF or PDF-to-X conversion. ExaDev/pdf-codec#65 scoped the frames-mapping half of this boundary to this package ("readPdf/writePdf ... gain mappings to and from package frames at the edge"); that half was never implemented here and instead lives in documents.js as layoutDocumentFromPackage -- a deliberate reassignment, not an oversight, but one #65 itself doesn't record. Adding a DocumentPackage-facing wrapper here would mean this package owning either a reconstruction heuristic or a frame-walking pass that depends on a layout stage documents.js runs, and would make pdf-codec depend on the package that depends on it. export type { ReadPdfOptions } from './read'; export { readPdf } from './read'; export type { WritePdfOptions } from './write';