Skip to content

Release v1.2.0 — addSignaturePlaceholder (#45), ASN.1 DN slice fix (#46), page-by-page streaming, UAX #9 embeddings, USE-lite, smart tables, bold-width fix#47

Merged
Nizoka merged 13 commits into
mainfrom
release/v1.2.0
May 27, 2026
Merged

Conversation

@Nizoka

@Nizoka Nizoka commented May 27, 2026

Copy link
Copy Markdown
Owner

Description

This PR cuts pdfnative v1.2.0. 100 % backward-compatible. Every new feature is additive or opt-in. Pre-existing PDFs are byte-identical for unchanged code paths in their body rendering; the only documented visual shift is a 2–5 pt correctness fix on right-/centre-aligned bold table headers (Helvetica-Bold metrics now drive bold positioning).

What changed and why

  • feat(crypto, Feature request — addSignaturePlaceholder(...) API #45): new addSignaturePlaceholder(pdfBytes, options?) API — AcroForm + invisible signature widget + /Sig dictionary via incremental update (ISO 32000-1 §7.5.6). Idempotent on already-signed PDFs. Enables one-call signPdfBytes(addSignaturePlaceholder(buildDocumentPDFBytes(...))).
  • fix(crypto, Bug — parseCertificate(...) returns issuer.raw / subject.raw with malformed slices #46): ASN.1 decodeAt() now recursively absolutises descendant offsets so parseCertificate() issuer/subject raw slices begin with the SEQUENCE tag 0x30. CMS IssuerAndSerialNumber parses correctly in Adobe Reader, openssl-cms, and pdfnative's own verify path.
  • feat(core): buildDocumentPDFStreamPageByPage() and buildPDFStreamPageByPage() emit assembled PDFs as AsyncGenerator<Uint8Array> chunked at PDF object boundaries (\nendobj\n).
  • feat(shaping): UAX chore(deps): bump typescript-eslint from 8.57.2 to 8.58.1 in the dev-dependencies group #9 explicit embeddings — normalizeBidiEmbeddings() rewrites LRE/RLE/LRO/RLO/PDF (U+202A–U+202E) to sealed-isolate equivalents before BiDi resolution. resolveBidiRuns() invokes the normaliser transparently.
  • feat(shaping): USE-lite cluster classifier (classifyUseCategory, classifyClusters) shipped as a public API. Devanagari/Bengali/Tamil shaper rewire follows in v1.3.0.
  • feat(core, tables): Smart tables — planner-driven multi-page rendering with wrap: 'auto' | 'always' | 'never', repeatHeader, zebra, caption, minRowHeight, cellPadding. Two-phase pipeline (planTable()_paginateBlocks()renderTable(slice)). Tagged-mode /Table continues across slices via shared structure-tree accumulator (ISO 14289-1 §7.10.6). Existing single-page tables are byte-identical to v1.1.0 in their body rendering.
  • fix(fonts, tables): right- and centre-aligned bold text (table headers, captions) is now measured with Adobe Helvetica-Bold AFM widths instead of Helvetica-Regular. Pre-1.2.0 the Amount header overshot its column by ~2 pt at 8 pt; the trailing t was clipped. New public helveticaBoldWidth(str, sz) + opt-in bold flag on txtR/C/RTagged/CTagged. Wired through smart-table headers, legacy buildPDF(), captions, and autoFitColumns. Unicode/CIDFont mode unaffected.
  • fix(core, tables): the document-builder renderTable() no longer hardcodes the 4th column (i === 3) as "Amount" with bold + credit/debit colour. Styling is opt-in via the new ColumnDef.kind === 'amount' field. Legacy buildPDF() (financial statement path) keeps the historical heuristic for byte-identical v1.0/v1.1 output.
  • fix(core, tables): emitCell now applies the v1.1 character truncate (mx / mxH) only when wrap: 'never'. Under 'auto' (default) and 'always', the planner has already sized the column to fit; the redundant char-truncate previously inserted spurious ellipses in auto-fitted tables.
  • feat(core, mcp): new PDF_A_CONFORMANCE_TARGETS = ['pdfa1b','pdfa2b','pdfa2u','pdfa3b'] as const + PdfAConformanceTarget type exported from the root. Single source of truth for tooling — pdfnative-mcp consumes via import { PDF_A_CONFORMANCE_TARGETS } from 'pdfnative' for its tool-schema enum:, materially improving how Gemini-CLI and other LLM agents discover the legal pdfA values.
  • chore(types): SigDictMetadata re-exported from the root (release notes already advertised it).
  • fix(shaping): invisible UAX chore(deps): bump typescript-eslint from 8.57.2 to 8.58.1 in the dev-dependencies group #9 controls (LRM/RLM, LRE/RLE/PDF/LRO/RLO, isolates) stripped at the encoder boundary via new public stripBidiControls() — prevents .notdef tofu on pure-LTR paragraphs containing orphan controls.
  • fix(samples): bidi-embeddings-showcase.pdf orphan-PDF paragraph ("textwith""text with"); table-wrap-auto.pdf / table-zebra-caption.pdf amount columns formatted via toFixed(2) (was +37.019999999999996); emoji samples register latin alongside emoji so ASCII digits route to Noto Sans VF; signature samples gain inline clarifier paragraphs.
  • docs(demo): new smart-tables example added to the live demo gallery at pdfnative.dev — 32-row table exercising wrap: 'auto', repeatHeader: true, zebra: true, caption end-to-end in the browser.

Zero-dependency guarantee — still intact

package.json v1.2.0 has no dependencies, no peerDependencies, no optionalDependencies. Every new feature is implemented with pdfnative's own primitives.

Deferred to v1.3.0

COLRv1 colour emoji renderer; USE-lite shaper rewire (Devanagari/Bengali/Tamil); internal page-by-page assembly; pixel-diff visual regression; UAX #9 X4–X5 character-level overrides.

Full notes: release-notes/v1.2.0.md.

Downstream coordination

  • pdfnative-mcp: import PDF_A_CONFORMANCE_TARGETS for tool-schema enum:; collapse prepare_signature_placeholder to a thin wrapper around addSignaturePlaceholder(); forward the six new optional TableBlock fields plus ColumnDef.kind in add_table. Repinning to pdfnative@1.2.0 lights up all v0.4.0 roadmap items.
  • pdfnative-cli: drop the local placeholder injector in sign; invalidate any cached issuer/subject DN slices from previously-signed PDFs (Bug — parseCertificate(...) returns issuer.raw / subject.raw with malformed slices #46 fix). To preserve v1.1.0 table output, set wrap: 'never', repeatHeader: false.

Risk & rollback

Risk surface: (a) renderer fix for wrap: 'auto' — 3 new regression tests guard the byte-stability boundary; (b) ColumnDef.kind opt-in — legacy financial PDFs go through buildPDF() which retains i === 3 (byte-stable); (c) Helvetica-Bold positioning — 2 regression tests cover the bold header right-edge. Rollback: revert the tip commit; no schema/data migrations involved.

Related Issues

Fixes #45
Fixes #46

Checklist

  • Tests pass (npm run test) — 53 files / 1822 tests, all green
  • Type check passes (npm run typecheck:all) — clean (src/ + tests/ + scripts/)
  • Lint passes (npm run lint) — clean
  • New code has tests (coverage thresholds must not regress) — statements 91.42 % / branches 88.10 % / functions 95.29 % / lines 91.42 %, all above thresholds (90/80/85/90)
  • CHANGELOG.md updated (if user-facing change) — entry under [1.2.0] covering every change
  • No breaking changes (or documented in description) — 100 % backward-compatible; opt-in ColumnDef.kind, wrap, repeatHeader; bold-header positioning shift is a documented correctness fix

Additional release hygiene

Nizoka added 13 commits May 27, 2026 14:40
Closes #46. parseCertificate() was returning issuer.raw / subject.raw
slices that did not begin with the ASN.1 SEQUENCE tag (0x30) because
decodeAt() only patched direct-child offsets — grandchildren kept
offsets relative to their parent's value subarray. Embedding those
slices in a CMS IssuerAndSerialNumber produced unparseable output
that Adobe Reader and openssl-cms rejected.

Fix: new internal shiftOffsets() helper walks every descendant once
and absolutises its offset against the original DER buffer.

Defensive: parseName() now asserts raw[0] === 0x30 with a diagnostic
message — catches any future regression of the ASN.1 offset machinery.

Tests: 5 new regression cases in tests/crypto/crypto.test.ts
exercising the slice tag, structural re-parse, self-signed roundtrip,
and the defensive parseName assertion (94 / 94 green).
…on (#45)

Adds a public addSignaturePlaceholder(pdfBytes, options?) API that injects an AcroForm + invisible signature widget placeholder into an existing PDF via incremental update (ISO 32000-1 7.5.6, 12.7.4.5, 12.8). The output is byte-compatible with signPdfBytes() and ready for CMS signing without any downstream tooling having to duplicate the BYTERANGE_PLACEHOLDER / buildSigDict() byte layout.

- New module src/core/pdf-sig-placeholder.ts (addSignaturePlaceholder + AddSignaturePlaceholderOptions).

- Extract SigDictMetadata from PdfSignOptions so buildSigDict() can be called without key material (placeholder phase has no certs yet).

- New PdfModifier.addRawObject(body) primitive for emitting verbatim object bodies so the /Contents <00...> and /ByteRange [0 ...] placeholders remain byte-identical.

- Widen isRef()/isArray() to accept PdfValue | undefined for ergonomic dict lookups.

- 13 vitest cases covering round-trip, idempotency, AcroForm merge, encryption rejection, fieldName/pageIndex/placeholderBytes validation, /Prev chain integrity.

- Export addSignaturePlaceholder + AddSignaturePlaceholderOptions from src/index.ts.

Closes #45
…aming

Adds buildDocumentPDFStreamPageByPage() and buildPDFStreamPageByPage() that yield Uint8Array chunks aligned at PDF object boundaries (endobj). Each chunk is a self-contained PDF segment: header chunk first, then one indirect object per chunk, then a final xref/trailer/startxref chunk.

This is the v1.2 step toward constant-memory PDF generation. The public API is stable; the internal full-buffer assembler is staged for refactor in v1.3 without any caller-visible change.

- chunkAtObjectBoundaries() splits a binary PDF string at endobj.

- 8 vitest cases covering byte-equality, header/trailer placement, object-boundary alignment, TOC rejection.

- Exported from src/index.ts.
…rmalization

Adds normalizeBidiEmbeddings() which maps the legacy explicit directional formatting characters (LRE/RLE/LRO/RLO/PDF) to their sealed-isolate equivalents (LRI/RLI/PDI) so the existing BiDi pipeline processes them uniformly. The stack handles nesting up to UAX #9 BD13 max depth (125).

Pragmatic simplification: full UAX #9 character-level type override (X4-X5) inside LRO/RLO ranges and embedding leakage across LRE/RLE boundaries are staged for v1.3. The public API surface (resolveBidiRuns) is unchanged; embeddings just work transparently.

- 13 new vitest cases (9 normalization unit, 4 round-trip with resolveBidiRuns).

- Exported normalizeBidiEmbeddings from src/index.ts.

- Updated bidi.ts header docstring to reflect v1.2 capabilities.
Adds src/shaping/use-lite.ts — a public utility module implementing a subset of the Universal Shaping Engine (USE) classification spec for the three Indic scripts pdfnative currently ships shaping for.

- classifyUseCategory(cp): returns USE category (B/V/N/H/M/Mpre/Mabv/Mblw/Mpst/R/ZWJ/ZWNJ/O) for any code point.

- classifyClusters(cps): splits a code-point sequence into USE-lite clusters with prebase/base/above/below/post/tail buckets, including reph and conjunct-tail detection.

Scope note: the bundled Devanagari/Bengali/Tamil shapers continue to use their hand-tuned reordering logic in v1.2; rewiring them to drive from this module is staged for v1.3 once a shaping benchmark harness is in place. Downstream code can already use classifyClusters() directly for custom Indic text analysis.

- 23 vitest cases (11 single-codepoint, 12 cluster-level).

- Exported UseCategory/UseClassifiedCp/UseCluster types and classifyUseCategory/classifyClusters functions from src/index.ts.
…erators

Phase 8 of v1.2.0 release plan. Adds two new generators:

- signature-placeholder.ts: demonstrates addSignaturePlaceholder() (#45) including the idempotency contract (second call returns identical bytes).

- bidi-embeddings-showcase.ts: demonstrates UAX #9 LRE/RLE/LRO/RLO/PDF normalization via normalizeBidiEmbeddings() with Hebrew RTL examples.

Wired into scripts/generate-samples.ts. Total sample PDFs: 157.
Phase 10+11 of the v1.2.0 release plan.

- package.json: 1.2.0-alpha.1 -> 1.2.0.

- release-notes/v1.2.0.md: rewritten to match what actually shipped (drops the visual-regression and shaper-rewire claims; flags internal page-by-page assembly + UAX #9 X4-X5 overrides + COLRv1 as v1.3 targets).

- CHANGELOG.md: [1.2.0] entry rewritten to match the new release notes.

- ROADMAP.md: v1.2.0 items moved to Released; v1.3.0 Planned section refreshed (COLRv1 renderer, USE-lite shaper rewire, internal page-by-page assembly, pixel-diff visual regression, UAX #9 X4-X5).

- README.md: pdfnative line bumped to v1.2.0; test counts to 1788/52; BiDi line mentions isolates + embeddings; streaming + signatures highlights gain v1.2.0 anchors.

- .github/copilot-instructions.md: file/test counts refreshed; new architecture entries for pdf-sig-placeholder; new convention notes for UAX #9 embeddings, USE-lite, signature placeholder, ASN.1 grandchild fix, and page-by-page streaming.

- .gitignore: RELEASE_PR_*.md scratchpads.

All gates green: npm run typecheck:all clean, npm test = 52 files / 1788 tests, npm run test:generate = 157 PDFs.
…facts

Phase A - scripts/README.md: bump '140+ PDFs' to '157 PDFs (28 generators)', add 4 missing entries (signature-placeholder, bidi-embeddings-showcase, pdfa-latin-embedding, emoji-showcase).

Phase B - README.md: add USE-lite highlight bullet alongside the v1.2 BiDi embeddings line.

Phase C - docs/index.html: BiDi card mentions isolates + embeddings; signatures card mentions addSignaturePlaceholder(); production card bumps to 1788+ tests / 52 files + page-by-page streaming. docs/guides/onboarding.md: v1.1.0 -> v1.2.0. docs/guides/index.html: 23 -> 28 generators / ~140 -> 157 PDFs, new signatures guide entry.

Phase D - new docs/guides/signatures.{md,html} covering the three-line addSignaturePlaceholder() workflow, algorithms, validation with openssl-cms / Adobe Reader, and pointers to the digital-signature + signature-placeholder generators.

Phase E - new llms.txt (machine-readable doc index, 2026 OSS standard) + AGENTS.md (editor-agnostic agent guidance, DRY against .github/copilot-instructions.md). release-notes/v1.2.0.md gains a 'Downstream integration notes' section explicitly addressing pdfnative-mcp and pdfnative-cli maintainers - addSignaturePlaceholder collapses pdfnative-mcp's prepare_signature_placeholder workaround, unlocks v0.4 'sign any PDF in one call', and #46 invalidates cached X.509 issuer/subject slices.
…ator output in signature samples

Phase 1 (runtime fix): new stripBidiControls() in src/shaping/bidi.ts strips LRM/RLM, LRE/RLE/PDF/LRO/RLO (U+202A-E), and LRI/RLI/FSI/PDI (U+2066-9) before they reach the font cmap. Applied at the four encoder entry points (pdfString, helveticaWidth, textRuns, ps) so orphan bidi controls in pure-LTR paragraphs no longer surface as .notdef tofu. Exported from src/index.ts. Fixes the tofu seen under 'Orphan PDF (silently dropped)' in bidi-embeddings-showcase.pdf. 6 new tests added; total 1794.

Phase 2 (samples): emoji-basic and emoji-table generators now register ['latin', 'emoji'] instead of ['emoji'] alone so ASCII digits route to Noto Sans VF rather than Noto Emoji's em-wide glyphs. Fixes right-margin overflow in emoji-basic.pdf and garbled Duration column in emoji-table.pdf.

Phase 3 (docs): clarifier paragraphs added to signature-placeholder.ts and digital-signature.ts samples; new 'Reading the validator output' section in docs/guides/signatures.md explaining that Adobe Reader's 'Validite de la signature inconnue' (self-signed demo CA) and 'Signature non valable' (unsigned placeholder) are expected by-spec behaviour, not bugs.

Docs: CHANGELOG, llms.txt, release-notes/v1.2.0.md updated to reflect 1794 tests and the new Fixed/Changed entries.
….2.0)

Adds six optional TableBlock fields (all @SInCE 1.2.0):

  - wrap: 'auto' | 'always' | 'never' (default 'auto')

  - repeatHeader: boolean (default true)

  - zebra: boolean | PdfColor

  - caption: string

  - minRowHeight: number (default 12)

  - cellPadding: number (default 4)

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 section 7.10.6); /Caption emitted once on first slice.

Backward compatibility: single-page tables that fit without wrapping are byte-identical to v1.1.0. Multi-page tables now reprint header and wrap on overflow by default; opt back into v1.1 behaviour with repeatHeader:false + wrap:'never'.

Also fixes scripts/generators/bidi-embeddings-showcase.ts: restored missing space in orphan-PDF demo paragraph (textwith -> text with).

Tests: 14 new (7 planTable unit + 7 end-to-end). Total 1808 tests / 53 files.

Samples: 4 new (document/table-wrap-auto.pdf, table-multipage-header-repeat.pdf, table-zebra-caption.pdf, table-smart-autofit.pdf). Total 161 PDFs.

Docs: new guides/tables.md + tables.html guide; updated README, CHANGELOG, ROADMAP, AGENTS.md, copilot-instructions.md, llms.txt, docs/index.html, guides/index.html, guides/architecture.md, guides/mcp.md, release-notes/v1.2.0.md.
…polish)

Right- and centre-aligned bold text (table headers via enc.f2 and table captions) is now measured with Adobe Helvetica-Bold AFM advance widths instead of Helvetica-Regular. Pre-1.2.0 the renderer measured 'Amount' at ~25.44pt (Regular) but the glyphs rendered ~30.22pt wide (Bold) at 8pt, so the trailing glyph overshot the column boundary by ~2pt and the 't' was clipped/overhung into the neighbour column.

Changes:

  - New public helveticaBoldWidth(str, sz) in src/fonts/encoding.ts (re-exported from root and pdfnative/fonts).

  - txtR/txtC/txtRTagged/txtCTagged in src/core/pdf-text.ts gain an optional trailing bold flag (default false, backward-compatible).

  - emitCell() in src/core/pdf-renderers.ts passes bold:isHeader; caption passes bold:true.

  - Legacy buildPDF() headers in src/core/pdf-builder.ts pass bold:true on all four right/centre header sites.

  - computeAutoFitColumns() in src/core/pdf-column-fit.ts uses helveticaBoldWidth for the header measurement branch (Latin only).

  - SigDictMetadata interface re-exported from src/index.ts (release notes already advertised it as public).

  - Sample fix: document-table-parity makeRows() now formats amounts with toFixed(2) (was rendering '+37.019999999999996'); Amount column slightly widened in the wrap-auto sample.

Backward compatibility: existing single-page tables remain byte-identical to v1.1.0 in their BODY rendering. Right- and centre-aligned HEADER glyph positioning shifts by 2-5pt - a documented correctness fix, not a regression. Unicode/CIDFont mode unaffected.

Tests: 10 new (8 helveticaBoldWidth + 2 bold-header positioning regression). Total 1818 / 53 files.

Docs: cellPadding default corrected 4 -> 3 in release notes, tables.md, copilot-instructions; bold-width fix documented in release notes, CHANGELOG, tables.md migration table.
feat(types): PDF_A_CONFORMANCE_TARGETS + PdfAConformanceTarget exported

docs(demo): 10th live-demo example for smart tables

Two compounding bugs in the v1.2.0 smart-table renderer surfaced on table-smart-autofit.pdf:

  1. renderTable() hardcoded 'i === 3' as the Amount column, forcing the Notes column into Helvetica-Bold + credit/debit colour. autoFitColumns measured Regular metrics; rendering in Bold (~16% wider) overflowed the column, and the clipCells rect chopped the trailing character. Fix: opt-in styling via the new optional ColumnDef.kind === 'amount' field. The legacy buildPDF() financial path keeps i === 3 for byte-identical v1.0/v1.1 output.

  2. emitCell() applied truncate(text, col.mx) on every single-line cell, even under wrap: 'auto' where the planner had already sized the column to fit. The redundant char-truncate produced spurious '...' ellipses. Fix: gate the v1.1 char-truncate on wrapMode === 'never'.

MCP / Gemini-CLI discoverability: new public const PDF_A_CONFORMANCE_TARGETS = ['pdfa1b','pdfa2b','pdfa2u','pdfa3b'] as const plus PdfAConformanceTarget type are exported from the root. Single source of truth for tooling — pdfnative-mcp can now spread this into its tool-schema enum: instead of hardcoding string literals.

Live demo: 10th EXAMPLES entry in docs/app.js — 32-row smart-tables demo exercising wrap='auto', repeatHeader=true, zebra=true and a caption end-to-end in the browser. Playgrounds left untouched (the v1.2.0 features showcase best as an inline live demo).

Zero-dependency policy: verified intact — package.json v1.2.0 has no dependencies, no peerDependencies, no optionalDependencies.

Tests: 3 new in tests/core/pdf-table.test.ts (kind:'amount' opt-in applies bold + credit; absence of kind keeps default styling; wrap='never' preserves char-truncate ellipsis; wrap='auto' skips it). Total 53 files / 1822 tests, all green.

Docs refresh: release-notes/v1.2.0.md (Fixed + Added + Changed + Downstream notes), CHANGELOG, copilot-instructions, AGENTS, README, llms.txt, docs/index.html, docs/guides/tables.md (ColumnDef.kind), docs/guides/pdfa.md (PDF_A_CONFORMANCE_TARGETS), docs/guides/mcp.md (MCP adoption note). RELEASE_PR_v1.2.0.md fully rewritten.
@Nizoka Nizoka self-assigned this May 27, 2026
@Nizoka Nizoka added bug Something isn't working enhancement New feature or request release Tracks a versioned release — implementation, quality gates, and publish workflow labels May 27, 2026
@Nizoka Nizoka merged commit 5312a53 into main May 27, 2026
6 checks passed
@Nizoka Nizoka deleted the release/v1.2.0 branch May 27, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request release Tracks a versioned release — implementation, quality gates, and publish workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug — parseCertificate(...) returns issuer.raw / subject.raw with malformed slices Feature request — addSignaturePlaceholder(...) API

1 participant