pdfmake中文乱码解决:字体子集化与vfs_fonts配置详解

📅 发布时间:2026/9/7 8:58:15
pdfmake中文乱码解决:字体子集化与vfs_fonts配置详解
简介pdfmake 是浏览器端生成 PDF 的流行开源库但默认字体并不包含中文字形导致导出文档经常出现方框乱码。这款资源包面向前端开发者提供了一套免后端、纯浏览器运行的完整示例下载解压后直接打开 test.html点击页面上的下载按钮即可得到一份中文无乱码的 PDF 文件直观看到方正字体带来的效果。资源包共 4 个文件核心包括 1 个 HTML 演示页、1 个主库 JavaScript 脚本和 1 个方正字体 JavaScript 脚本另有一个系统自动生成的 .DS_Store 元数据文件不影响使用整个压缩包仅 2.36MB非常轻量。目前已有 1307 人学习/下载适合刚接触 pdfmake、需要在项目中输出报表、合同、发票等文档的开发者参考。通过查看演示页中的调用方式与字体注册写法可以快速理解中文 PDF 的配置思路并把字体文件和初始化逻辑直接移植到自己的前端工程中免去反复调试乱码的烦恼。1. 项目背景为什么pdfmake导出中文会乱码先说结论pddfmake本身不是不支持中文而是它默认加载的字体文件里没有中文字形。这个坑我踩过好几次最开始用pdfmake做前端导出PDF的时候英文数字一切正常一到中文就变成一排排的方块或者问号网上搜了一圈翻到的大多是把字体转成vfs_fonts.js再引入、然后改defaultStyle.font的步骤但里面的细节——比如字体子集化怎么处理、字体文件为什么那么大、动态文本怎么传参——没人讲清楚。这个需求典型出现在什么场景呢管理后台的报表导出、订单明细下载、简历生成、发票打印只要你的用户群体是中文环境PDF导出就绕不开中文乱码这件事。pdfmake的优势在纯前端不用后端渲染模板也不用装什么转换服务一个npm包就够所以这个方案在很多中小型项目里使用频率很高。这篇文章我就把自己的完整实现过程、踩过的坑、以及最终能跑通的配置全部写出来。不管你是第一次接这种需求还是已经改了好几轮乱码但没根治照着下面的流程走一遍基本上能一次解决。2. 乱码根源解析2.1 字体缺失是唯一原因pdfmake底层用的是pdfkit一个老牌的PDF生成库PDF文件里的文字并不是把“字”存进去而是存“字符编码 字形引用”。你看PDF里能显示中文是因为PDF文件里嵌入了一种叫CIDFont的字体资源这种字体把这些字符映射到了具体的字形上。pdfmake默认内置的Roboto字体只覆盖拉丁字符和部分符号压根没有汉字映射表所以遇到中文时要么显示不出字形要么直接乱码。对比一下你用Word导出PDFMicrosoft Word会把宋体或者微软雅黑一起嵌入进去所以任何电脑打开都正常pdfmake不可能默认带中文字体因为中文字体动辄十几MB没哪个库敢默认打包进去这是它不做中文字体的根本原因。2.2 各种“乱码”形态对应的不同问题我在实际使用中见过三种“乱码”表现成因不完全一样方块字□□□□最常见字体文件没嵌入中文字形PDF viewer显示不了。中文挤成一团但英文正常Roboto对中文的fallback处理出错通常是因为字体注册顺序有问题导致最佳匹配失败。导出后文件名乱码但内容正常这个跟pdfmake字体无关是浏览器download属性编码问题后面我会专门说。第一种走“注册中文字体”这条路线就能解决第二种多半是你在生成docDefinition的时候手动给每个text都标了font属性反而干扰了全局字体设置第三种别怪pdfmake接口层处理一下URL编码就行。3. 字体选型与准备3.1 中文字体怎么选中文PDF需要嵌入中文字体但中文字体不能随便选。得综合考虑体积、版权、字形完整度三件事。我这边实际测试下来适合pdfmake用的中文开源字体主要有三款思源黑体Source Han SansGoogle和Adobe联合出品字形规范开源免费对应grep源码包里的SourceHanSansSC-Normal.otf体积较大完整版可达16MB以上。思源宋体Source Han Serif适合公文、论文风格同样体积很大。文泉驿微米黑开源中文字体体积相对小一点但字形老一点不过日常使用完全够。有人会问为什么不用微软雅黑或者宋体这两个字体虽然系统里有但Windows系统字体版权是微软的你把它打包进项目再分发给用户有法律风险而且微软雅黑的字重设计是针对显示器的不是针对印刷和PDF的在打印场景下不如思源黑体。再补充一点pdfmake支持TTF和OTF字体我推荐用TTF因为加载速度快一点。思源黑体的OTF是CFF轮廓PDF里嵌入性能不如TrueType轮廓的TTF稳定。3.2 体积优化思路完整中文字体打进前端bundle加载就会变慢。实测思源黑体完整版大约16MB转换出来的vfs_fonts.js约8-9MB因为做了Base64编码前端加载这种文件在4G网络下几乎不可用。办法是字体子集化。保留你项目里可能用到的常用汉字一般3500个常用字数字字母符号就够了体积能从16MB降到2MB左右实际体感是打开页面不再卡顿。我用的工具是fontmin一个npm包可以做字体子集化按字符集裁剪字体文件保留字形白名单。用法很简单npm install -g fontmin然后创建一个字符集文件比如chars.txt里面放你系统里可能出现的所有中文汉字最少也要把GB2312的6763个汉字放进去的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边边路和几则观七山程必许取权持信何诉白百即委每叫部六金界则济料至教务难放议单记早九华六联号称交铁确京除速区院马验带议该南条据车辆必讲手器办九放花受听务资观清反约省劳眼断空其八命较安与做件农张东风士任气决收变北被装整口取转即色式角问七及华信接话集维增米试观义市保修造身更观做格力太军外段山土界技立写往必议科证类运快志满且深走研济治导育济验准规更信据况区持层劳代己基确需斯知拉各海半空边将或金世维战济调务装九立按号产别光根型单何等指思东已验证集示历格适华统路元断精头热志响细信众华层社难供传约先必影何计铁组火南解整角华行里织志观包再始组土流往记思极强率或众维度运并重精决满和且议圆或维深铁结技准接何维共体前长局速集标铁金或指局主比风称极干警必装消别器将南列积军求资治济常者注意上面这份字符集是我示例用的实际项目你应该从数据库的用户真实数据里提取去重最准确。生成子集化字体fontmin SourceHanSansSC-Normal.otf -t chars.txt -o ./subset-fonts生成的subset/SourceHanSansSC-Normal.ttf就是裁剪后的字体。4. 实操pdfmake接入中文字体的完整流程4.1 将字体转换为vfs_fonts.jspdfmake官方提供了一个脚本专门把字体文件转换为项目可引入的JavaScript文件。核心是调用pdfmake自带的build-vfs.jscd node_modules/pdfmake node build-vfs.js 你的字体文件路径/SourceHanSansSC-Normal.ttf运行完会在pdfmake目录下生成一个vfs_fonts.js文件里面是一大段Base64编码。这个文件就是pdfmake在浏览器端的“字体数据库”。补充一下很多人到这一步会漏了文件路径。vfs_fonts.js生成后默认是在node_modules/pdfmake/build/下面你把这份文件复制到项目的静态资源目录比如src/assets/下再手动引入import pdfmake/build/vfs_fonts;4.2 注册中文字体引入之后需要在pdfmake里明确声明这个字体。关键在pdfMake.fonts配置import pdfMake from pdfmake/build/pdfmake; import pdfFonts from pdfmake/build/vfs_fonts; pdfMake.vfs pdfFonts.pdfMake.vfs; pdfMake.fonts { SourceHanSans: { normal: SourceHanSansSC-Normal.ttf, bold: SourceHanSansSC-Normal.ttf, italics: SourceHanSansSC-Normal.ttf, bolditalics: SourceHanSansSC-Normal.ttf } };这段代码的意思pdfmake内部维护一个vfs对象key是字体文件名value是字体文件数据。pdfMake.fonts里定义字体名与文件名的映射。注意这里的normal、bold等属性的值必须和vfs对象里的key完全一致。我没单独准备bold字重而是直接复用常规字重因为思源黑体的bold和normal在PDF里视觉差异足够明显浏览器端如果字体没加载好伪加粗的效果很差不如统一用normal字重再靠PDF渲染端的加粗处理。4.3 全局默认字体声明注册完成后还没完。需要在生成PDF的配置里把全局默认字体指过去。这一步很多人会漏漏了就是“一部分中文正常一部分中文还是乱码”的假象。比如你在表格里没显式指定字体的单元格就是乱码。生成PDF的核心配置如下const docDefinition { defaultStyle: { font: SourceHanSans }, content: [ // 业务内容 ] }; pdfMake.createPdf(docDefinition).download(测试文件.pdf);defaultStyle.font的值必须和pdfMake.fonts里定义的key一致不能写文件名。我最初就踩过这个坑写了“SourceHanSansSC-Normal.ttf”结果全局字体匹配失败啥也不显示。4.4 动态内容导出参数处理如果导出的是动态数据比如从接口拉取的订单列表记得把长文本整体包成一个对象而不是把变量直接拼进模板字符串里const orderText 订单号${orderId}金额${amount}时间${time}; const docDefinition { defaultStyle: { font: SourceHanSans }, content: [ { text: orderText, fontSize: 12 } ] };这样写的好处是pdfmake能正确识别整段文本的字体不会因为单个字符的字形映射失败而乱码。如果直接往数组里扔一长串中文某些版本的pdfmake在vfs查找时遇到未注册字形会直接跳过导致部分文字缺失。5. 文件下载命名的编码坑很多人在内容不乱码之后栽在下载文件名上。前端下载PDF用pdfmake的download方法默认文件名如果是中文在某些浏览器比如旧版Chrome、Edge、Safari下下载下来的文件名有可能乱码或者变成一串百分号编码。这个问题的根源在于浏览器对URL里的中文文件名处理机制不是pdfmake的问题。解决方案有两种第一种直接用download方法传中文名这个在多数现代浏览器上已经没问题了pdfMake.createPdf(docDefinition).download(月度销售报表.pdf);第二种如果你们测试环境有老浏览器就手动获取blob再用URL.createObjectURL导出pdfMake.createPdf(docDefinition).getBlob((blob) { const link document.createElement(a); link.href URL.createObjectURL(blob); link.download 月度销售报表.pdf; link.click(); URL.revokeObjectURL(link.href); });后端配合的话还可以把文件名放到Content-Disposition头里前端用encodeURIComponent处理一次filename*UTF-8${encodeURIComponent(filename)}6. pdfmake常见问题和排查速查表这块内容是我整理了自己和几个前端群里的同学遇到的典型问题汇总成一张速查表边排查边对照用。问题现象可能原因排查重点全部中文显示为方块vfs_fonts.js未引入或defaultStyle.font未设置看浏览器Network是否加载了vfs_fonts.jsconsole里有没有报错部分中文乱码、部分正常子集化字体漏了字符临时换回完整字体测试排除是否子集化裁剪问题表格/列表里中文正常但某些文字异常局部显式设置的font覆盖了全局搜索content里有没有单独的font属性导出文件名乱码浏览器兼容问题改用getBlob方式下载或后端配合Content-Disposition英文正常但中文间距怪异字体被当成纯拉丁字体处理确认defaultStyle.font是否正确指向中文字体且vfs_fonts.js已经加载字体文件太大导致页面加载卡顿使用了完整中文字体用fontmin子集化3500常用字可以控制在2MB以内pdfmake.createPdf报错font not foundvfs里没有对应字体文件检查pdfMake.vfs是否已赋值且key与注册名一致这些问题是多端、多版本情况下最容易出现的。建议你把vfs_fonts.js放在静态资源目录并加版本号避免浏览器缓存旧文件导致更新后乱码。7. 实战案例将一个管理后台报表导出做到无乱码接下来我用一个真实项目片段演示完整流程场景是导出“订单汇总月报”。这个报表包含标题、表格、页脚总数据量大概几百行要求导出成PDF后排版清晰、中文不乱码、并能按月份命名。第一步初始化项目并安装依赖npm install pdfmake fontmin第二步准备子集化字体。我直接把订单系统里所有可能出现的中文字符商品名、地区名、备注等去重之后做成chars.txt然后执行fontmin ./fonts/SourceHanSansSC-Normal.otf -t ./chars.txt -o ./public/fonts/第三步生成vfs_fonts.js并拷贝到项目src目录cd node_modules/pdfmake node build-vfs.js ../../public/fonts/SourceHanSansSC-Normal.ttf生成的vfs_fonts.js复制到src/assets/pdfmake/下。第四步编写导出模块import pdfMake from pdfmake/build/pdfmake; import pdfFonts from ../assets/pdfmake/vfs_fonts; pdfMake.vfs pdfFonts.pdfMake.vfs; pdfMake.fonts { SourceHanSans: { normal: SourceHanSansSC-Normal.ttf, bold: SourceHanSansSC-Normal.ttf, italics: SourceHanSansSC-Normal.ttf, bolditalics: SourceHanSansSC-Normal.ttf } }; export function exportMonthlyReport(month, rows) { const tableBody rows.map(item [ item.orderId, item.productName, item.region, item.amount.toFixed(2), item.status ]); const docDefinition { defaultStyle: { font: SourceHanSans }, content: [ { text: ${month}月度订单汇总, fontSize: 18, alignment: center, margin: [0, 0, 0, 16] }, { table: { headerRows: 1, widths: [auto, *, auto, auto, auto], body: [ [订单编号, 商品名称, 地区, 金额(元), 状态], ...tableBody ] } } ], pageSize: A4, pageMargins: [40, 60, 40, 60] }; pdfMake.createPdf(docDefinition).download(${month}订单汇总.pdf); }第五步测试验证。因为导出文件名带了中文我建议在Windows和macOS下各侧一次下载确认文件名无乱码。若不放心就把download参数拆出来单独用encodeURIComponent再做一次。8. 关于字体加密、压缩和加载性能的补充中文字体体积大是痛点除了子集化还能做几件事减少影响第一把vfs_fonts.js放到CDN不要打进主bundle里。vfs_fonts.js本质是个纯静态配置文件放CDN利用浏览器缓存第二次加载直接命中本地缓存。第二如果能接受个别字符用系统字体代替还可以做成“异步加载”等用户点导出的时候动态import这个文件而不是页面初始化就加载。这个优化对低端手机特别有效。第三如果你们应用有权限体系导出的PDF如果涉及敏感数据建议后端把PDF流化传输前端不要直接暴露vfs_fonts.js给页面。不过这是架构层面的权衡业务没这个要求就不用过度设计。9. 最后的经验分享这套方案我前后用了大半年不同项目里字体选型、打包方式、加载策略来回调了几轮。给新接触pdfmake的同学一条最稳的起步路径先不管体积下载思源黑体完整版直接转vfs_fonts.js接上defaultStyle.font等整个链路通了再回来做子集化。这样排查问题的时候能区分是“字体缺失”还是“业务代码逻辑”的问题。等你把项目交给别人维护时一定要在代码注释里写清楚字体是子集化的新增内容涉及生僻字时需要重新生成chars.txt、重新跑fontmin、重新转vfs_fonts.js。这个细节没写上线一个月后业务方反馈某个生僻字导出乱码届时排障成本远大于现在补几行注释的成本。本文还有配套的精品资源点击获取