Skip to content

v1.5.0 — typography, 27 scripts, CMYK & PDF/X-4

Choose a tag to compare

@Nizoka Nizoka released this 20 Sep 22:47
· 22 commits to main since this release
441ab75

pdfnative-cli v1.5.0

Released 2026-09-17

Built on pdfnative 1.8.0. This release exposes every 1.8.0 engine feature — the
typography engine, CMYK colours and colour bars, PDF/X-4 output with a structural
validator, five more scripts (27 Unicode scripts) with latin aliases, custom fonts
from disk, and UTC, pinnable creation dates that make output byte-reproducible — and
delivers every roadmap item the engine unblocked. It also adopts the engine's engineering:
one quality gate, a byte-exact sample baseline, a PDF/A + PDF/X conformance corpus,
hardened CI with Trusted Publishing and attestations, a documentation verifier and a
committed Claude Code layer. 21 commands, 19 schema subjects and 12 stable error codes
are unchanged; the command surface is 100% backward-compatible.

Highlights

  • Typography — layout.typography (widows/orphans, keep-with-next, splittable
    paragraphs, justification, optical margins, soft hyphens, French punctuation spacing
    with unit binding, kerning, OpenType features, exact base-14 metrics) plus four flags:
    pdfnative render -i report.json -o report.pdf --font latin --lang latin \
      --split-paragraphs --keep-headings-with-next --kerning --font-features onum,smcp
  • CMYK & PDF/X-4 — every colour accepts CMYK, printer's marks gain colour bars, and
    render --pdfx pdfx4 --output-intent-icc press.icc --trapped false --strict writes a
    PDF/X-4 file that inspect --check pdfx re-validates.
  • Reproducible output — the global --creation-date <iso8601> (or
    SOURCE_DATE_EPOCH) pins /CreationDate, xmp:CreateDate, {date} and the trailer
    /ID in UTC: the same input renders to identical bytes on every host and timezone.
  • 27 scripts & custom fonts — --font lo|nod|khb|tdd|cjm (Lao, Tai Tham, New Tai
    Lue, Tai Le, Cham), ha/yo/ig/sw aliases of latin, --font-file <path.ttf>[:name]
    (validated, capped, never from a JSON payload); --variant table embeds fonts too.
  • Roadmap items delivered — global flags before the command
    (pdfnative --json --dry-run render …), annotate link, sign --timestamp-timeout,
    a weak-digest note in verify (SHA-1 imprints refused under --strict),
    inspect --iso-dates, richer doctor checks, three more SSRF-blocked ranges, a size
    cap on --layout, CHANGELOG compare links.
  • Engineered like the engine — npm run gate (fast / CI / publish, --require-all),
    a 91-sample byte/semantic baseline, a 22-file PDF/A + PDF/X corpus, verify:docs
    (26 rules over docs/assets/ecosystem.json), release-prepare.ts, hardened workflows
    (harden-runner, SHA pins, Trusted Publishing, SBOM + attestations, committed rulesets),
    CLAUDE.md = @AGENTS.md with a guard hook and a /release-audit skill.

What's new

