Skip to content

v1.2.0 — Charts v2, print production, and the conformance channel

Latest

Choose a tag to compare

@Nizoka Nizoka released this 28 Aug 18:35
19130b0

pdfnative-react v1.2.0

Released 2026-08-26

Charts v2, print production, and the conformance channel. This release tracks
the pdfnative engine's 1.7.0
and keeps a promise this package made in writing: since 1.1.0,
docs/CHARTS.md said that when the engine shipped "Charts v2", the new
capabilities would "reach this package as new ChartProps fields". They
shipped; here they are — together with print-production page geometry, the
PDF/A conformance diagnostics channel, seven new lint rules, and the quality
backlog deferred from the 1.1.0 review.

Everything is additive. No public API was removed or changed.

Compatibility — read this first

npm install pdfnative-react@^1.2.0 pdfnative@^1.7.0 react@^19
Requirement 1.1.0 1.2.0
pdfnative (peer) ^1.6.0 ^1.7.0
React (peer) ^19.0.0 ^19.0.0
Node.js >=22 >=22

The peer floor moves because the four new chart kinds, layout.print and the
diagnostics channel do not exist before engine 1.7.0 — an older engine would
throw mid-render on the new authoring surface. doctor() now tells a 1.6.x
engine apart from a missing one and says exactly what to upgrade.

Rendering behaviour also inherits engine 1.7.0's correctness fixes, with no
code change here: RTL digit runs keep logical order, paired delimiters mirror
and Arabic/Persian letterforms join per UAX #9 (Arabic-script documents render
differently — and correctly), colour-emoji flag and ZWJ sequences resolve,
form documents gain a complete /ToUnicode map (their bytes change; form text
becomes searchable), and crowded chart x-labels are strided automatically
(labelStride: 1 restores the old draw-everything behaviour).

Highlights

Charts v2

Nine chart kinds — bar, barH, line, pie, donut, and now
stackedBar, stackedBarH, area and scatter — all still pure vector PDF
operators, PDF/A-safe, no chart library.

<Chart
    chartType="line"
    xAxis={{ type: 'time' }}
    series={[
        { label: 'Revenue (k€)', values: [210, 245, 262], xValues: ['2026-01', '2026-02', '2026-03'] },
        { label: 'Margin (%)', values: [12.1, 13.4, 15.2], xValues: ['2026-01', '2026-02', '2026-03'], yAxis: 'right' },
    ]}
    axis2={{ yMin: 0, yMax: 20 }}
    dataLabels={{ decimals: 1 }}
    altText="Revenue and margin both rise across Q1 2026."
/>

New surface: axis.scale ('linear' | 'log'), a secondary right axis
(axis2 + ChartSeries.yAxis), xAxis ('category' | 'linear' | 'time',
UTC-deterministic time ticks) with per-point ChartSeries.xValues, per-point
dataLabels, and x-label collision control (labelStride, labelRotation).
Full DocSpec and JSON-schema parity, as always — the compile-time
ChartPropsCoversChartBlock lock made the peer bump a build error until every
new engine field reached the props, the spec grammar and the schema.
Guide · sample

Print production

<Document
    print={{ bleed: 9, marks: true }}
    metadata={{ trapped: 'False' }}
    layout={{ viewerPreferences: { duplex: 'duplexFlipLongEdge', printPageRange: [[1, 4]] } }}
>

The print prop is document-level sugar in the exact mold of watermark /
header / footer / attachments / tagged: bleed/trim/art/crop boxes (or
the one-line bleed shorthand), vector printer's marks outside the TrimBox,
and large-format /UserUnit. metadata.trapped writes /Trapped with XMP
parity; the print-dialog viewer preferences (duplex, pickTrayByPDFSize,
printPageRange, numCopies) ride on layout.viewerPreferences; and
layout.outputIntent accepts a caller-supplied RGB ICC profile for tagged
output. Sample

The conformance channel

Engine 1.7.0 stops silently stamping PDF/A claims it cannot honour: an
unembedded font or a CMYK image under a PDF/A target now surfaces as a
diagnostic (PDFA_NO_FONT_ENTRIES, PDFA_UNEMBEDDED_FORM_FONT,
PDFA_DEVICE_CMYK_IMAGE). Reach it through any layout door:
layout.onDiagnostic receives diagnostics programmatically, and
layout.strict: true — the JSON-safe switch — escalates them to thrown
errors. lintDocument remains the pre-render tier; the channel sees what only
a render can see. Guide · sample

Linting: 18 → 25 rules

Thirteen rules now pre-empt an exception the engine would raise mid-render.
The three new chart rules (L_CHART_LOG_SCALE, L_CHART_X_AXIS,
L_CHART_LABELS) mirror every Charts v2 constraint; L_VIEWER_PRINT_RANGE
covers the print-dialog preferences; L_OUTPUT_INTENT_IGNORED and
L_TAGGED_FORM_FONTS warn about the two silent traps. L_PRINT_BOXES takes
a different approach worth naming: instead of re-stating the engine's
print-geometry rules, it calls the engine's own validatePrintOptions in a
try/catch and reports its message verbatim — zero duplicated rules, zero
drift, by construction. Guide

Proven PDF/A, not just declared: the veraPDF gate

npm run validate:pdfa

