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:
- Client-side
store.saveAsPDF()— the easiest path; output is a raster PDF - Server-side
@ydesign/node— headless browser, pixel-identical to the client (planned) - 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:
- Client:
store.saveAsPDF()✅ implemented - Server:
@ydesign/node'srenderer.jsonToPDF()🚧 planned
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
| Scenario | Use 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:
| Package | Engine | Output | Role |
|---|---|---|---|
polotno (frontend) | Browser canvas | raster PDF | Client-side convenience |
polotno-node | Headless Chrome (Puppeteer) | raster PDF | Server-side batch rendering |
@polotno/pdf-export | Pure Node + GhostScript | vector | Print-grade vector PDF / CMYK |
Ydesign follows the same layered approach:
| Ydesign package | Engine | Output | Polotno counterpart |
|---|---|---|---|
@ydesign/react-editor | Browser + Fabric.js | raster PDF | polotno frontend |
@ydesign/node 🚧 | Headless Chrome + Fabric.js | raster PDF | polotno-node |
@ydesign/pdf-export 🚧 | Pure Node, no browser | vector | @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
- macOS:
When it fits
| Scenario | Recommendation |
|---|---|
| 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:
| Dimension | Client saveAsPDF | @ydesign/node 🚧 | @ydesign/pdf-export 🚧 |
|---|---|---|---|
| Runs on | Browser | Node server | Node server |
| Underlying engine | Browser canvas | Headless Chrome | Pure JS, no browser |
| Output type | Raster PDF | Raster PDF | Vector PDF |
| Matches canvas | 100% | 100% | Slight variance possible |
| File size | Large | Large | Small |
| Scaling clarity | Fixed | Fixed | Infinite |
| Per-canvas ceiling | Browser limit | Browser limit | No limit |
| Font consistency | Local user font | Server-registered | Server-registered |
| CMYK / spot colors | ❌ | ❌ | ✅ |
| PDF/X-1a | ❌ | ❌ | ✅ |
| Status | ✅ shipped | 🚧 planned | 🚧 long-term |
| External dependencies | None | Chromium | GhostScript (optional) |
Best practices
- Client-side is enough for typical cases — user clicks, file downloads. Just
store.saveAsPDF. - For print workflows, set the canvas at print pixels: A4 at
2480 × 3508(@300dpi) is more reliable than595 × 842+pixelRatio: 4.2. - 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. - Always give a progress bar on large canvases — otherwise users think it's stuck.
- Mobile, batch, and large canvases all go server-side. Don't try to force the client.
- 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
imageFormatfrom'png'to'jpeg'withimageQuality: 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-exportwith 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:
- Did you call
store.setBleed(35)? - Did you pass
saveAsPDF({ includeBleed: true })? - 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
- 👉 Large Format & HD Exports — full discussion of
pixelRatioand server-side selection - 👉 Server-side image generation — full
@ydesign/nodedeployment guide - 👉 Cloud Render API — hosted server-side rendering
- 👉 Units & measurements — DPI / pixelRatio relationships
- 👉 Scenes & import/export — PNG / JPEG / WebP export
- 👉 Font consistency — server-side font configuration