PDF 导出
PDF 是设计稿最常见的交付格式 —— 预览、打印、印刷厂送印都离不开它。Ydesign 在三个层次上提供 PDF 导出能力:
- 客户端
store.saveAsPDF()— 最便捷,产物是栅格(raster)PDF - 服务端
@ydesign/node— 无头浏览器方案,效果和客户端像素级一致(规划中) - 服务端
@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 里就是什么
- ✅ 实现简单,客户端即可生成
- ✅ 复杂滤镜、渐变、阴影、动效截帧都能完美还原
缺点:
- ❌ 文件大(图片嵌入)
- ❌ 分辨率固定(放大会糊)
- ❌ 不符合专业印刷标准(印刷厂可能要求矢量)
实现方式:
- 客户端:
store.saveAsPDF()✅ 已实现 - 服务端:
@ydesign/node的renderer.jsonToPDF()🚧 规划中
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 (前端) | 浏览器 canvas | raster PDF | 客户端便捷导出 |
polotno-node | 无头 Chrome (Puppeteer) | raster PDF | 服务端批量渲染 |
@polotno/pdf-export | 纯 Node + GhostScript | vector | 印刷级矢量 PDF / CMYK |
我们 Ydesign 会做一样的分层:
| Ydesign 包 | 底层 | 产物 | 对应 Polotno |
|---|---|---|---|
@ydesign/react-editor | 浏览器 + Fabric.js | raster PDF | polotno 前端 |
@ydesign/node 🚧 | 无头 Chrome + Fabric.js | raster PDF | polotno-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 下载
- macOS:
适用场景
| 场景 | 建议 |
|---|---|
| 书籍封面 / 包装 / 海报,要送印厂 | ✅ @ydesign/pdf-export 矢量 |
| 名片 / 宣传单,普通喷墨打印 | ⚠️ 客户端 saveAsPDF 通常就够了 |
| 烫金 / 镂空 / 特殊工艺 | ✅ 矢量 PDF + 专色 |
| 超大喷绘(> 12000 px 单边) | ✅ 矢量 PDF(绕过 canvas 像素上限) |
三种 PDF 导出对比
一张表回到你开头看到的三种方案:
| 维度 | 客户端 saveAsPDF | @ydesign/node 🚧 | @ydesign/pdf-export 🚧 |
|---|---|---|---|
| 运行位置 | 浏览器 | Node 服务器 | Node 服务器 |
| 底层渲染 | 浏览器 canvas | 无头 Chrome | 纯 JS,不跑浏览器 |
| 产物类型 | Raster PDF | Raster PDF | Vector PDF |
| 和画布一致性 | 100% | 100% | 视效果略有差异 |
| 文件大小 | 大 | 大 | 小 |
| 放大清晰度 | 固定分辨率 | 固定分辨率 | 无限放大 |
| 单张画布上限 | 浏览器上限 | 浏览器上限 | 无上限 |
| 字体一致性 | 依赖用户本地 | 服务端显式注册 | 服务端显式注册 |
| CMYK / 专色 | ❌ | ❌ | ✅ |
| PDF/X-1a | ❌ | ❌ | ✅ |
| 开发状态 | ✅ 已实现 | 🚧 规划中 | 🚧 长期规划 |
| 外部依赖 | 无 | Chromium | GhostScript (可选) |
最佳实践
- 普通场景,客户端就够:用户点按钮立刻下载的场景,直接用
store.saveAsPDF - 规划印刷工作流,画布就按印刷像素设:A4 设
2480 × 3508(@300dpi) 比设595 × 842然后pixelRatio: 4.2更稳 - 出血提前配,别等导出时才加:设计阶段就调用
store.setBleed(35),用户能看到出血线,设计才会把背景延伸过去 - 进度回调体验:画布大时一定要给进度条,否则用户以为卡死
- 移动端、批量、大画布统一切服务端:不要硬让客户端扛
- 真正要印刷的物料走矢量:不要用 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:出血位没出来
检查三步:
store.setBleed(35)已调用saveAsPDF({ includeBleed: true })传了- 设计稿的背景元素真的延伸到了出血区(用户没设计到出血区,导出来依然会是白边)