跳到主要内容

PDF 导出

PDF 是设计稿最常见的交付格式 —— 预览、打印、印刷厂送印都离不开它。Ydesign 在三个层次上提供 PDF 导出能力:

  1. 客户端 store.saveAsPDF() — 最便捷,产物是栅格(raster)PDF
  2. 服务端 @ydesign/node — 无头浏览器方案,效果和客户端像素级一致(规划中)
  3. 服务端 @ydesign/pdf-export — 纯 Node 矢量 PDF,印刷级 CMYK / PDF/X-1a / 专色(更长期规划)

本文讲客户端导出的完整用法(已实现),并给出三种方式的选型指南。


快速上手

await store.saveAsPDF({
fileName: 'design.pdf',
pixelRatio: 2, // 高清
});

这一行代码就能导出一份 PDF。下面展开讲它在做什么、有哪些参数、出血怎么配、印刷级效果怎么出。


Raster 还是 Vector:先搞清楚你要哪种 PDF

这是选型的第一个问题。两种 PDF 不是"谁更好",而是"谁更合适"。

Raster PDF(栅格 PDF)

本质:每一页都是一张被压平的图片(JPEG/PNG)嵌入到 PDF 容器里。

优点

  • 效果 100% 和画布一致 —— 你看到什么,PDF 里就是什么
  • 实现简单,客户端即可生成
  • ✅ 复杂滤镜、渐变、阴影、动效截帧都能完美还原

缺点

  • ❌ 文件大(图片嵌入)
  • ❌ 分辨率固定(放大会糊)
  • ❌ 不符合专业印刷标准(印刷厂可能要求矢量)

实现方式

Vector PDF(矢量 PDF)

本质:文字保留为字形(glyph),形状保留为矢量路径,图片仍作位图内嵌。

优点

  • 分辨率无关,放大到 10 倍依然清晰
  • ✅ 文件体积小(没有大块栅格图像)
  • 印刷厂友好,可转 CMYK、支持专色
  • 不受浏览器 canvas 像素上限限制 —— 3m × 5m 的喷绘稿也能秒出

缺点

  • ❌ 实现复杂,底层要重写一套 "JSON → PDF 指令" 的渲染引擎
  • ❌ 某些前端特效(复杂合成阴影、部分滤镜)可能还原不完全
  • ❌ 必须在服务端运行(需要字体文件、GhostScript 等)

实现方式

  • 服务端:@ydesign/pdf-export 🚧 更长期规划

快速决策

场景用哪个
用户点"导出 PDF",立刻下载✅ 客户端 store.saveAsPDF()
批量后台导出、需要字体一致性@ydesign/node
超大尺寸喷绘(> 12000 px)@ydesign/node@ydesign/pdf-export
送印厂印刷(CMYK / 专色 / 烫金)@ydesign/pdf-export(矢量)
需要 PDF/X-1a 标准@ydesign/pdf-export(矢量)

客户端 store.saveAsPDF 完整用法

最小示例

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

默认:

  • pixelRatio: 1(按画布原始尺寸)
  • format: 'png' 嵌入 PDF
  • 单页 PDF(等于画布当前 scene)

完整参数

interface SaveAsPDFOptions {
/** 下载文件名,默认 'design.pdf' */
fileName?: string;

/** 渲染倍数(高清)。印刷建议 2-4 */
pixelRatio?: number;

/** 是否包含出血区。默认 false */
includeBleed?: boolean;

/** 裁切标记长度(像素)。设了就显示,不设就没有 */
cropMarkSize?: number;

/** 嵌入到 PDF 的图片格式 */
imageFormat?: 'png' | 'jpeg';

/** imageFormat='jpeg' 时的质量,0-1 */
imageQuality?: number;

/** 进度回调,值 0-1 */
onProgress?: (progress: number) => void;

/** 页面元数据(显示在 PDF 属性里) */
metadata?: {
title?: string;
author?: string;
subject?: string;
keywords?: string;
};
}

高清导出(印刷级)

印刷业标配是 300 DPI。有两种做法:

做法 1:画布就是印刷像素(所见即所得)

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

await store.saveAsPDF({
fileName: 'a4-300dpi.pdf',
pixelRatio: 1, // 画布已经是印刷尺寸,1× 够了
});

做法 2:画布按 72dpi,用 pixelRatio 放大

// 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
});

两种做法的详细对比见 大尺寸 & 高清导出

