Repository navigation
v1.5.0 — typography, 27 scripts, CMYK & PDF/X-4
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 --strictwrites a
PDF/X-4 file thatinspect --check pdfxre-validates. - Reproducible output — the global
--creation-date <iso8601>(or
SOURCE_DATE_EPOCH) pins/CreationDate,xmp:CreateDate,{date}and the trailer
/IDin 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/swaliases oflatin,--font-file <path.ttf>[:name]
(validated, capped, never from a JSON payload);--variant tableembeds fonts too. - Roadmap items delivered — global flags before the command
(pdfnative --json --dry-run render …),annotate link,sign --timestamp-timeout,
a weak-digest note inverify(SHA-1 imprints refused under--strict),
inspect --iso-dates, richerdoctorchecks, 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 overdocs/assets/ecosystem.json),release-prepare.ts, hardened workflows
(harden-runner, SHA pins, Trusted Publishing, SBOM + attestations, committed rulesets),
CLAUDE.md=@AGENTS.mdwith a guard hook and a/release-auditskill.
What's new
render
layout.typographypassthrough (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
TypographyOptionskeys verbatim), paragraphalign: "justify", block-level
keepWithNext/splittable;
--split-paragraphs,--keep-headings-with-next,--kerning,--font-features <tag,…>.
Nestedtypography/outputIntentobjects now merge one level deep between the
document,--layoutand 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,acspchecked, merged into
layout.outputIntent),--output-intent-id <s>,--trapped true|false|unknown.
Pre-checks refuse--pdfxwith PDF/A or encryption (exit 2); the engine's PDF/X and print
coherence errors map toE_INPUT;PDFX_NO_FONT_ENTRIES,PDFX_DEVICE_CMYK,
PDFX_ANNOTATIONSandTYPOGRAPHY_FEATURE_INEFFECTIVEjoin the--strictdiagnostics
(9 codes); the envelope carriespdfx.--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 tableembeds--langfonts.--layoutfiles and inlinelayoutrevivecreationDateandoutputIntent.iccProfile
arrays;--layoutfiles share the 50 MB cap.
Global flags
--creation-date <iso8601>(10th global flag; fallbackSOURCE_DATE_EPOCH) pins the
creation instant process-wide throughsetDefaultCreationDate()—batchtasks inherit
it; the envelope carriescreationDate; an invalid value is a usage error.
sign --signing-timeandmetadata --mod-datestay separate; encrypted output is never
reproducible.- Global flags may precede the command name:
parseArgsgained 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;--summaryaddspdfx; text report printsPDF/X:lines.annotatetypelink(rect+url—http,https,mailto;E_INPUTotherwise),
emitted as a/Link+/URIaction; CMYK colours accepted.sign --timestamp-timeout <ms>— bound on the TSA round-trip, reported as
timestamp.timeoutMs.verifyreportstimestampDigest; a SHA-1 messageImprint adds
weak digest: RFC 3161 messageImprint uses SHA-1 (refused under --strict)and fails
under--strict(E_VERIFY_FAILED).doctorchecksfonts(31 modules / 27 scripts, probed on disk),unicode
(USE_UNICODE_VERSION) andconformance(pdfa1b,pdfa2b,pdfa2u,pdfa3b,pdfx4).
Agent surface
schema:renderdescribes typography,layout.pdfx, CMYK and colour bars;inspect
requirespdfxConformanceand pinspdfx;inspect-summaryrequirespdfx;status
pinspdfx,creationDate,timestamp.timeoutMs;annotategainslink+url;
verifygainstimestampDigest. Completions carry every new flag.govern policyprints the full 1.1.0 policy (claude_codeblock included), identical to
.github/ai-governance.json;govern rulesprints.github/AGENT_RULES.mdverbatim.render --dry-runpre-flights the real build in memory and discards the bytes: engine
coherence errors (--pdfxwithout an output intent, inlinelayout.pdfx+tagged,
attachments without PDF/A-3, …) and diagnostics —--strictincluded — surface exactly as
on a real run; the envelope carriesdryRun: true, nobytes,diagnosticswhen any.schema statuspins every emitted field (31 properties, command-specific ones
included), held by a parity test over everyemitStatus({…})call.inspectemitsmetadata.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.txtcarriesCurrent 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.logor
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-fontsproves 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 GrayprtrICC 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(compositesetup-verapdfaction, 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
underTZ=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-auditskill,.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 byschema 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/comparereturn the source text of tagged documents (/ActualText),
not the shaped glyph string.samples/run-all.jsis removed;npm run build && npm run test:generate(or
PDFNATIVE_CLI=… npm run test:generateagainst 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-1192.0.2.0/24and NAT64
64:ff9b::/96(and their IPv4-mapped forms) are refused. - Bounded, validated binary inputs:
--layout≤ 50 MB, ICC ≤ 16 MiB with theacsp
signature, fonts ≤ 32 MiB with magic-byte and parser validation; fonts and profiles are
only ever loaded from command-line paths. verifysurfaces SHA-1 timestamp imprints and refuses them under--strict.annotate linkURLs go throughvalidateURLand 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, weeklynpm 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 aCliErrorwith a stable code. It drove three hardenings:
prototype-key and depth guards in the--templatedeep-merge, the config loader and the
manifest parser, and the 16 MiB cap on aniccProfilearray supplied in JSON. - The gate's
bundle-checkstep proves the published bundle keeps the engine external and
carries no font data, PEM block,console.logor attribution trailer.
Fixed
pdfnative --json render …no longer swallows the command name.--layoutno longer replaces a nestedtypography/outputIntentobject 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 syntheticprtrCMYK profile (also shipped undersamples/render/print/). --font-filederives the same default font name on every platform:C:\fonts\a.ttfnamed
the fontaon Windows andc-fonts-aon Linux, because only the host's separator was
honoured. Both/and\now end a directory everywhere.inspectno longer answerstitle: null(norcompareraw 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.renderrefuses an unknownformField.fieldTypewithE_INPUT(also under--dry-run).
The engine does not validate the value:textareaorselectfell through its geometry
table and wroteNaNcoordinates, a PDFfillthen refused. The six engine types are
text,multilineText,checkbox,radio,dropdown,listbox; three shipped samples
usedtextareaand 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 statusprints the
statusschema); 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
pdfnativestill prints the usage (final review D-02).
Notes
inspect --check pdfxuses 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:
metadatarewrites 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_INEFFECTIVEreports a requested feature the font
lacks (tnumon 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, andsubs/supsglyphs 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--fontregisters a
font, that font measures the text.samples/render/base14/renders it without font flags.layout.typography.kerningon an untagged document can surface a stray space inside a
kerned word inextract-textoutput (the engine splits the run at the kerning adjustment);
tagged output (--tagged, PDF/A, PDF/UA) carries/ActualTextand is unaffected.- Under
--json, stderr may carry warning lines before the envelope: the envelope is always
the LAST line (--quietdrops the rest) — see AGENT_CONTRACT §1. - The CI
eol-lfrule 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.0Upgrade
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 beforetest: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-allgreen locally — 14 steps, 0 skipped (veraPDF 1.30.2, JDK 13).- Sample corpus: 91 sample PDFs generated twice, byte-identical,
verify-samples --strictgreen. - Conformance corpus: 22 files —
validate:pdfa14 PASS + 4 XFAIL (negative canaries
correctly rejected),validate:pdfx3 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 underTZ=Europe/ParisandTZ=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 inrelease-notes/draft/PR-v1.5.0.md. npm auditclean; SBOM (CycloneDX) and build attestations attached by the release
workflow; verify an install withnpm audit signatures.