render

  • layout.typography passthrough (widows, orphans, splitParagraphs,
    keepHeadingsWithNext, opticalMargins, punctuationSpacing: "fr" | "fr-CA" | rules[],
    unitBinding, bindShortWords, hyphenationLanguage, kerning, fontFeatures,
    metrics; soft hyphens U+00AD are honoured unconditionally — the engine's
    TypographyOptions keys verbatim), paragraph align: "justify", block-level
    keepWithNext / splittable;
    --split-paragraphs, --keep-headings-with-next, --kerning, --font-features <tag,…>.
    Nested typography / outputIntent objects now merge one level deep between the
    document, --layout and the flags.
  • CMYK colours ("c m y k" 0–1 or [c,m,y,k] percent) everywhere;
    layout.print.marks.colourBars: true | { tints, size }.
  • --pdfx pdfx4, --output-intent-icc <file.icc> (16 MiB cap, acsp checked, merged into
    layout.outputIntent), --output-intent-id <s>, --trapped true|false|unknown.
    Pre-checks refuse --pdfx with PDF/A or encryption (exit 2); the engine's PDF/X and print
    coherence errors map to E_INPUT; PDFX_NO_FONT_ENTRIES, PDFX_DEVICE_CMYK,
    PDFX_ANNOTATIONS and TYPOGRAPHY_FEATURE_INEFFECTIVE join the --strict diagnostics
    (9 codes); the envelope carries pdfx.
  • --font / --lang: lo, nod, khb, tdd, cjm; ha, yo, ig, sw → latin
    (31 bundled font modules / 27 scripts). --font-file <path.ttf>[:name] (repeatable):
    path check, 32 MiB cap, magic bytes, parseFontData + validateFontData, name from the
    file stem, added to --lang. --variant table embeds --lang fonts.
  • --layout files and inline layout revive creationDate and outputIntent.iccProfile
    arrays; --layout files share the 50 MB cap.

Global flags

  • --creation-date <iso8601> (10th global flag; fallback SOURCE_DATE_EPOCH) pins the
    creation instant process-wide through setDefaultCreationDate() — batch tasks inherit
    it; the envelope carries creationDate; an invalid value is a usage error.
    sign --signing-time and metadata --mod-date stay separate; encrypted output is never
    reproducible.
  • Global flags may precede the command name: parseArgs gained a boolean-flag table and
    splitCommandArgv() finds the command wherever the flags sit.

inspect / annotate / sign / verify / doctor

  • inspect --pdfx (validation report), --check pdfx, pdfxConformance (always present),
    --iso-dates; --summary adds pdfx; text report prints PDF/X: lines.
  • annotate type link (rect + url — http, https, mailto; E_INPUT otherwise),
    emitted as a /Link + /URI action; CMYK colours accepted.
  • sign --timestamp-timeout <ms> — bound on the TSA round-trip, reported as
    timestamp.timeoutMs.
  • verify reports timestampDigest; a SHA-1 messageImprint adds
    weak digest: RFC 3161 messageImprint uses SHA-1 (refused under --strict) and fails
    under --strict (E_VERIFY_FAILED).
  • doctor checks fonts (31 modules / 27 scripts, probed on disk), unicode
    (USE_UNICODE_VERSION) and conformance (pdfa1b,pdfa2b,pdfa2u,pdfa3b,pdfx4).

Agent surface

  • schema: render describes typography, layout.pdfx, CMYK and colour bars; inspect
    requires pdfxConformance and pins pdfx; inspect-summary requires pdfx; status
    pins pdfx, creationDate, timestamp.timeoutMs; annotate gains link + url;
    verify gains timestampDigest. Completions carry every new flag.
  • govern policy prints the full 1.1.0 policy (claude_code block included), identical to
    .github/ai-governance.json; govern rules prints .github/AGENT_RULES.md verbatim.
  • render --dry-run pre-flights the real build in memory and discards the bytes: engine
    coherence errors (--pdfx without an output intent, inline layout.pdfx + tagged,
    attachments without PDF/A-3, …) and diagnostics — --strict included — surface exactly as
    on a real run; the envelope carries dryRun: true, no bytes, diagnostics when any.
  • schema status pins every emitted field (31 properties, command-specific ones
    included), held by a parity test over every emitStatus({…}) call.
  • inspect emits metadata.modDate (normalised by --iso-dates); .pdfnativerc.json
    per-command sections apply to all 21 commands.
  • The consumer contract moved to docs/AGENT_CONTRACT.md (from AGENTS.md, which now
    holds the repository rules for agents); llms.txt carries Current version, the 12
    codes, the new flags and the reproducibility promise.

