Skip to main content

PDF Export

PDF is the most common delivery format for designs — preview, print, and the print shop all rely on it. Ydesign offers PDF export at three layers:

  1. Client-side store.saveAsPDF() — the easiest path; output is a raster PDF
  2. Server-side @ydesign/node — headless browser, pixel-identical to the client (planned)
  3. Server-side @ydesign/pdf-export — pure Node vector PDF, print-grade CMYK / PDF/X-1a / spot colors (longer-term)

This article covers the client-side export in full (implemented today) and provides a selection guide for the three approaches.


Quick start

await store.saveAsPDF({
fileName: 'design.pdf',
pixelRatio: 2, // high-res
});

That's all you need. Below: what it does under the hood, what options exist, how to configure bleed, and how to hit print-grade quality.


Raster or Vector: figure this out first

This is the first selection decision. The two aren't "better vs worse" — they're "right for the job vs not".

Raster PDF

Essence: Each page is a flattened image (JPEG/PNG) embedded in a PDF container.

Pros:

  • 100% identical to the canvas — what you see is what you get
  • Simple to implement, generated client-side
  • ✅ Complex filters, gradients, shadows, animated frames all render perfectly

Cons:

  • ❌ Large files (images are embedded)
  • ❌ Fixed resolution (zooming blurs)
  • ❌ Doesn't meet professional print standards (print shops often want vector)

Where:

Vector PDF

Essence: Text stays as glyphs, shapes stay as vector paths, images remain embedded bitmaps.

Pros:

  • Resolution-independent — crisp at 10× zoom
  • ✅ Smaller files (no large raster blobs)
  • Print-shop friendly — CMYK conversion, spot colors supported
  • Not bound by the browser canvas pixel ceiling — 3m × 5m prints in seconds

Cons:

  • ❌ Complex to implement — it's a whole "JSON → PDF instructions" renderer under the hood
  • ❌ Some front-end effects (compound composite shadows, some filters) may not translate 100%
  • ❌ Must run server-side (needs font files, GhostScript, etc.)

Where:

  • Server: @ydesign/pdf-export 🚧 longer-term plan

Quick decision

ScenarioUse this
User hits "Export PDF" and downloads right away✅ Client-side store.saveAsPDF()
Batch backend export, font consistency required@ydesign/node
Very large sizes (> 12000 px)@ydesign/node or @ydesign/pdf-export
Sending to print shop (CMYK / spot / foil)@ydesign/pdf-export (vector)
Need PDF/X-1a compliance@ydesign/pdf-export (vector)

Client-side store.saveAsPDF in detail

Minimal example

await store.saveAsPDF({ fileName: 'design.pdf' });