An 11-file corpus — rendered through the built package, across both authoring
doors and all four conformance targets, including this release's dual-axis
charts and print production under a PDF/A claim — is validated with the
pinned veraPDF reference validator, locally and as a
blocking CI and pre-publish gate. Two negative canaries the validator
must reject keep the gate honest: an unexpected pass is fatal, so a validator
that accepts everything can never turn CI green. Same hardened runner design
as the rest of the ecosystem (manifest-driven, fail-closed in CI,
SHA-256-verified installer). Guide

Agents that can see their work

The agent contract gains tier 5 — verifying the rendered output:
extractText for text truth, validatePdfUA/veraPDF for conformance truth,
and for vision-capable agents, rasterize and look — render, rasterize with
a standard external tool (pdftoppm or mutool; nothing is bundled), read
the PNG, and judge the page against your intent. Geometry tells you where
blocks landed; only looking tells you whether the page communicates.
Contract · sample

Server rendering: HTTP caching

return renderToResponse(<Invoice />, {
    cacheControl: 'private, max-age=60',
    etag: `"invoice-${id}-${updatedAt}"`,   // or etag: true to derive from the bytes
});

cacheControl and etag are opt-in; defaults are unchanged. etag: true
derives a strong validator from the rendered bytes (and therefore implies
buffering); a validator derived from your own data is cheaper because it lets
your handler answer If-None-Match with a 304 before rendering at all.
Guide

Errors carry their cause

PdfReactError and PdfStructureError accept the standard ES2022
ErrorOptions, so wrapped failures keep the original reachable via
error.cause. The JSON envelope is unchanged — a cause may hold
non-serializable state, so it deliberately stays out.

Under the hood

  • resolveFonts no longer corrupts font-embedded output. It emitted
    fontRef without the leading slash, so every document rendered through the
    documented font path carried BT latin 10 Tf where ISO 32000 requires the
    name /latin — malformed for conforming readers. The new PDF/UA
    round-trip test caught it: the engine's validatePdfUA was rejecting this
    package's own output, and the release's adversarial-review arbiter traced
    the failure to the missing slash by bisection. Hand-built fontEntries
    with a correct /F3-style ref were never affected; re-render anything
    produced via resolveFonts or the options.fonts map.
  • Unstreamable documents fail fast now. The engine's streaming path
    rejects <TableOfContents> and {pages} templates, but checked this inside
    the generator — so renderToResponse (streaming by default) failed
    mid-response, after the headers were sent. renderToStream now runs the
    engine's validateDocumentStreamable eagerly; every streaming entry throws
    a catchable error at call time.
  • setDeflateImpl joins initNodeCompression in the re-export surface, so
    layout.compress can produce real compression in browsers and workers
    (without an injected DEFLATE, the engine's fallback made output slightly
    larger). PdfColors is re-exported type-only.
  • doctor() probes the engine newest-first (validatePrintOptions, a 1.7.0
    marker, then estimateChartHeight, the 1.6.0 marker) for graded messages: a
    1.6.x engine is reported as 1.6.x — this release needs >= 1.7.0 instead of
    a generic failure.
  • eslint-plugin-react-hooks (v7, recommended-latest) now lints src/; the
    two deliberate latest-value-ref patterns in hooks.ts are documented with
    per-site justifications rather than rule-wide opt-outs.
  • New tests: deterministic structural fuzzing of validateSpec (seeded PRNG —
    it must never throw on arbitrary malformed input), and the PDF/UA
    round-trip deferred from the 1.1.0 review — render tagged output, then
    validate the finished bytes with the engine's validatePdfUA.
  • The publish workflow's Trusted Publishing fix (an OIDC-capable npm is
    installed and verified before npm publish) ships in a release for the
    first time — it landed on main right after v1.1.0, whose publish it
    un-broke.

What is deliberately not here

Engine 1.7.0's headline feature — complete PAdES LTV signing (B-B → B-LTA),
document timestamps, /DSS revocation material, multiple signatures — is
byte-level post-processing, and golden rule 7 keeps it out of an authoring
renderer on purpose. The same goes for PdfModifier.updateMetadata, form
fill/flatten, merge/split, text extraction and validatePdfUA (which this
release does use — in the test suite, on the bytes it renders).
docs/RECIPES.md shows how to run each of them with the
engine directly on the bytes this package produces, including the new LTV
ladder.

Validation

  • npm run typecheck:all · npm run lint · npm test (292 tests, 18
    files
    ) · npm run build — all green.
  • Coverage 95.0 % statements / 89.8 % branches / 97.8 % functions / 95.9 %
    lines, against thresholds raised this release to 90/84/92/90.
  • npm audit --omit=dev --audit-level=high: 0 vulnerabilities.
  • Two independent exhaustiveness audits walked the engine's 1.7.0 surface
    against this package in both directions (engine → renderer and
    field-by-field parity chains); every finding was fixed or dispositioned —
    the full table is in the PR draft.
  • The new samples were executed end to end and produce valid PDFs.
  • npm run corpus:pdfa: 11 PDF/A-claiming files (2 negative canaries)
    generated and self-checked (%PDF- magic, XMP claims match the manifest);
    full veraPDF validation runs blocking in CI with the pinned 1.30.2
    reference validator.

Full changelog

CHANGELOG.md — 1.2.0