⚠️ pixelRatio 很高(比如 8+)时会占用大量浏览器内存,移动端可能崩溃。生产环境请看 选型建议

出血(Bleed)

出血是超出裁切边缘的预留区域,印刷切纸时吃点边也不会留白边。Ydesign 通过 store.bleed 设置:

// 设 3mm(约 35px @ 300dpi)出血
store.setBleed(35);

await store.saveAsPDF({
fileName: 'print.pdf',
includeBleed: true, // 导出时把出血区域也包含进 PDF
});

如果想在画布上直观看到出血范围,编辑器会用一圈虚线标出来(UI 内置)。

裁切标记(Crop Marks)

印刷厂用来定位裁切线的角标。只在 PDF 导出时生效:

await store.saveAsPDF({
fileName: 'print.pdf',
includeBleed: true,
cropMarkSize: 20, // 角标长度(px)
});

三者一起配合:

┌╴┐                              ┌╴┐
┊ ┊ <- 裁切标记(cropMark)
┊ ┊
┊ ┌───────────────────────┐ ┊
┊ │ │ ┊
┊ │ 画布内容区 │ ┊ <- 中间:画布
┊ │ │ ┊
┊ └───────────────────────┘ ┊
┊ ┊ <- 出血区(bleed)
┊─┘ ┊─┘

进度回调 & 取消

大画布 PDF 可能要几秒钟,给用户一个进度条体验会好很多:

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}>导出 PDF</Button>
<Modal open={exporting} footer={null} closable={false}>
<Progress percent={progress} />
</Modal>
</>
);
});

元数据

PDF 查看器里右键"属性"能看到的作者/标题等字段:

await store.saveAsPDF({
fileName: 'design.pdf',
metadata: {
title: '2026 春季产品目录',
author: '市场部',
subject: '内部使用',
keywords: '目录,春季,产品',
},
});

低层 API:store.toPDFDataURL

如果你不想立刻触发下载(比如需要先上传到自己的 OSS),可以用:

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

// 转成 Blob 走上传
const blob = await (await fetch(dataUrl)).blob();
await uploadToOSS(blob);

参数和 saveAsPDF 完全一致,只是不下载,返回 data:application/pdf;base64,... 字符串。


什么时候该切到服务端

客户端 saveAsPDF 不是万能的。遇到下面任一情况,建议走服务端:

① 画布太大,浏览器崩

// 3m × 5m @ 200dpi 的真实像素:23622 × 39370
// 远超浏览器 canvas 上限(约 12000px)
// 客户端导出会失败或输出全白 PDF

→ 走 @ydesign/node(无头浏览器自动处理大尺寸)或 @ydesign/pdf-export(矢量不受像素限制)。

② 批量生成(1 秒出 100 张)

客户端一次只能一个人导出,不能扩容。

→ 走 @ydesign/node + 队列 / Webhook,详见 Node 渲染文档

③ 移动端用户

移动端浏览器 canvas 内存很紧,300dpi 印刷级 PDF 导出很容易让 app 卡死。

→ 移动端统一走服务端,详见 自动挑选渲染策略

④ 送印厂,需要 CMYK / PDF/X-1a / 专色

客户端和服务端无头浏览器都输出 sRGB 栅格 PDF,印刷厂不一定认。真正专业印刷得用矢量 + 专色分版。

→ 走 @ydesign/pdf-export(矢量 PDF,更长期规划)。


服务端矢量 PDF(@ydesign/pdf-export,规划中)

🚧 这是 Ydesign 路线图上最长期的一个规划,优先级低于 @ydesign/node。下面内容为设计草稿,最终 API 可能会调整。

为什么要单独做一个矢量包?

回答之前先看一下 Polotno 的做法作为参考。它有三个包:

底层产物定位
polotno (前端)浏览器 canvasraster PDF客户端便捷导出
polotno-node无头 Chrome (Puppeteer)raster PDF服务端批量渲染
@polotno/pdf-export纯 Node + GhostScriptvector印刷级矢量 PDF / CMYK

我们 Ydesign 会做一样的分层

Ydesign 包底层产物对应 Polotno
@ydesign/react-editor浏览器 + Fabric.jsraster PDFpolotno 前端
@ydesign/node 🚧无头 Chrome + Fabric.jsraster PDFpolotno-node
@ydesign/pdf-export 🚧纯 Node,不跑浏览器vector@polotno/pdf-export

