@polotno/pdf-export
v0.11.0
Published
Convert Polotno JSON into vector PDFs in browsers and Node.js, with custom-font embedding and natively written PDF/X-4 and PDF/X-1a print output
Readme
@polotno/pdf-export
Export Polotno design JSON as a vector PDF. Text stays selectable, shapes stay vector, and custom fonts are embedded.
The package supports regular PDF and PDF/X-4. It also supports the legacy PDF/X-1a format. All modes work in Node.js and browsers without Ghostscript.
Runtime: Node.js 22.13 or later, or a modern browser.
License: Non-production use is free, with no time limit. Production use requires a Polotno subscription. Read the license.
Install
npm install @polotno/pdf-exportUsage
Use the default entry in Node and the browser entry in browsers.
import { jsonToPDFBytes } from '@polotno/pdf-export'; // Node
import { jsonToPDFBytes } from '@polotno/pdf-export/browser'; // browser
const bytes = await jsonToPDFBytes(json); // Uint8Array, both runtimesBoth entries accept the same export options. blockPrivateNetwork and
fetchGuard are Node-only.
All export functions accept metadata with title, author, application,
and producer fields.
Node.js
import { jsonToPDF } from '@polotno/pdf-export';
await jsonToPDF(json, './output.pdf'); // writes the fileBrowser
import { jsonToPDFBlob } from '@polotno/pdf-export/browser';
const blob = await jsonToPDFBlob(store.toJSON()); // application/pdfPrint export
| Mode | Option | Use | Transparency |
| ----------------- | -------------- | ------------------------- | ------------------------------------------- |
| Regular PDF | Omit pdfx | General vector PDF | Preserved |
| PDF/X-4 | pdfx: 'x-4' | Preferred print format | Preserved |
| PDF/X-1a (legacy) | pdfx: 'x-1a' | Vendors that require X-1a | Transparent pages become opaque CMYK images |
PDF/X-4 is the preferred print format. If your vendor requires PDF/X-1a, use
that format. Both PDF/X modes need an ICC output intent. Ask your vendor for
the correct profile and identifier. The deprecated pdfx1a: true option still
selects PDF/X-1a, but it cannot be combined with pdfx.
import { readFile } from 'node:fs/promises';
const profile = new Uint8Array(await readFile('./CoatedFOGRA39.icc'));
await jsonToPDF(json, './print-ready.pdf', {
pdfx: 'x-4',
outputIntent: { profile, identifier: 'FOGRA39' },
colorMode: 'cmyk', // optional for X-4; default is 'preserve-rgb'
});Both runtimes accept profile bytes. Fetch the bytes in a browser:
const response = await fetch('/CoatedFOGRA39.icc');
const profile = new Uint8Array(await response.arrayBuffer());Common European profiles include Coated FOGRA39 and PSO Coated v3. GRACoL is common in North America.
PDF/X-4 preserves ICC-based RGB by default. colorMode: 'cmyk' converts fills,
strokes, and gradients through the output intent. Raster images stay ICC-based
RGB. Pure RGB black can become rich black. The exporter has no black-generation
option.
PDF/X-1a is always CMYK and does not accept colorMode. It handles each page
as follows:
- An opaque page stays vector, with selectable text and spot separations.
- A transparent process-color page becomes one opaque CMYK image. The
pdfxRasterDpioption sets its resolution and defaults to 300. - A page that combines transparency with a spot color causes an error.
Transparency includes opacity, shadows, soft image alpha, SVG masks, and
transparent gradients. onWarning reports each flattened page and lost
process-color overprint. The default handler uses console.warn.
The color engine loads only for PDF/X. The X-1a rasterizer loads only for
transparent pages. Both components are included.
Node flattening requires Node.js 22.13 or later. Browser flattening requires
OffscreenCanvas (Safari 16.4 or later). A strict browser CSP must allow
script-src 'wasm-unsafe-eval' for CMYK conversion.
Spot colors and overprint
Map a design color to a named separation ink:
await jsonToPDF(json, './output.pdf', {
pdfx: 'x-4',
outputIntent: { profile, identifier: 'FOGRA39' },
spotColors: {
'rgba(255,215,0,1)': {
name: 'Gold Foil',
cmyk: [0, 0.15, 0.5, 0], // fallback for viewers without spot support
overprint: true,
},
},
});Colors match by value, not syntax or alpha. For example, #FFD700 and
rgb(255,215,0) select the same ink. Text, lines, figures, and SVG elements
support spot colors. The cmyk process-color fallback uses values from 0 to
1.
overprint: true prints the spot ink over the process colors. This setting can
prevent white halos around a foil or varnish plate.
PDF/X-4 preserves spot-color transparency. PDF/X-1a rejects a page that
combines a spot color with transparency. SVG masks and clip paths count as
transparency. line and table colors ignore alpha. Use element opacity
for these elements.
Layout and image detail
Set the bleed in pixels on each page. Then include it during export:
const json = {
pages: [{ bleed: 36, children: [] }],
};
await jsonToPDF(json, './print-ready.pdf', {
includeBleed: true,
cropMarkSize: 18,
});bleed sets all four sides. bleedTop, bleedRight, bleedBottom, and
bleedLeft override individual sides. A value of 0 disables that side.
cropMarkSize reserves a separate margin and draws crop marks at the trim edge.
Element coordinates stay relative to the trim corner. Page backgrounds extend
through the bleed area.
dpi converts design pixels to PDF points. The option overrides the design DPI,
which defaults to 72. The conversion is points = pixels * 72 / dpi.
imagePpi limits the raster detail per printed inch. It defaults to 300, the
print-shop standard. Use 450 for premium stock or 150 for proofs. This option
only removes pixels.
Set imagePpi for the output medium, not for a target file size. A lower value
can create a larger file because a JPEG below the limit embeds unchanged.
imageQuality sets the JPEG quality (0 to 1) for photos the exporter must
decode — a cropped, filtered, or downsampled photo. It defaults to 0.85.
Only opaque images with a JPEG source re-encode as JPEG. PNG art, transparent
images, and unmodified pass-through JPEGs never do. Set imageQuality: 1 to
embed decoded rasters lossless instead; expect several times larger files.
An untouched crop or aspect fit does not decode the photo at all: the exporter embeds the source bytes once and writes the crop into the page. Different crops of one photo share a single embedded image.
A PDF/X file also embeds the outputIntent ICC profile you pass. A large
press profile (ECI FOGRA39 is 1.8 MB, about 1.4 MB compressed) sets the
minimum size of every file. Your print vendor can supply a smaller profile
when file size matters.
Fonts and text
The exporter embeds design fonts and Google Fonts as subsets. Regular PDFs can
keep standard PDF fonts unembedded. When a font lacks a glyph, the exporter
selects a fallback by writing system. Set fallbackFont to choose the family.
Color emoji render in black and white.
A font that cannot load causes a FONT_FAILED error. Set skipFontError: true
to substitute fallbackFont, or Helvetica when no fallback is set. A regular
PDF draws a missing-glyph box when no font covers a character; PDF/X fails.
PDF/X embeds every font. It replaces Arial, Times New Roman, and Courier with Arimo, Tinos, and Cousine. Symbol and ZapfDingbats require a registered font file.
Text that is taller than its box shrinks by default. Set
textOverflow: 'resize' to keep the authored font size and natural text height.
The exporter does not support 'ellipsis'.
Progress and cancellation
const controller = new AbortController();
await jsonToPDF(json, './output.pdf', {
onProgress: (progress) => console.log(progress),
signal: controller.signal,
});onProgress receives a value from 0 to 1. The value never decreases. It
measures completed work, not elapsed time. Errors from this callback are logged.
signal cancels the export before the final write and leaves no partial output
file. A later abort is ignored. An active response stream can finish first.
Shared font downloads cannot be canceled.
Assets and network access
The browser fetches remote images, fonts, and SVG sources during export. These
assets require CORS access. Google Fonts works by default. Other assets must
send Access-Control-Allow-Origin or use a data: URL.
Node.js blocks cloud-metadata addresses by default. Use
blockPrivateNetwork: true to also block loopback and private addresses. Use
fetchGuard to replace these rules with your own URL policy. The exporter
applies the policy to every redirect.
// Use the built-in strict policy.
await jsonToPDF(json, './output.pdf', {
blockPrivateNetwork: true,
});
// Or replace the built-in policy.
await jsonToPDF(json, './output.pdf', {
fetchGuard: (url) => isAllowed(url),
});Errors
Export failures are plain Error objects with a stable code and structured
details. Supported codes are DESIGN_INVALID, IMAGE_FAILED, FONT_FAILED,
FETCH_FAILED, and EXPORT_FAILED. Use code instead of the message text in
application logic.