Defaults:

  • pixelRatio: 1 (original canvas size)
  • format: 'png' for embedded images
  • Single-page PDF (the canvas' current scene)

Full options

interface SaveAsPDFOptions {
/** Download file name, default 'design.pdf' */
fileName?: string;

/** Render multiplier (HD). Print recommends 2-4 */
pixelRatio?: number;

/** Include the bleed area. Default false */
includeBleed?: boolean;

/** Crop mark length (px). Set it to show marks, omit to disable */
cropMarkSize?: number;

/** Embedded image format */
imageFormat?: 'png' | 'jpeg';

/** Quality when imageFormat='jpeg', 0-1 */
imageQuality?: number;

/** Progress callback, value 0-1 */
onProgress?: (progress: number) => void;

/** Document metadata (visible in PDF properties) */
metadata?: {
title?: string;
author?: string;
subject?: string;
keywords?: string;
};
}

HD export (print-grade)

Print industry standard is 300 DPI. Two ways to get there:

Option 1: canvas uses print pixels directly (WYSIWYG)

// A4 @ 300dpi = 2480 × 3508
store.setSize({ width: 2480, height: 3508 });

await store.saveAsPDF({
fileName: 'a4-300dpi.pdf',
pixelRatio: 1, // The canvas is already at print size, 1× is enough
});

Option 2: 72dpi canvas + pixelRatio upscaling

// A4 @ 72dpi = 595 × 842
store.setSize({ width: 595, height: 842 });

await store.saveAsPDF({
fileName: 'a4-300dpi.pdf',
pixelRatio: 4.2, // 4.2 × 72 ≈ 300 dpi
});

Detailed trade-offs: see Large Format & HD Exports.

⚠️ Very high pixelRatio (8+) eats significant browser memory and may crash mobile. For production see when to move server-side.

Bleed

Bleed is the extra area beyond the trim edge — it prevents white strips at the cut. Set via store.setBleed:

// 3mm bleed (≈ 35px @ 300dpi)
store.setBleed(35);

await store.saveAsPDF({
fileName: 'print.pdf',
includeBleed: true, // Include the bleed area in the exported PDF
});

The editor shows the bleed range as a dashed outline on the canvas (built-in UI).

Crop marks

The corner marks a print shop uses to locate the trim line. Only produced in PDF output:

await store.saveAsPDF({
fileName: 'print.pdf',
includeBleed: true,
cropMarkSize: 20, // Corner length (px)
});

All three working together:

┌╴┐                              ┌╴┐
┊ ┊ <- crop marks
┊ ┊
┊ ┌───────────────────────┐ ┊
┊ │ │ ┊
┊ │ canvas content │ ┊ <- center: canvas
┊ │ │ ┊
┊ └───────────────────────┘ ┊
┊ ┊ <- bleed area
┊─┘ ┊─┘

Progress callback & cancellation

Large canvas PDFs can take a few seconds. A progress bar massively improves UX:

import { Progress, Modal } from 'antd';
import { observer } from 'mobx-react-lite';
import { useState } from 'react';

export const ExportPDFButton = observer(({ store }) => {
const [progress, setProgress] = useState(0);
const [exporting, setExporting] = useState(false);

const handleExport = async () => {
setExporting(true);
setProgress(0);
try {
await store.saveAsPDF({
fileName: 'design.pdf',
pixelRatio: 3,
onProgress: p => setProgress(Math.round(p * 100)),
});
} finally {
setExporting(false);
}
};

return (
<>
<Button onClick={handleExport}>Export PDF</Button>
<Modal open={exporting} footer={null} closable={false}>
<Progress percent={progress} />
</Modal>
</>
);
});

Metadata

The author/title fields visible in a PDF viewer's "Properties" dialog:

await store.saveAsPDF({
fileName: 'design.pdf',
metadata: {
title: 'Spring 2026 Product Catalog',
author: 'Marketing',
subject: 'Internal use',
keywords: 'catalog,spring,products',
},
});

Lower-level API: store.toPDFDataURL

If you don't want to trigger a download immediately (e.g. need to upload to your own OSS first):

const dataUrl = await store.toPDFDataURL({
pixelRatio: 2,
includeBleed: true,
});
// dataUrl = 'data:application/pdf;base64,...'

// Convert to Blob to upload
const blob = await (await fetch(dataUrl)).blob();
await uploadToOSS(blob);

Same options as saveAsPDF, just no download — you get a data:application/pdf;base64,... string back.


When to switch to server-side

Client-side saveAsPDF isn't a silver bullet. If any of the following apply, go server-side:

① Canvas too large, browser crashes

// 3m × 5m @ 200dpi → real pixels 23622 × 39370
// Far beyond the browser canvas ceiling (~12000px)
// Client-side export either fails or produces a blank PDF

→ Use @ydesign/node (headless browser handles large canvases) or @ydesign/pdf-export (vector, no pixel ceiling).

② Batch generation (100 in a second)

Client-side only renders one for one user — you can't scale it.

@ydesign/node + queue / Webhook. See the Node render doc.

③ Mobile users

Mobile browser canvas memory is tight; 300dpi print-grade PDFs will freeze the app.

→ Mobile should universally go server-side. See auto-pick strategy.

④ Going to print with CMYK / PDF/X-1a / spot colors

Both client-side and headless-browser outputs are sRGB raster PDFs, which print shops may reject. Real professional print wants vector + channel separation.

@ydesign/pdf-export (vector PDF, longer-term plan).


Server-side vector PDF (@ydesign/pdf-export, planned)

🚧 This is the longest-term item on the Ydesign roadmap, lower priority than @ydesign/node. What follows is a design draft — the final API may evolve.

Why a separate vector package?

Before answering, look at how Polotno structured things. They ship three packages:

PackageEngineOutputRole
polotno (frontend)Browser canvasraster PDFClient-side convenience
polotno-nodeHeadless Chrome (Puppeteer)raster PDFServer-side batch rendering
@polotno/pdf-exportPure Node + GhostScriptvectorPrint-grade vector PDF / CMYK

Ydesign follows the same layered approach:

Ydesign packageEngineOutputPolotno counterpart
@ydesign/react-editorBrowser + Fabric.jsraster PDFpolotno frontend
@ydesign/node 🚧Headless Chrome + Fabric.jsraster PDFpolotno-node
@ydesign/pdf-export 🚧Pure Node, no browservector@polotno/pdf-export

Key difference: the first two produce PDFs by "screenshotting the canvas into a PDF". The third reads Fabric JSON directly and emits vector PDF instructions. Text is preserved as glyphs, shapes as vector paths — the print shop can scale to any size without blur.

Expected API (draft)

// 🚧 Not published
import { jsonToPDF } from '@ydesign/pdf-export';

const json = JSON.parse(await fs.readFile('./design.json', 'utf-8'));

// Basic vector PDF
await jsonToPDF(json, './output.pdf');

// Print-grade: PDF/X-1a + CMYK + font outlining
await jsonToPDF(json, './print.pdf', {
pdfx1a: true, // Requires GhostScript installed on the host
metadata: {
title: 'Spring 2026 Product Catalog',
author: 'Marketing',
},
});

// Spot colors (gold foil / Pantone)
await jsonToPDF(json, './foil-cover.pdf', {
pdfx1a: true,
spotColors: {
'#FFD700': {
name: 'Gold Foil',
type: 'pantone',
pantoneCode: 'Pantone 871 C',
cmyk: [0, 0.15, 0.5, 0], // Fallback for printers that don't support spot
},
},
});

Environment

  • Node.js ≥ 18
  • GhostScript (only required for PDF/X-1a mode)
    • macOS: brew install ghostscript
    • Ubuntu: apt-get install ghostscript
    • Windows: download from ghostscript.com

When it fits

ScenarioRecommendation
Book covers / packaging / posters for print shop@ydesign/pdf-export vector
Business cards / flyers on a standard inkjet⚠️ Client-side saveAsPDF is usually fine
Foil / die-cut / special finishes✅ Vector PDF + spot colors
Very large outdoor prints (> 12000 px)✅ Vector PDF (bypasses pixel ceiling)

Three PDF approaches compared

Bringing back the opening table, filled in:

DimensionClient saveAsPDF@ydesign/node 🚧@ydesign/pdf-export 🚧
Runs onBrowserNode serverNode server
Underlying engineBrowser canvasHeadless ChromePure JS, no browser
Output typeRaster PDFRaster PDFVector PDF
Matches canvas100%100%Slight variance possible
File sizeLargeLargeSmall
Scaling clarityFixedFixedInfinite
Per-canvas ceilingBrowser limitBrowser limitNo limit
Font consistencyLocal user fontServer-registeredServer-registered
CMYK / spot colors
PDF/X-1a
Status✅ shipped🚧 planned🚧 long-term
External dependenciesNoneChromiumGhostScript (optional)

Best practices

  1. Client-side is enough for typical cases — user clicks, file downloads. Just store.saveAsPDF.
  2. For print workflows, set the canvas at print pixels: A4 at 2480 × 3508 (@300dpi) is more reliable than 595 × 842 + pixelRatio: 4.2.
  3. Configure bleed at design time, not at export time: call store.setBleed(35) during design so the user sees the bleed line and extends backgrounds into it.
  4. Always give a progress bar on large canvases — otherwise users think it's stuck.
  5. Mobile, batch, and large canvases all go server-side. Don't try to force the client.
  6. Real print-bound output goes vector. Don't hand raster PDFs to a print shop — text blurs when scaled.

FAQ

Q1: Exported PDF is blank white pages

Almost always pixelRatio × canvas size exceeding the browser canvas ceiling. Lower pixelRatio, or move server-side.

Q2: The file is huge — 30MB per PDF

  • Switch imageFormat from 'png' to 'jpeg' with imageQuality: 0.85
  • Or go vector PDF for truly small files (@ydesign/pdf-export)

Q3: The print shop says the colors are wrong

Client-side / headless-browser outputs are sRGB. Print shops use CMYK. Different gamuts mean reds and greens noticeably darken. Not a bug — it's the color space difference.

Options:

  • Ask the print shop to print in sRGB (some digital presses support this)
  • Do sRGB → CMYK yourself (Photoshop / ColorSync), though shift can still happen
  • Best: export via @ydesign/pdf-export with PDF/X-1a CMYK

Q4: Text in the PDF is fuzzy

Client-side PDF is image-embedded. Small font size + low pixelRatio will blur. For print-ready text:

  • pixelRatio ≥ 2
  • Or use vector PDF (text stays as glyphs, always crisp)

Q5: Bleed didn't show up

Check three things:

  1. Did you call store.setBleed(35)?
  2. Did you pass saveAsPDF({ includeBleed: true })?
  3. Did the design actually extend the background into the bleed area? (If the user didn't design into it, the export still looks like a white border.)

Further reading