关键差异:前两个的 PDF 本质是"把画布截图塞进 PDF",而第三个是直接读 Fabric JSON 画矢量 PDF 指令。文字保留为字形、形状保留为矢量路径,印刷厂可以缩放到任何尺寸都不糊。

预期 API(草稿)

// 🚧 未发布
import { jsonToPDF } from '@ydesign/pdf-export';

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

// 基础矢量 PDF
await jsonToPDF(json, './output.pdf');

// 印刷级:PDF/X-1a + CMYK + 字体轮廓化
await jsonToPDF(json, './print.pdf', {
pdfx1a: true, // 需要机器装了 GhostScript
metadata: {
title: '2026 春季产品目录',
author: '市场部',
},
});

// 专色(烫金 / 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], // 打印机不支持专色时的回退值
},
},
});

环境依赖

  • Node.js ≥ 18
  • GhostScript(仅 PDF/X-1a 模式需要)
    • macOS: brew install ghostscript
    • Ubuntu: apt-get install ghostscript
    • Windows: ghostscript.com 下载

适用场景

场景建议
书籍封面 / 包装 / 海报,要送印厂@ydesign/pdf-export 矢量
名片 / 宣传单,普通喷墨打印⚠️ 客户端 saveAsPDF 通常就够了
烫金 / 镂空 / 特殊工艺✅ 矢量 PDF + 专色
超大喷绘(> 12000 px 单边)✅ 矢量 PDF(绕过 canvas 像素上限)

三种 PDF 导出对比

一张表回到你开头看到的三种方案:

维度客户端 saveAsPDF@ydesign/node 🚧@ydesign/pdf-export 🚧
运行位置浏览器Node 服务器Node 服务器
底层渲染浏览器 canvas无头 Chrome纯 JS,不跑浏览器
产物类型Raster PDFRaster PDFVector PDF
和画布一致性100%100%视效果略有差异
文件大小
放大清晰度固定分辨率固定分辨率无限放大
单张画布上限浏览器上限浏览器上限无上限
字体一致性依赖用户本地服务端显式注册服务端显式注册
CMYK / 专色
PDF/X-1a
开发状态✅ 已实现🚧 规划中🚧 长期规划
外部依赖ChromiumGhostScript (可选)

最佳实践

  1. 普通场景,客户端就够:用户点按钮立刻下载的场景,直接用 store.saveAsPDF
  2. 规划印刷工作流,画布就按印刷像素设:A4 设 2480 × 3508 (@300dpi) 比设 595 × 842 然后 pixelRatio: 4.2 更稳
  3. 出血提前配,别等导出时才加:设计阶段就调用 store.setBleed(35),用户能看到出血线,设计才会把背景延伸过去
  4. 进度回调体验:画布大时一定要给进度条,否则用户以为卡死
  5. 移动端、批量、大画布统一切服务端:不要硬让客户端扛
  6. 真正要印刷的物料走矢量:不要用 raster PDF 交印厂,字体缩放后会糊

常见问题

Q1:导出的 PDF 打开只有白页

几乎一定是 pixelRatio × 画布尺寸超过浏览器 canvas 上限了。降低 pixelRatio,或者走服务端。

Q2:文件太大了,一张 PDF 30MB

  • imageFormat'png' 改成 'jpeg',再配 imageQuality: 0.85
  • 或者:真的要小文件就上矢量 PDF(@ydesign/pdf-export

Q3:印刷厂说颜色不对

客户端 / 无头浏览器都是 sRGB 输出,印刷厂用 CMYK。两者色域不同,红色、绿色会明显变暗。这不是 Bug,是两种色彩空间的本质差异。

解决:

  • 要求印刷厂按 sRGB 印(部分数码印支持)
  • 自己做 sRGB → CMYK 预转(Photoshop / ColorSync),但可能还会走色
  • 最佳方案:上 @ydesign/pdf-export 的 PDF/X-1a CMYK 导出

Q4:PDF 里的文字是模糊的

客户端 PDF 本质是"图片嵌入",小字号 + 低 pixelRatio 就会糊。印刷字一定要:

  • pixelRatio ≥ 2
  • 或者用矢量 PDF(文字用 glyph 保留,永远清晰)

Q5:出血位没出来

检查三步:

  1. store.setBleed(35) 已调用
  2. saveAsPDF({ includeBleed: true }) 传了
  3. 设计稿的背景元素真的延伸到了出血区(用户没设计到出血区,导出来依然会是白边)

延伸阅读