Tooling & CI (ported from pdfnative 1.8.0)

  • scripts/gate.ts — npm run gate / gate:fast / --publish --require-all; steps
    typecheck:all, lint, test (fast) / build, dist-check, smoke (built binary), bundle-size,
    bundle-check (the engine stays external; no font data, PEM block, console.log or
    undeclared require in the bundle), test:generate, test:coverage, verify:docs,
    verify:samples, corpus:pdfa, validate:pdfx, validate:pdfa.
  • scripts/generate-samples.ts + verify-samples.ts — hermetic sample corpus over the
    built binary (TZ=UTC, pinned date), 91 sample PDFs held to
    tests/regression/baselines/samples.sha256.json (69 byte-exact, 10 semantic); replaces
    samples/run-all.js.
  • scripts/generate-pdfa-corpus.ts, validate-pdfa.ts, validate-pdfx.ts — a reproducible
    22-file corpus: 18 PDF/A (4 negative canaries, veraPDF 1.30.2), 4 PDF/X-4 (1 negative
    canary, in-process validator) — an AcroForm under PDF/A-2b, Gray and CMYK output intents,
    an ICC v4 profile refused under PDF/A-1b; table-pdfa2b-fonts proves the table variant embeds fonts.
  • tests/regression/engine-surface.json + engine-surface.test.ts — the traceability matrix of
    the pdfnative 1.8.0 changelog: each bullet mapped to named tests and baseline samples or
    waived with a reason; 9 diagnostic codes triggered, 27 script codes and 12 typography keys
    rendered by a sample; the suite fails when the engine pin moves without the matrix.
    scripts/lib/synthetic-gray-profile.ts — a valid 408-byte Gray prtr ICC profile.
  • scripts/verify-docs.ts (26 rules) + docs/assets/ecosystem.json;
    scripts/release-prepare.ts; scripts/build-claude-rules.ts.
  • Workflows: ci.yml (Node 22/24, audit, gate --require-all), publish.yml (Trusted
    Publishing with npm 11.19.1, publish gate, npm sbom + build attestations),
    verapdf.yml (composite setup-verapdf action, SHA-256-verified installer),
    sample-regression.yml, dependency-review.yml, audit.yml, docs.yml, codeql.yml,
    scorecard.yml — harden-runner, SHA pins, persist-credentials: false,
    npm ci --ignore-scripts; .github/rulesets/main.json + tags.json.
  • Repository hygiene: .npmrc, .nvmrc / .node-version = 22, .gitattributes
    (eol=lf, binaries, generated files), opt-in git hooks, tsconfig.scripts.json, vitest
    under TZ=UTC.
  • Claude Code layer: CLAUDE.md = @AGENTS.md + addendum, .claude/settings.json,
    .claude/hooks/guard.mjs (fail-closed HITL hook, tested), generated .claude/rules/,
    /release-audit skill, .github/prompts/quality-gate.prompt.md.

Compatibility

  • 100% backward-compatible command surface — no flag, default, exit code or error code
    changed; every envelope field is additive and pinned by schema status. 21 commands,
    19 subjects, 12 codes; 10 global flags (was 9).
  • Inherited pdfnative 1.8.0 byte-level changes: every date is written in UTC
    (+00'00'), {date} follows the pinned creation date, tagged output carries
    /ActualText, TrueType subsets carry hinting tables. Every PDF the CLI writes therefore
    differs byte-wise from 1.4.0 output while staying spec-valid; a consumer that hashed
    1.4.0 output must rebaseline once — and can now pin the bytes with --creation-date.
  • extract-text / compare return the source text of tagged documents (/ActualText),
    not the shaped glyph string.
  • samples/run-all.js is removed; npm run build && npm run test:generate (or
    PDFNATIVE_CLI=… npm run test:generate against a global install) replaces it.
  • Encrypted output is never byte-reproducible; a signed file's incremental revision carries
    a per-revision /ID (upstream — see Notes).

