Generate native PDF files from JSON or TypeScript. No headless Chromium, no wkhtmltopdf, no HTML conversion — a custom PDF object model with Yoga flex layout, fontkit / HarfBuzz text shaping, and a progressive capability pipeline.
npm install @paperjsx/json-to-pdfRequires Node.js >=18. ESM only.
import { PdfEngine } from "@paperjsx/json-to-pdf";
import { writeFileSync } from "node:fs";
const buffer = await PdfEngine.render({
meta: { title: "Monthly Update", author: "Acme Inc." },
page: { size: "Letter", margin: 48 },
children: [
{ type: "heading", value: "Monthly Update", level: 1 },
{ type: "paragraph", value: "Revenue grew 18% month over month." },
{
type: "table",
columns: [{ width: 120 }, { width: 80, align: "right" }],
rows: [
{ isHeader: true, cells: [{ value: "Region" }, { value: "Revenue" }] },
{ cells: [{ value: "North America" }, { value: "$5.1M" }] },
{ cells: [{ value: "Europe" }, { value: "$3.6M" }] },
],
},
],
});
writeFileSync("update.pdf", buffer);- Custom PDF object model — full control over PDF output. No dependency on
pdfkitorpdf-libat runtime. - Flexbox layout — Yoga-based structured layout with widow/orphan control and Knuth-Plass-style line breaking.
- Text shaping — fontkit for metrics, HarfBuzz (pro) for emoji / RTL / complex scripts,
subset-fontto shrink embedded fonts to used glyphs only. - Tables with spans — row spans, column spans, cross-page continuation, header repeat.
- Graphics — solid fills, gradients, strokes, RGB + CMYK color, SVG paths parsed into PDF draw commands.
- Interactive features — forms, annotations, TOC, bookmarks, named destinations.
- Accessibility — tagged PDF structure for WCAG compliance (headings, lists, tables marked up for screen readers).
- PDF/A-2a — ICC profile embedding and XMP metadata for long-term archival (pro).
- Streaming —
renderStream()emits PDF bytes as they're produced for large documents. - Digital signatures — OpenSSL-based signing with timestamp support (pro).
- Deterministic output — same input, byte-identical PDF.
import type { PdfDocument } from "@paperjsx/json-to-pdf";
const doc: PdfDocument = {
meta: { title, author, subject, keywords },
page: {
size: "Letter", // "Letter" | "A4" | "Legal" | { width, height } (pt)
orientation: "portrait",
margin: 48, // or { top, right, bottom, left }
},
defaultFont: "Helvetica",
children: [ /* PdfNode[] */ ],
};Elements: heading, paragraph, list, table, image, page-break, divider, container, group, svg, form-field, annotation, toc.
The engine auto-detects which capability phases are needed (simple docs skip table / interactive / accessibility / PDF/A pipelines), so minimal documents render on a minimal codepath.
For large documents, stream bytes as they're produced instead of buffering the entire PDF in memory:
import { PdfEngine } from "@paperjsx/json-to-pdf";
import { createWriteStream } from "node:fs";
const out = createWriteStream("large-report.pdf");
await PdfEngine.renderStream(doc, {
onChunk: (chunk) => out.write(chunk),
onEnd: () => out.end(),
});{
type: "table",
columns: [
{ width: 200 },
{ width: 100, align: "right" },
{ width: 100, align: "right" },
],
rows: [
{
isHeader: true,
cells: [{ value: "Item" }, { value: "Qty" }, { value: "Total" }],
},
{
cells: [
{ value: "Enterprise License" },
{ value: "1" },
{ value: "$12,000.00" },
],
},
],
style: { borderColor: "#E5E7EB", headerBackground: "#F3F4F6" },
}Tables longer than a page continue automatically with the header row repeated. colSpan and rowSpan are validated against the declared column count before rendering.
import { PdfEngine } from "@paperjsx/json-to-pdf";
import { readFileSync } from "node:fs";
await PdfEngine.render({
...doc,
fonts: [
{ family: "Inter", weight: 400, source: readFileSync("./Inter-Regular.ttf") },
{ family: "Inter", weight: 700, source: readFileSync("./Inter-Bold.ttf") },
],
});Fonts are subset to only the glyphs actually used in the document, typically reducing PDF size by 80–95%. Standard 14 PDF fonts (Helvetica, Times, Courier, etc.) are built in and do not need to be supplied.
{ type: "image", src: "./logo.png", width: 200, alt: "Acme logo" }
{ type: "image", src: "https://example.com/chart.jpg", width: 400 }
{ type: "image", src: "data:image/png;base64,...", width: 300 }PNG and JPEG are decoded natively. Remote URLs pass through an SSRF guard (private IP blocklist, scheme allowlist, DNS timeout, 50MB data-URL cap).
PdfEngine.render(doc, options?) // Uint8Array
PdfEngine.renderStream(doc, handlers) // chunked output
// Validation / phase detection
validatePdfDocument(doc)
isPhase3Document(doc), containsTableNode(doc), containsFormNode(doc)
// Utilities
registerFont({ family, weight, source })
parseSvgPath(d) // SVG "d" → PDF command array
// Determinism
setDeterministicMode(true)Full type surface in dist/index.d.ts.
import { setDeterministicMode } from "@paperjsx/json-to-pdf";
setDeterministicMode(true);Freezes creation timestamps, document IDs, and any other sources of nondeterminism. Verified with a byte-equality test suite.
import { PdfError } from "@paperjsx/json-to-pdf";
try {
await PdfEngine.render(doc);
} catch (err) {
if (err instanceof PdfError) {
console.error(err.code, err.phase, err.message);
}
throw err;
}@paperjsx/json-to-pdf-pro adds:
- commercial / self-hosted production license
- HarfBuzz text shaping (RTL, complex scripts, emoji)
- PDF/A-2a compliance (ICC profiles, XMP metadata)
- digital signatures with timestamping
- advanced typography (hyphenation, justification, OpenType features)
- validation & repair tooling (XRef rebuild, stream length fixing)
- PDF form fill
The API is identical — swap the import and provide PAPERJSX_LICENSE_KEY.
- Docs: paperjsx.com/docs
- Playground: paperjsx.com/playground
- Pricing: paperjsx.com/pricing
Apache-2.0. See LICENSE.