Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
3621659
docs: align core docs with pdfnative-mcp 1.6.0 (28 tools, PAdES ladde…
Nizoka Aug 25, 2026
76206c3
docs(site): 28-tool MCP surface on the landing page, playground, SVG …
Nizoka Aug 25, 2026
94e95c4
docs(site): regenerate the Open Graph card for 1.7.0 and fix the test…
Nizoka Aug 25, 2026
f6ff12f
docs: correct every remaining factual claim against pdfnative 1.7.0 g…
Nizoka Aug 25, 2026
103802d
docs: surface the 1.7.0 features the guide vitrines and agent files o…
Nizoka Aug 25, 2026
5f7165b
docs(site): pre-render every guide server-side and publish the machin…
Nizoka Aug 25, 2026
5c7e30f
docs(site): live demo with inline PDF preview, agent brief, answer-fi…
Nizoka Aug 25, 2026
dea3333
docs: executable recipes, api.json, two new guides, and a hardened ap…
Nizoka Aug 25, 2026
4010c8e
docs(site): inspect playground, JSON permalinks, live benchmark, anch…
Nizoka Aug 25, 2026
c153543
docs(site): honest comparison quiz and the engine diagnostic registry
Nizoka Aug 25, 2026
dde2ea0
docs(site): fix the three reported UI bugs and the blockers the revie…
Nizoka Aug 25, 2026
4fac3b7
docs(site): majors and minors from the triple review - responsive, a1…
Nizoka Aug 25, 2026
9d449e5
docs(site): replace the one uncovered emoji and make PDF/A visibility…
Nizoka Aug 25, 2026
c0afbeb
docs(site): sanitise data-md at the source (CodeQL js/xss-through-dom)
Nizoka Aug 25, 2026
302c14e
docs(site): derive the Markdown source from the page URL, not from DO…
Nizoka Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ fonts/ # Pre-built font data modules (.js/.d.ts) — 22 scripts + mat
tools/ # CLI tools: build-font-data.cjs (TTF → data module); build-emoji-font (bundled via tsup from scripts/build-emoji-font.ts → dist/tools/, npx pdfnative-build-emoji-font — generates colour-emoji data modules up to the full ~3,600-glyph set)
scripts/ # Modular sample PDF generation (48 generators; persian-showcase.ts + print-showcase.ts, signature-ltv.ts + incremental-metadata.ts added in v1.7.0; text-extract-showcase.ts added in v1.6.0; math-symbols.ts + svg-text-labels.ts + layout-debug-overlay.ts + annotations-showcase.ts + font-compiler-demo.ts added in v1.5.0; outline-bookmarks.ts + pdf-manipulation.ts added in v1.4.0; currency-symbols.ts + color-emoji-showcase real-world rewrite added in v1.3.0; signature-placeholder.ts, bidi-embeddings-showcase.ts, document-table-parity.ts, use-lite-showcase.ts added in v1.2.0/v1.3.0). scripts/lib/ holds the shared deterministic emoji-build core (emoji-font-core.ts, curated-emoji.ts, emoji-cli.ts) used by both build-color-emoji-data.ts and the bundled build-emoji-font CLI
test-output/extreme/ # Visual regression baselines for extreme scripts (extreme-bidi.pdf, extreme-tamil.pdf, extreme-bengali-devanagari.pdf, extreme-arabic-harakat.pdf, extreme-bidi-isolates.pdf)
tests/ # 2665+ tests (122 files: unit/integration/fuzz/parser/visual) mirroring src/ structure
tests/ # 2686+ tests (123 files: unit/integration/fuzz/parser/visual/docs) mirroring src/ structure
bench/ # Performance benchmarks (vitest bench)
docs/ # GitHub Pages landing site (pdfnative.dev) — pure HTML/CSS/JS, zero build deps
└── playgrounds/ # Interactive browser playgrounds (extreme-scripts.html, scale.html)
└── playgrounds/ # 9 interactive browser playgrounds (extreme-scripts, all-scripts, toolkit, charts, authoring-plus, scale, cli, mcp, react)
```

- **Single entry point**: `src/index.ts` re-exports everything. All public API surfaces live there.
Expand All @@ -103,7 +103,7 @@ docs/ # GitHub Pages landing site (pdfnative.dev) — pure HTML/CSS/

```bash
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
npm run test # vitest run (2665+ tests, 122 files)
npm run test # vitest run (2686+ tests, 123 files)
npm run test:watch # vitest (watch mode)
npm run test:coverage # vitest with v8 coverage (thresholds: 88/80/85/90)
npm run test:generate # Generate ~242 sample PDFs → test-output/ (incl. extreme/, emoji/, pdfa-latin/, forms/, charts/, parser/ baselines)
Expand All @@ -113,14 +113,14 @@ npm run typecheck:scripts # tsc --project tsconfig.scripts.json --noEmit
npm run typecheck:all # typecheck src/ + tests/ + scripts/
npm run lint # eslint src/ (ESLint 9 + typescript-eslint strict)
npm run validate:pdfa # veraPDF over every PDF/A-claiming sample (auto-detected; coverage canary vs declared.pdfaSamples; skips with exit 0 when veraPDF is absent, blocking in CI)
npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth)
npm run verify:docs # 22 offline rules over docs/, playgrounds, README, llms files (ecosystem.json is the source of truth)
```

- Build tool: **tsup** (dual ESM/CJS, tree-shakeable, sourcemaps)
- Test runner: **vitest** (fast, native ESM, watch mode, v8 coverage)
- CI: GitHub Actions — lint/typecheck/test/build on Node 22/24
- Publish: GitHub Actions OIDC Trusted Publishing (`npm publish --access public`; provenance is attached automatically via the workflow's `id-token: write`)
- All new code must have tests. 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90. 2665+ tests (122 files)
- All new code must have tests. 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90. 2686+ tests (123 files)

## Conventions

Expand Down Expand Up @@ -186,7 +186,7 @@ npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms
- Watermarks: ExtGState for transparency (`/ca opacity`), text rotation via `cos(θ) sin(θ) -sin(θ) cos(θ) cx cy Tm`, image centering with aspect ratio
- Watermark validation: PDF/A-1b blocks transparency (ISO 19005-1 §6.4) — `validateWatermark()` throws
- Watermark position: `'background'` = ops before content stream; `'foreground'` = ops after content stream
- Table of contents: `TocBlock` with multi-pass pagination (max 3 passes), `_renderToc()` with dot leaders, right-aligned page numbers
- Table of contents: `TocBlock` with multi-pass pagination (max 3 passes), `renderToc()` with dot leaders, right-aligned page numbers
- TOC internal links: named destinations `/Dests << /toc_h_N [pageObj /XYZ x y null] >>` in catalog; annotations use `/Dest /toc_h_N` (not `/URI`)
- TOC tagged mode: `/TOC` structure element with `/TOCI` children for PDF/UA compliance
- Smart tables (v1.2.0): `TableBlock` gains six optional fields — `wrap` (`'auto'`|`'always'`|`'never'`, default `'auto'`), `repeatHeader` (default `true`), `zebra` (`boolean|PdfColor`, default `false`, true uses `'0.969 0.973 0.984'`), `caption`, `minRowHeight` (default `12`), `cellPadding` (default `3`). Architecture: `planTable()` in `pdf-renderers.ts` measures once; `_paginateBlocks()` in `pdf-document.ts` slices at row boundaries into `TableSlice` items; `renderTable()` is page-lifecycle-free and accepts an optional `slice` arg. Tagged-mode `/Table` continues across slices via shared `tableStructAccum` array (ISO 14289-1 §7.10.6); `/Caption` emitted once. Single-page tables that fit without wrapping are byte-identical to v1.1.0 in their **body** rendering (header baseline `+4`, data baseline `+3`, `ROW_H=12`, `TH_H=15` preserved); right- and centre-aligned **header** glyph positioning shifts 2–5pt because v1.2.0 corrects a pre-1.2.0 width-measurement bug (see next bullet). `planTable()` and `TableSlice` are internal — NOT re-exported from `src/index.ts`.
Expand Down Expand Up @@ -256,7 +256,7 @@ npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms
- Latin VF (PDF/A): Noto Sans VF (OFL-1.1) bundled as `fonts/noto-sans-data.{js,d.ts}` under lang `'latin'`. Activates automatically for PDF/A documents containing non-WinAnsi Latin (curly quotes, em-dash, ellipsis…). Opt-in via `registerFont('latin', () => import('pdfnative/fonts/noto-sans-data.js'))`.
- Document outline/bookmarks (v1.4.0): `buildOutlineObjects(items, startObjNum, pageObjNumFor, defaultY, fmtNum, pageCount)` in `src/core/pdf-outline.ts` builds the `/Outlines` tree (`/First /Last /Next /Prev /Parent /Count`, nested children, `/F` flags bold=2 italic=1, `/C` color, `/Dest [pageObj /XYZ 0 y null]`). Titles via `encodePdfTextString`. `OutlineItem.open?: boolean` (default true) — `false` emits a **negative** `/Count` (collapsed, ISO 32000-1 §12.3.3); a collapsed node contributes only itself (not its hidden descendants) to ancestors' visible counts (`openDescendantCount` magnitude + sign by `open`). Wired into `pdf-document.ts` as **trailing indirect objects** appended after embedded files (`totalObjs = outlineStart + built.totalObjects - 1`) so the catalog-rewrite offset-adjustment loop covers them — same pattern as colour-emoji/embedded-files. `DocumentParams.outline?: readonly OutlineItem[] | 'auto'`; `'auto'` derives a nested tree from heading levels via `autoOutlineFromHeadings()`. Catalog gains `/Outlines N 0 R`.
- Page labels (v1.4.0): `buildPageLabelsDict(ranges, pageCount)` in `src/core/pdf-page-labels.ts` emits an inline `/PageLabels << /Nums [...] >>` number tree. `PageLabelStyle` decimal/roman/Roman/alpha/Alpha/none → `/S /D|r|R|a|A` (none omits `/S`). `PageLabelRange { startPage, style?, prefix?, start? }` — validated for ordering/bounds; prefix parens escaped. `DocumentParams.pageLabels?: readonly PageLabelRange[]`. Inline (not indirect) so no object-number impact.
- Page-tree manipulation (v1.4.0): `mergePdfs(sources, opts?)`, `splitPdf(src, ranges)`, `extractPages(src, indices)` in `src/parser/pdf-pagetree.ts` deep-copy kept pages + their transitive object graph into a fresh object-number space (obj 1=Catalog, 2=Pages, 3+=graph). `copyObject()` is memoized per-reader and cycle-safe; `rewrite()`/`copyObject()`/`serializeStreamBody()` thread a `depth` capped at `MAX_COPY_DEPTH=2000` (throws on deeper nesting/ref-chains — stack-overflow hardening). `resolveInherited()` folds MediaBox/CropBox/Rotate/Resources from ancestors onto each page; `filterAnnotations()` keeps only URI `/Link` annots; `serializeValue/Dict` are binary-safe (Latin-1). `serializeDocument()` emits a deterministic content-addressed trailer `/ID [<hex> <hex>]` via `md5(...)` imported from `core/pdf-encrypt.js` (parser→core is an allowed edge). `assertNotEncrypted()` throws on `/Encrypt`. `MAX_MERGE_SOURCES=50`. `MergeOptions { dropSignatures?, dropAnnotations?, maxOutputSize?, password?, encrypt? }` (`password` decrypts encrypted sources and `encrypt` re-encrypts the rebuilt output — both added in v1.6.0) — `maxOutputSize` (default `DEFAULT_MAX_OUTPUT_SIZE = 256*1024*1024`; `Infinity` disables) is a secure-by-default output ceiling enforced via `accountBytes()`/`setBody()` and a pre-flight `accountBytes(ctx, stream.data.length)` in `serializeStreamBody()` *before* the Latin-1 conversion (rejects multi-GB streams before they materialise — OOM hardening); validated at entry via `resolveMaxOutputSize()`. `splitPdf(src, ranges, opts?)` and `extractPages(src, indices, opts?)` also accept `MergeOptions` (additive trailing param). Full rebuild (not in-place surgery) — unblocks `pdfnative-mcp` merge_pdfs/split_pdf.
- Page-tree manipulation (v1.4.0): `mergePdfs(sources, opts?)`, `splitPdf(src, ranges)`, `extractPages(src, indices)` in `src/parser/pdf-pagetree.ts` deep-copy kept pages + their transitive object graph into a fresh object-number space (obj 1=Catalog, 2=Pages, 3+=graph). `copyObject()` is memoized per-reader and cycle-safe; `rewrite()`/`copyObject()`/`serializeStreamBody()` thread a `depth` capped at `MAX_COPY_DEPTH=2000` (throws on deeper nesting/ref-chains — stack-overflow hardening). `resolveInherited()` folds MediaBox/CropBox/Rotate/Resources from ancestors onto each page; `filterAnnotations()` keeps only URI `/Link` annots; `serializeValue/Dict` are binary-safe (Latin-1). `serializeDocument()` emits a deterministic content-addressed trailer `/ID [<hex> <hex>]` via `md5(...)` imported from `core/pdf-encrypt.js` (parser→core is an allowed edge). `MAX_MERGE_SOURCES=50`. `MergeOptions { dropSignatures?, dropAnnotations?, maxOutputSize?, password?, encrypt? }` (`password` decrypts encrypted sources and `encrypt` re-encrypts the rebuilt output — both added in v1.6.0) — `maxOutputSize` (default `DEFAULT_MAX_OUTPUT_SIZE = 256*1024*1024`; `Infinity` disables) is a secure-by-default output ceiling enforced via `accountBytes()`/`setBody()` and a pre-flight `accountBytes(ctx, stream.data.length)` in `serializeStreamBody()` *before* the Latin-1 conversion (rejects multi-GB streams before they materialise — OOM hardening); validated at entry via `resolveMaxOutputSize()`. `splitPdf(src, ranges, opts?)` and `extractPages(src, indices, opts?)` also accept `MergeOptions` (additive trailing param). Full rebuild (not in-place surgery) — unblocks `pdfnative-mcp` merge_pdfs/split_pdf.
- streamToFile (v1.4.0): `streamToFile(stream, filePath, { signal? })` in `src/core/pdf-stream-writer.ts` drains any `AsyncGenerator<Uint8Array>` to disk in Node, honouring write back-pressure (awaits `'drain'`) and `AbortSignal`; returns `{ bytesWritten, path }`. On abort (pre-start, mid-iter, or post-loop) or any write error it releases the fd (awaits `'close'`) and best-effort `fs.rmSync(filePath, { force: true })` so no orphaned partial file is left behind. Uses top-level `import type * as NodeFs from 'node:fs'` + dynamic `await import('node:fs')` (no static node dep — keeps browser/Deno builds clean). ESLint forbids inline `import()` type annotations, so the type-only import MUST be top-level.
- COLRv1 advanced compositing (v1.4.0): `colr-parser.ts` resolves `PaintSweepGradient` (format 8) → `SweepGradientPaint { kind:'sweep', center, startAngle, endAngle, stops, extend }` (matrix rotation folded via `Math.atan2`), and `PaintComposite` (format 32) → backdrop+source layers with the source tagged `ColorLayer.blendMode` via `compositeModeToBlendMode(mode)` (separable modes 3/13–27 → Normal/Multiply/Screen/Overlay/Darken/Lighten/ColorDodge/ColorBurn/HardLight/SoftLight/Difference/Exclusion/Hue/Saturation/Color/Luminosity; structural Porter-Duff modes → `null` → `UnsupportedPaint` → mono fallback). `pdf-color-glyph.ts`: `emitSweep()` renders flat triangular wedges (no `/Shading` resource); unified `gsFor(alpha, bm)` ExtGState helper combines `/ca`+`/BM`. New types `SweepGradientPaint` + `ColorLayer.blendMode?` in `src/types/pdf-types.ts`.
- Pluggable signature crypto provider (v1.4.0): `src/crypto/crypto-provider.ts` mirrors the `setDeflateImpl` global-registry pattern — module-level `_cryptoProvider`, `setCryptoProvider(provider | null)`, `getCryptoProvider()`. `CryptoProvider { sign(tbs, algorithm): Uint8Array }` receives the **DER-encoded CMS signed attributes** (`signedAttrsForSig`, a SET tag 0x31) and hashes them internally (native `crypto.sign('sha256', …)`); RSA → PKCS#1 v1.5 over SHA-256(tbs), ECDSA → DER-encoded sig. Dispatch lives in `buildSignerInfo()` in `src/crypto/cms.ts`: `const provider = options.provider ?? getCryptoProvider()` — when present, `signatureValue = provider.sign(...)` and `rsaKey`/`ecKey` are not required; else the pure-JS `rsaSignHash`/`ecdsaSignHash` fallback runs. `CmsSignOptions.provider` and `PdfSignOptions.provider` thread the per-call provider (per-call wins over global). Exported from `src/index.ts`. This is the in-library escape hatch for the SECURITY.md BigInt timing caveat.
Expand Down Expand Up @@ -305,7 +305,7 @@ npm run verify:docs # 17 offline rules over docs/, playgrounds, README, llms
- **PDF /Info metadata** — Title, Producer (pdfnative), CreationDate in D:YYYYMMDDHHmmss format
- **Input validation** — at `buildPDF()` boundary: null/undefined/type checks, 100K row limit
- **URL validation** — at `validateURL()`: blocks javascript:, file:, data: schemes
- **Test coverage** — 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90 (statements/branches/functions/lines). 2665+ tests (122 files), 89 fuzz tests across 5 files (including recursion/zip-bomb/xref-chain hardening), dual-mode visual-regression suite, performance benchmarks
- **Test coverage** — 95%+ statements measured at the v1.6.0 release; CI enforces 88/80/85/90 (statements/branches/functions/lines). 2686+ tests (123 files), 89 fuzz tests across 5 files (including recursion/zip-bomb/xref-chain hardening), dual-mode visual-regression suite, performance benchmarks
- **NPM provenance** — signed builds via GitHub Actions OIDC
- Security: no `eval()`, no `Function()`, no dynamic code execution
- No `console.log` in library code (only in tools/ and scripts/). `console.warn` is allowed **only** inside `src/core/pdf-diagnostics.ts` — the single sanctioned sink for conformance diagnostics (silence or redirect via `onDiagnostic`, escalate via `strict`)
Expand Down
5 changes: 4 additions & 1 deletion .github/instructions/api-design.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ applyTo: "src/index.ts"
17. Core — Digital Signatures (`buildSigDict`, `signPdfBytes`, `estimateContentsSize`, `addSignaturePlaceholder`, `PdfSignOptions`, `SigDictMetadata`)
18. Core — Streaming (`buildPDFStream`, `buildDocumentPDFStream`, `buildPDFStreamTrue`, `buildDocumentPDFStreamTrue`, `buildPDFStreamPageByPage`, `buildDocumentPDFStreamPageByPage`, `streamToFile`, `StreamOptions`)
19. Crypto — Primitives (`sha384`, `sha512`, `hmacSha256`, `rsaSign`, `ecdsaSign`, `parseCertificate`, `buildCmsSignedData`, `setCryptoProvider`)
20. Parser — Read & Modify (`openPdf`/`PdfReader`, `createModifier`/`PdfModifier`, `createTokenizer`/`PdfTokenizer`, `parseValue`, `parseIndirectObject`, `parseXrefTable`, `PdfValue`, `ParsedDict`, `PdfRef`, `extractText`, `readFormFields`-family lives in core, `mergePdfs`/`splitPdf`/`extractPages` + streaming variants, `validatePdfUA`, decode filters)
20. Parser — Read & Modify (`openPdf`/`PdfReader`, `createModifier`/`PdfModifier`, `createTokenizer`/`PdfTokenizer`, `parseValue`, `parseIndirectObject`, `parseXrefTable`, `PdfValue`, `ParsedDict`, `PdfRef`, `extractText`, `readFormFields`-family lives in core, `mergePdfs`/`splitPdf`/`extractPages` + streaming variants, `validatePdfUA`, decode filters, `PdfModifier.updateMetadata` (v1.7.0))
21. Core — LTV / PAdES (v1.7.0: `signPdfBytesWithTimestamp`, `collectValidationInfo`, `embedValidationInfo`, `addValidationInfo`, `addDocumentTimestamp`, `listSignatures`, `vriKeyForContents`, `buildDocTimeStampDict`; injected transports: `setTimestampProvider`/`getTimestampProvider`, `setRevocationProvider`/`getRevocationProvider`, `TimestampProvider`, `RevocationProvider` — the engine itself never opens a socket)
22. Core — Print Production (v1.7.0: `layout.print` page boxes + `bleed` shorthand + `marks`, `/Trapped` metadata with XMP parity, custom OutputIntent ICC, `/UserUnit`, print-dialog `viewerPreferences`: `duplex`, `pickTrayByPDFSize`, `printPageRange`, `numCopies`)
23. Core — PDF/A Diagnostics (v1.7.0: `strict`, `onDiagnostic`, `PdfDiagnostic`, `PdfDiagnosticCode` — codes `PDFA_NO_FONT_ENTRIES`, `PDFA_UNEMBEDDED_FORM_FONT`, `PDFA_DEVICE_CMYK_IMAGE`)

## Parser Option-Type Precedent
- Parser-module option/result types live in the module itself (`MergeOptions`,
Expand Down
Loading
Loading