Security

  • SSRF guard: benchmarking 198.18.0.0/15, TEST-NET-1 192.0.2.0/24 and NAT64
    64:ff9b::/96 (and their IPv4-mapped forms) are refused.
  • Bounded, validated binary inputs: --layout ≤ 50 MB, ICC ≤ 16 MiB with the acsp
    signature, fonts ≤ 32 MiB with magic-byte and parser validation; fonts and profiles are
    only ever loaded from command-line paths.
  • verify surfaces SHA-1 timestamp imprints and refuses them under --strict.
  • annotate link URLs go through validateURL and PDF-string escaping.
  • Supply chain: Trusted Publishing (OIDC, npm ≥ 11.5.1), SBOM + build attestations,
    harden-runner and SHA-pinned actions on every job, npm ci --ignore-scripts, dependency
    review, weekly npm audit, committed rulesets, a HITL guard hook for agents.
  • A deterministic hostile-input suite (tests/fuzz/: argv, page selectors, config,
    manifests, layout JSON, mutated and xref-looping PDFs, dates, font headers) — the only
    acceptable failure is a CliError with a stable code. It drove three hardenings:
    prototype-key and depth guards in the --template deep-merge, the config loader and the
    manifest parser, and the 16 MiB cap on an iccProfile array supplied in JSON.
  • The gate's bundle-check step proves the published bundle keeps the engine external and
    carries no font data, PEM block, console.log or attribution trailer.

Fixed

  • pdfnative --json render … no longer swallows the command name.
  • --layout no longer replaces a nested typography / outputIntent object the document
    supplied when the file sets a sibling key.
  • A test used a 128-byte fake ICC profile the 1.8.0 engine rejects; the suite now uses a
    real synthetic prtr CMYK profile (also shipped under samples/render/print/).
  • --font-file derives the same default font name on every platform: C:\fonts\a.ttf named
    the font a on Windows and c-fonts-a on Linux, because only the host's separator was
    honoured. Both / and \ now end a directory everywhere.
  • inspect no longer answers title: null (nor compare raw bytes) for a title, author or
    subject outside Latin-1: pdfnative writes those as UTF-16BE text strings and both commands
    now decode them (utils/pdftext.ts). An em dash in a title was enough to lose it.
  • render refuses an unknown formField.fieldType with E_INPUT (also under --dry-run).
    The engine does not validate the value: textarea or select fell through its geometry
    table and wrote NaN coordinates, a PDF fill then refused. The six engine types are
    text, multilineText, checkbox, radio, dropdown, listbox; three shipped samples
    used textarea and are corrected (final review D-01).
  • A command flag placed before the command (pdfnative --strict render …) no longer swallows
    the command name: the dispatcher recovers it from the known command names — only before the
    first positional, and the flag keeps its boolean reading (--pretty schema status prints the
    status schema); a misspelt command is still reported as unknown (exit 1). Arguments with
    no command at all now exit 2 (E_USAGE) instead of printing the usage with exit 0; a bare
    pdfnative still prints the usage (final review D-02).

Notes

  • inspect --check pdfx uses pdfnative's structural validator — validation evidence, not a
    certified preflight. PDF/X and PDF/A cannot be claimed by the same file.
  • Upstream limits recorded in ROADMAP.md: metadata rewrites the XMP without the
    PDF/X identification (re-check after editing); signed output carries a per-revision /ID
    drawn at signing time (signed samples are fingerprinted semantically); PDF/X-1a / -3 /
    -4p and spot colours are not exposed by the engine; hyphenation dictionaries from the CLI
    are deferred by posture.
  • punctuationSpacing: "fr" inserts U+202F only where a font is embedded (--font latin --lang latin); TYPOGRAPHY_FEATURE_INEFFECTIVE reports a requested feature the font
    lacks (tnum on Noto Sans).
  • Untagged text extraction of shaped scripts has engine limits the test suite pins with
    it.fails: stacked Khmer / Myanmar clusters return U+FFFD, a CJK ideograph sharing its glyph
    with a Kangxi radical returns the radical, reordered glyphs (reph, Tamil and Thai pre-base
    signs) come back in visual order, and subs / sups glyphs as their own code points. Tagged
    output (--tagged) returns the source text exactly (samples/extract-text/02-actualtext).
  • layout.typography.metrics: "exact" acts on the base-14 path only: once --font registers a
    font, that font measures the text. samples/render/base14/ renders it without font flags.
  • layout.typography.kerning on an untagged document can surface a stray space inside a
    kerned word in extract-text output (the engine splits the run at the kerning adjustment);
    tagged output (--tagged, PDF/A, PDF/UA) carries /ActualText and is unaffected.
  • Under --json, stderr may carry warning lines before the envelope: the envelope is always
    the LAST line (--quiet drops the rest) — see AGENT_CONTRACT §1.
  • The CI eol-lf rule warns on the CRLF blobs still in history; the maintainer's
    renormalisation commit (git add --renormalize .) will flip it to fail.

