pdfmake 导出中文 PDF 乱码解决:字体注册与 vfs 实战

发布时间:2026/10/15 17:16:08
pdfmake 导出中文 PDF 乱码解决:字体注册与 vfs 实战
简介这份资源面向需要在Web前端或Node环境中导出中文PDF的开发者尤其是曾因字体缺失导致中文乱码而受阻的初中级工程师。它基于pdfmake给出可直接运行的示例通过内置中文字体方案解决导出内容显示为方块或问号的问题适用于报表导出、订单打印、数据归档等场景。压缩包共4个文件约2.36MB以2个JavaScript文件为主分别承担pdfmake核心库与中文字体映射的职责另有1个HTML页面作为调用入口整体结构精简解压后即可在浏览器中验证导出效果。目前已有1313人学习下载说明该方案在实际项目中具备一定参考价值。读者可借此快速理解pdfmake的字体注册与文档定义方式掌握中文无乱码导出的关键配置并以此为模板替换成自己的业务数据减少从零排查字体问题的时间成本。1. 前端导出中文 PDF 的乱码困局pdfmake 到底能不能扛住做过管理后台的同行大概率都遇到过这个需求表格数据要一键导出成 PDF还得带公司抬头、分页页码、中文字段名。用浏览器原生打印吧样式不可控用后端生成吧又得来回传文件。于是很多人把目光投向 pdfmake——一个纯前端的 PDF 生成库声明式文档定义跑在浏览器里不依赖服务端。但真正上手就会发现一个绕不开的坎英文数字一切正常一遇到中文就翻车。要么整段空白要么满屏方块要么控制台报一个看不懂的字体错误。这不是 pdfmake 的 bug而是它默认只内置了有限的西文字体中文字形根本不在里面。这篇笔记就围绕「pdfmake 实现导出中文 PDF 无乱码文件」这条主线把字体注册、vfs 虚拟文件系统、打包体积、分页表格这些实操细节讲透让新手能照着跑通熟手能看到参数边界和踩坑点。2. 乱码的根因与字体选型为什么内置字体救不了中文2.1 从 PDF 字体嵌入机制说起要理解乱码得先知道 PDF 是怎么显示文字的。PDF 文件本身不存储「字形图片」它存储的是「字符编码 字体引用」。渲染器拿到编码后去对应字体里查字形轮廓再画到页面上。如果字体里没有这个字符的字形渲染器就只能画空白或者一个方块占位符。pdfmake 默认使用的是一套标准 PDF 字体类似 Helvetica 这类这套字体只覆盖拉丁字母和少量符号。当你把「订单编号」这样的中文塞进文档定义pdfmake 在生成 PDF 时找不到对应字形结果就是乱码或空白。所以解决思路很明确把一套包含中文字形的字体文件嵌入到生成的 PDF 里。这里有个关键点容易被忽略嵌入字体不是「引用系统字体」而是把字体文件的子集真正写进 PDF 二进制流。这意味着字体文件本身要能被 pdfmake 读取且格式要兼容。2.2 TTF 还是 OTF字体格式的取舍常见的中文字体格式有 TTF 和 OTF 两种。pdfmake 底层依赖的 PDF 生成引擎对 TTF 的支持最稳定OTF 在部分版本上会出现解析异常。我一般优先选 TTF尤其是那些字形完整、授权清晰的开源字体。选字体时还要看三个维度维度说明建议字形覆盖是否包含常用汉字、标点、数字选覆盖 GB2312 以上的字体文件体积直接影响打包和加载时间完整中文字体动辄 10MB需做子集化授权协议商用是否允许选开源可商用协议一个完整的简体中文字体TTF 文件通常在 8MB 到 20MB 之间。如果直接把这个文件打进前端 bundle首屏加载会非常难看。所以字体子集化是绕不开的一步——只保留项目里实际用到的字符体积能压到几百 KB 甚至几十 KB。2.3 字体子集化的基本思路子集化的逻辑是扫描项目源码和可能的数据源提取所有出现过的中文字符生成一个字符集然后用工具从这个字符集里裁剪字体。这样生成的字体文件只包含用到的字形体积大幅下降。但这里有个坑如果导出内容是动态的比如用户输入的名字、数据库里的地址字符集就无法提前穷举。这种情况下要么保留完整字体要么在运行时做动态子集化复杂度高。多数后台系统的导出字段是固定的表头和有限的枚举值静态子集化就够用。3. 把中文字体接进 pdfmake从注册到出稿的完整链路3.1 安装依赖与准备字体文件先装 pdfmake。用 npm 或 yarn 都行npm install pdfmake --save然后把准备好的中文 TTF 字体放到项目里比如src/assets/fonts/目录下。假设字体文件叫SourceHanSans.ttf这里只是举例实际用你选的开源字体。pdfmake 在浏览器端需要一个虚拟文件系统vfs来存放字体数据。它提供了一个pdfFonts对象我们可以把字体文件转成 base64 字符串塞进去。手动转 base64 很麻烦通常用构建工具处理。3.2 用构建脚本把 TTF 转成 pdfmake 可用的 vfs最省事的做法是写一个 Node 脚本读取 TTF 文件生成一个 JS 模块导出 base64。下面是一个可复用的脚本// scripts/build-font-vfs.js const fs require(fs); const path require(path); // 字体文件路径按实际项目调整 const fontPath path.resolve(__dirname, ../src/assets/fonts/SourceHanSans.ttf); // 输出路径 const outPath path.resolve(__dirname, ../src/assets/fonts/vfs_fonts.js); const fontBuffer fs.readFileSync(fontPath); const base64 fontBuffer.toString(base64); // pdfmake 的 vfs 结构是一个对象key 是文件名value 是 base64 const vfsContent module.exports { SourceHanSans.ttf: ${base64} }; ; fs.writeFileSync(outPath, vfsContent, utf8); console.log(vfs 字体文件已生成大小, (base64.length / 1024).toFixed(1), KB);这段脚本做了三件事读取 TTF 二进制、转成 base64、按 pdfmake 要求的对象结构写出 JS 模块。生成的vfs_fonts.js可以直接被前端代码 import。参数说明fontPath指向你的字体文件outPath是生成的目标文件。base64 会让体积膨胀约 33%所以子集化之后再做这一步否则生成的文件会很大。3.3 在业务代码里注册字体并生成文档拿到 vfs 之后注册和使用就顺理成章了// pdfExport.js import pdfMake from pdfmake/build/pdfmake; import pdfFonts from ./assets/fonts/vfs_fonts; // 把自定义字体合并进 pdfmake 的字体表 pdfMake.vfs { ...pdfMake.vfs, ...pdfFonts }; // 注册字体族指定 normal/bold/italics 各自对应的字体文件 pdfMake.fonts { SourceHanSans: { normal: SourceHanSans.ttf, bold: SourceHanSans.ttf, italics: SourceHanSans.ttf, bolditalics: SourceHanSans.ttf } }; // 文档定义 const docDefinition { defaultStyle: { font: SourceHanSans // 关键全局默认字体指向自定义字体 }, content: [ { text: 订单导出报表, fontSize: 18, bold: true, margin: [0, 0, 0, 12] }, { text: 客户名称某科技有限公司, fontSize: 12 }, { text: 导出时间2025-01-01, fontSize: 12 } ] }; // 触发下载 pdfMake.createPdf(docDefinition).download(报表.pdf);逻辑说明pdfMake.vfs是虚拟文件系统pdfmake 从这里读取字体二进制。pdfMake.fonts定义字体族把 normal、bold 等字重都指向同一个 TTF 文件如果只有一个字重四个都填同一个即可。defaultStyle.font必须显式指定否则 pdfmake 会回退到默认西文字体中文照样乱码。参数说明fontSize控制字号margin是外边距数组[左, 上, 右, 下]。download方法直接触发浏览器下载也可以用open在新窗口预览或者getBuffer拿到二进制流做后续处理。3.4 表格与分页中文长文本的排版处理实际导出场景里表格才是重头戏。pdfmake 的表格用table类型定义const docDefinition { defaultStyle: { font: SourceHanSans, fontSize: 10 }, content: [ { table: { headerRows: 1, // 第一行作为表头跨页时重复 widths: [auto, *, 80, 100], // 列宽自动、自适应、固定 body: [ [序号, 商品名称, 数量, 金额], [1, 无线蓝牙耳机, 2, 399.00], [2, 机械键盘, 1, 599.00] ] }, layout: lightHorizontalLines // 内置样式也可自定义 } ] };headerRows: 1让表头在跨页时自动重复这对长表格很实用。widths里*表示该列占据剩余空间auto根据内容自适应数字表示固定像素宽度。中文列如果内容较长建议给固定宽度或*否则auto可能撑出页面。分页控制可以用pageBreakcontent: [ { text: 第一部分, pageBreak: after }, { text: 第二部分 } ]pageBreak: after表示该元素之后强制分页before则之前分页。4. 避坑与排查中文 PDF 导出最常见的五类翻车4.1 字体注册了但依然乱码现象代码里明明设置了defaultStyle.font导出的 PDF 中文还是方块。原因最常见的是 vfs 没合并成功或者字体文件名大小写不一致。pdfmake 在 vfs 里查找字体时是精确匹配的SourceHanSans.ttf和sourcehansans.ttf会被当成两个不同的 key。解决在浏览器控制台打印pdfMake.vfs的 key 列表确认字体文件名和pdfMake.fonts里写的完全一致。另外检查defaultStyle是否真的生效有些局部样式会覆盖全局字体。4.2 打包体积暴涨首屏加载变慢现象引入字体后构建产物从几百 KB 涨到十几 MB。原因完整中文字体没有做子集化base64 又放大了三分之一。解决先做字体子集化只保留项目用到的字符。如果导出内容动态考虑把字体文件放到 CDN 按需加载或者用fetch在导出前动态加载字体再注册。不要为了省事把完整字体打进主 bundle。4.3 加粗和斜体失效现象设置了bold: true但导出的中文没有加粗效果。原因pdfMake.fonts里 bold 指向的还是 normal 的字体文件而该字体本身没有独立的粗体字形pdfmake 无法合成粗体。解决如果字体族有独立的粗体文件分别指向如果没有可以接受「伪加粗」效果有限的事实或者换一套包含多字重的字体。中文粗体在 PDF 里通常需要独立字体文件支持不能靠算法加粗。4.4 特殊字符和标点显示异常现象中文正常了但某些标点、生僻字、emoji 还是乱码。原因子集化时字符集没覆盖到这些字符或者字体本身就不包含这些字形。解决检查子集化的字符提取逻辑把标点符号、数字、英文字母都纳入。生僻字和 emoji 需要字体本身支持选字体时留意覆盖范围。emoji 通常需要彩色字体pdfmake 支持有限建议避免在 PDF 里用 emoji。4.5 服务端渲染时字体路径找不到现象在 Node 环境用 pdfmake 生成 PDF报字体文件读取失败。原因Node 端的 pdfmake 用法和浏览器端不同vfs 的加载方式有差异。解决Node 端可以直接用fs.readFileSync读取字体文件构造 vfs 对象。或者用pdfmake/build/pdfmake的 Node 版本配合PdfPrinter。关键是确认字体文件的绝对路径正确且构建工具没有把字体文件排除掉。5. 进阶技巧动态子集化与导出质量验证5.1 运行时动态子集化的可行性前面提到静态子集化无法覆盖动态内容。如果导出字段确实不可预知一个折中方案是保留一份完整字体放在服务端或 CDN导出前根据实际内容提取字符集用字体处理库在运行时裁剪。这个方案复杂度较高适合对体积和灵活性都有要求的场景。更简单的做法是把完整字体放在 CDN用fetch拿到 ArrayBuffer 后转 base64 注册进 vfs。这样主 bundle 不受影响代价是首次导出需要等待字体加载。可以加一个 loading 状态提示用户。5.2 导出质量的三个验证点生成 PDF 后别急着交付先做三个检查第一用不同阅读器打开。浏览器内置阅读器、桌面端阅读器、移动端阅读器对字体嵌入的兼容性有差异确保主流阅读器都能正常显示。第二检查文本是否可选中、可搜索。如果字体嵌入方式不对文字可能变成轮廓图形无法选中。这对需要复制内容的用户很重要。第三检查分页和表格边框。长表格跨页时表头是否重复、边框是否断裂、页码是否正确这些细节决定导出文件的专业度。5.3 一个容易被忽略的参数pdfmake 的版本差异pdfmake 不同版本在字体处理上有细微差异。较新的版本对 vfs 的合并方式、字体族的定义结构可能有调整。我一般会在项目里锁定一个稳定版本升级前先在测试环境验证中文导出是否正常。血泪经验是不要在生产环境直接升级 pdfmake 大版本字体相关的 breaking change 往往不会写在显眼的更新日志里。另外如果项目同时用了多个 PDF 相关库注意它们对全局字体注册的干扰。pdfmake 的pdfMake.fonts是全局对象多个实例互相覆盖的情况并不少见。封装一个独立的导出模块把字体注册收敛到一处能省掉很多排查时间。我自己现在的习惯是任何涉及中文 PDF 导出的项目第一步先跑一个最小 demo只输出一行中文确认字体链路通了再往上叠表格和样式。这个习惯帮我省下了至少两次在复杂业务代码里大海捞针的时间。希望帮到你。本文还有配套的精品资源点击获取