Install

npm install -g pdfnative-cli@1.5.0

Upgrade

No action required from 1.x — all existing invocations behave identically on Node ≥ 22.
Hash-based consumers: 1.8.0 changes the bytes of every PDF (UTC dates, /ActualText,
hinting tables) — rebaseline once, then pin with --creation-date / SOURCE_DATE_EPOCH.
Sample baseline: tests/regression/baselines/samples.sha256.json is created in this
release (91 sample PDFs, all since: 1.5.0 — the 1.4.0 line had no determinism plumbing
to chain from); the conformance corpus grows from 12 to 22 files.
Baseline additions declared (before publication): twelve entries join it with since: 1.5.0
and no existing entry changes — base14/01-exact-metrics, font/06-color-emoji-sequences,
multilang/08-european-caucasian, 09-rtl, 10-indic, 11-cjk, pdfa/05-form-pdfa2b,
print/06-gray-pdfx4, print/07-tight-bleed-marks, typography/05-french-canadian-spacing,
06-custom-spacing-rules, 07-soft-hyphen-keep-with-next.
Rebaseline declared (before publication): form/01-contact-form.pdf, form/02-survey.pdf and
document/03-all-blocks.pdf changed when their fieldType: "textarea" became
"multilineText" (D-01) — the earlier bytes held NaN field rectangles.
Documents using a fieldType outside the six engine values now fail with E_INPUT instead of
producing a broken form: rename textarea → multilineText, select → dropdown.

Verification

  • 1403 vitest tests across 96 files green on Node 22 (Windows); the CI matrix (22 / 24)
    runs the same count: the gate builds and generates the samples before test:coverage,
    so the reproducible-build and sample-regression suites run there too and fail loudly
    (GATE_REQUIRE_ARTIFACTS=1) when their input is missing — before this release they
    skipped silently on a fresh runner (audit A-08). Coverage 86.06 % statements /
    75.91 % branches / 92.59 % functions / 87.92 % lines, thresholds raised to
    82 / 71 / 86 / 82 (vitest.config.ts).
  • npx tsx scripts/gate.ts --publish --require-all green locally — 14 steps, 0 skipped (veraPDF 1.30.2, JDK 13).
  • Sample corpus: 91 sample PDFs generated twice, byte-identical, verify-samples --strict green.
  • Conformance corpus: 22 files — validate:pdfa 14 PASS + 4 XFAIL (negative canaries
    correctly rejected), validate:pdfx 3 PASS + 1 XFAIL, exit 0.
  • npm run verify:docs: 26 rules across the documentation corpus, zero errors.
  • Built binary smoke-tested for every command; PDF/X-4 render → inspect --check pdfx
    round-trip; identical SHA-256 under TZ=Europe/Paris and TZ=UTC.
  • Independent release audit before publication: two auditors (55 claims checked against
    the code; 118 hardening mechanisms compared with pdfnative 1.8.0), an adversarial verifier
    (14 findings confirmed, 3 rejected, none blocking — all fixed in this release), a final
    reviewer (2 majors on form field types and flag placement, fixed) and a re-review of those
    fixes; the ledger is in release-notes/draft/PR-v1.5.0.md.
  • npm audit clean; SBOM (CycloneDX) and build attestations attached by the release
    workflow; verify an install with npm audit signatures.