资讯动态

彻底解决jsPDF中文乱码:基于思源黑体的多语言PDF生成实战

发布时间:2026/8/23 4:32:57 来源:尧图企业网站定制
1. 项目概述从“乱码”到“全球化”的PDF生成之路如果你在前端开发中用过jsPDF大概率都踩过过“中文乱码”这个坑。明明在网页上显示得好好的中文一导出PDF就变成了方框、问号或者一堆看不懂的乱码字符。这不仅仅是中文的问题日文、韩文、阿拉伯文等任何非拉丁语系的文字在jsPDF的默认世界里都寸步难行。这个问题的本质是PDF这种格式对字体嵌入的严格要求与前端动态生成PDF时字体资源缺失之间的矛盾。我处理过太多类似的项目从简单的报表导出到复杂的多语言合同生成核心痛点都绕不开字体支持。简单来说jsPDF默认只内置了少数几种标准拉丁字体如helvetica,times。当你试图渲染一个中文字符时它会在当前字体中寻找对应的字形glyph如果找不到就会用默认的占位符通常是方框替代这就是乱码的由来。因此解决方案的核心思路非常明确为jsPDF引入一个包含目标字符集的字体文件并正确注册和使用它。这听起来简单但实操中从字体文件的选择、转换、注册到最终渲染每一步都有细节和陷阱。本文将基于Source Han Sans思源黑体这款优秀的开源字体手把手带你彻底解决jsPDF的多语言支持问题并分享我趟过的所有坑和最佳实践。2. 核心原理深度拆解为什么字体是乱码的“钥匙”要根治乱码不能只知其然更要知其所以然。我们需要深入到PDF标准和jsPDF库的工作原理层面去理解。2.1 PDF的字体嵌入机制PDF文件为了确保在任何设备上都能精确还原视觉效果其核心哲学之一是“自包含”。这意味着显示文档所需的一切资源理论上都应该打包在PDF文件内部字体就是其中最关键的资源之一。一个PDF文件中可以嵌入字体的完整子集仅包含文档中用到的字符或完整文件。当查看者打开PDF时渲染引擎会优先使用文件内嵌入的字体如果未嵌入则会尝试在操作系统的字体目录中寻找匹配的字体这导致了显示的不确定性。jsPDF在生成PDF时默认使用的是PDF的14种标准字体这些字体是PDF规范要求所有阅读器都必须内置的但它们仅包含基本的拉丁字符集。这就是为什么英文数字没问题中文直接“开天窗”的根本原因。2.2 jsPDF的字体处理流程jsPDF作为一个纯前端的库其字体处理流程可以简化为以下几步字体注册通过jsPDF.API.addFont方法或jsPDF.addFileToVFS配合addFont将字体文件的二进制数据通常为base64编码或ArrayBuffer注册到jsPDF的虚拟文件系统VFS中并指定一个字体别名如‘SourceHanSansCN’。字体声明在PDF文档的内部结构中声明使用已注册的字体并将其与一个内部字体名称关联。文本渲染当调用doc.text(‘中文’, x, y)时jsPDF会检查当前激活的字体通过doc.setFont设置。如果该字体是标准字体直接使用。如果该字体是自定义字体则从VFS中获取对应的字体数据。将字符串中的每个字符映射到字体数据中对应的字形索引和轮廓信息。将这些轮廓信息以PDF绘图指令的形式写入PDF内容流。乱码就发生在第3步的映射阶段。如果字体数据中不包含“中”这个字符的字形轮廓映射就会失败。2.3 字体格式的选择TTF vs. WOFF vs. 转换后的js文件网络上字体格式繁多我们该如何选择TTF (TrueType Font)最通用的字体格式兼容性极好。jsPDF官方示例主要使用TTF。它是我们的首选源文件。WOFF/WOFF2 (Web Open Font Format)为网络优化过的字体格式压缩率更高。但jsPDF通常不能直接使用WOFF文件需要先转换回TTF或特定的js格式。.js文件 (如vfs_fonts.js)这是早期jsPDF插件jsPDF-CustomFonts-support的做法将字体文件的二进制数据以base64形式编码在一个js文件中并预置到VFS。这是一种便捷但不够灵活的方式字体文件硬编码在库中。我们的最佳实践是使用TTF字体文件并在构建时或运行时动态将其转换为base64并注册。这保证了最大的灵活性和可控性。2.4 Source Han Sans思源黑体的优势为什么推荐它来解决多语言问题字符集覆盖极广它是一款Pan-CJK泛中日韩字体单一个字重如Regular的字体文件就包含了简体中文、繁体中文、日文、韩文所需的绝大部分汉字和假名、谚文字符总计超过5万个字符。一个字体解决多国语言。开源免费遵循SIL Open Font License可免费用于商业项目没有版权风险。质量优秀由Adobe与Google合作推出设计现代屏幕显示效果清晰。风格统一多语言文本混排时视觉风格一致美观度高。注意思源黑体家族庞大有多个子集如SourceHanSansCN简中、SourceHanSansTW繁中、SourceHanSansJP日文、SourceHanSansKR韩文以及SourceHanSansHC简中-异体等。对于大多数涵盖简中的多语言场景直接使用SourceHanSansSC简中或SourceHanSans全量即可因为它已包含日韩常用汉字。若项目明确要求区分地区字形则需选用对应子集。3. 完整实操方案四步搞定中文及多语言PDF生成理论讲完我们进入实战环节。我将分享一套经过生产环境验证的、从零开始的完整方案。3.1 第一步获取并准备字体文件下载字体访问Adobe的GitHub仓库例如github.com/adobe-fonts/source-han-sans或通过NPM包fontsource/source-han-sans获取字体文件。我们这里以TTF格式为例。下载后你会得到诸如SourceHanSansSC-Regular.ttf、SourceHanSansSC-Bold.ttf等文件。字体精简可选但推荐完整的思源黑体Regular字重TTF文件大约16MB。直接嵌入PDF会导致文件体积巨大。在生产环境中强烈建议进行字体子集化即只提取你本次PDF生成中实际用到的字符。工具可以使用fontmin、pyftsubset(来自fonttools) 等工具。操作示例使用fontmin-clinpx fontmin ./SourceHanSansSC-Regular.ttf --text-file./used-characters.txt --output./dist/其中used-characters.txt是一个包含所有可能用到的字符的文本文件。你可以通过分析你的数据源动态生成这个文件。效果一个包含几千汉字的子集化字体文件体积可以缩小到几百KB对PDF体积和网络加载影响极小。3.2 第二步将字体集成到前端项目有两种主流方式静态引入和动态加载。方案A静态引入适用于固定字体、项目规模不大将TTF文件放入项目的静态资源目录如public/fonts/。在需要使用jsPDF的组件或模块中将字体文件转换为base64字符串。你可以使用在线工具转换或通过构建工具如Webpack的asset/source模块在构建时转换。// 假设使用Webpack配置module.rules { test: /\.(ttf|otf)$/, type: asset/source, // 将文件作为字符串导出 }在代码中导入import fontSourceHanSansRegular from ../fonts/SourceHanSansSC-Regular.subset.ttf;方案B动态加载推荐灵活且高效在用户触发生成PDF时动态从服务器或CDN获取字体文件。这避免了初始包体积过大也便于管理多套字体。async function loadFont(url, fontName) { const response await fetch(url); const fontArrayBuffer await response.arrayBuffer(); // 将ArrayBuffer转换为jsPDF需要的格式 const fontBase64 arrayBufferToBase64(fontArrayBuffer); return { fontBase64, fontName }; } function arrayBufferToBase64(buffer) { let binary ; const bytes new Uint8Array(buffer); for (let i 0; i bytes.byteLength; i) { binary String.fromCharCode(bytes[i]); } return window.btoa(binary); }3.3 第三步注册字体并生成PDF这是最核心的代码环节。我们以动态加载为例展示完整流程。import { jsPDF } from jspdf; async function generatePDFWithChinese() { // 1. 初始化jsPDF实例 const doc new jsPDF({ orientation: portrait, unit: mm, format: a4 }); // 2. 动态加载并注册字体 const fontUrl /api/fonts/SourceHanSansSC-Regular.subset.ttf; // 你的字体API或路径 const fontData await loadFont(fontUrl, SourceHanSansSC); // 关键步骤将字体添加到jsPDF的虚拟文件系统并注册 // 注意jsPDF版本不同API可能有差异。以下为常见写法。 const fontName SourceHanSansSC-Normal; // 你自定义的字体标识 doc.addFileToVFS(${fontName}.ttf, fontData.fontBase64); doc.addFont(${fontName}.ttf, fontName, normal); // 第三个参数是字重 // 3. 使用注册的字体 doc.setFont(fontName); // 设置当前字体 doc.setFontSize(12); // 4. 添加中文文本 doc.text(这是一段完全正常显示的中文内容。, 20, 20); doc.text(日本語のテキストも表示できます。, 20, 30); doc.text(한국어 텍스트도 표시 가능합니다., 20, 40); // 5. 保存PDF doc.save(多语言文档示例.pdf); } // 调用函数 generatePDFWithChinese().catch(console.error);关键点解析addFileToVFS: 将base64格式的字体数据存入jsPDF的内部虚拟文件系统并赋予一个文件名如‘SourceHanSansSC-Normal.ttf’。这个文件名是后续引用的关键。addFont: 告诉jsPDF这个VFS中的文件是一个字体并为其指定一个在setFont时使用的逻辑名称fontName和字重‘normal’,‘bold’等。逻辑名称、VFS中的文件名、setFont使用的名称这三者可以相同也可以不同但必须建立正确的映射关系这是新手最容易混淆出错的地方。我建议在简单场景下让它们保持一致。setFont: 切换当前绘图状态使用的字体到我们注册的自定义字体。3.4 第四步处理多字重粗体、斜体一份正式的文档通常需要常规体和粗体。你需要为每个字重单独注册字体。// 假设已加载Regular和Bold字重的字体数据fontRegularBase64, fontBoldBase64 const fontFamily SourceHanSansSC; // 注册常规体 doc.addFileToVFS(${fontFamily}-Normal.ttf, fontRegularBase64); doc.addFont(${fontFamily}-Normal.ttf, fontFamily, normal); // 注册粗体 doc.addFileToVFS(${fontFamily}-Bold.ttf, fontBoldBase64); doc.addFont(${fontFamily}-Bold.ttf, fontFamily, bold); // 使用 doc.setFont(fontFamily, normal); doc.text(常规文本, 20, 20); doc.setFont(fontFamily, bold); doc.text(加粗文本, 20, 30);重要提示setFont的第二个参数字重‘normal’,‘bold’必须与addFont时注册的字重严格对应。jsPDF不会自动将‘normal’字体模拟为粗体如果你用setFont(fontFamily, ‘bold’)但只注册了‘normal’字重它可能会回退到标准字体导致中文粗体部分再次乱码。4. 高级技巧与性能优化解决了基本问题后我们来看看如何做得更好、更稳、更快。4.1 字体缓存策略频繁从网络加载字体是不可接受的。我们可以在客户端建立缓存。IndexedDB/本地存储将加载后的字体base64字符串或ArrayBuffer缓存到IndexedDB中。下次使用时先检查缓存命中则直接使用未命中再请求网络并更新缓存。Service Worker缓存如果字体是静态资源可以通过Service Worker的Cache API进行缓存实现离线可用和快速加载。// 简单的LocalStorage缓存示例注意base64数据很大LocalStorage有容量限制仅适用于子集化后的小字体 const CACHE_KEY cached_font_sourcehansans_regular; async function getFontWithCache(url) { let fontBase64 localStorage.getItem(CACHE_KEY); if (fontBase64) { return fontBase64; } const data await loadFont(url); try { localStorage.setItem(CACHE_KEY, data.fontBase64); } catch (e) { console.warn(字体过大LocalStorage缓存失败考虑使用IndexedDB, e); } return data.fontBase64; }4.2 自动化子集生成与部署流水线对于内容动态的复杂项目手动维护used-characters.txt不现实。可以建立自动化流水线在后端服务或构建阶段分析一个周期内如一天所有生成的PDF内容去重后得到字符集。调用字体子集化工具如fonttools生成最新的子集化字体文件。将新字体文件部署到CDN并更新前端引用的URL或版本号。4.3 服务端渲染SSR与Node.js环境如果你在Node.js如Next.js, Nuxt.js的SSR中使用jsPDF字体处理方式略有不同。你无法使用fetch除非使用node-fetch和window.btoa。字体读取使用fs.readFileSync读取本地字体文件。Base64转换使用Buffer.from(fontData).toString(‘base64’)。import { jsPDF } from jspdf; import fs from fs; import path from path; export function generatePDFOnServer() { const doc new jsPDF(); const fontPath path.resolve(./fonts/SourceHanSansSC-Regular.subset.ttf); const fontData fs.readFileSync(fontPath); const fontBase64 Buffer.from(fontData).toString(base64); doc.addFileToVFS(SourceHanSans.ttf, fontBase64); doc.addFont(SourceHanSans.ttf, SourceHanSans, normal); doc.setFont(SourceHanSans); doc.text(服务端生成的中文PDF, 20, 20); // 输出为Buffer或保存到文件 const pdfBuffer doc.output(arraybuffer); // 或 doc.save(server.pdf); // 在Node中会保存到磁盘 return pdfBuffer; }4.4 与其他PDF库或功能的配合有时项目可能不仅需要生成PDF还需要编辑、合并或添加复杂元素。与html2canvas配合生成截图式PDF先通过html2canvas将DOM转成图片再用jsPDF添加图片。这种情况下字体问题由浏览器渲染和html2canvas处理只要页面能正确显示截图就不会乱码。但这种方法生成的是位图PDF体积大文字无法选择搜索。与pdf-lib配合pdf-lib是另一个强大的PDF操作库。如果你需要编辑一个已有PDF如填充表单可以使用pdf-lib它嵌入自定义字体也有一套API。有时在复杂场景下混合使用多个库可能是最佳选择。5. 常见问题排查与实战避坑指南即使按照步骤操作你可能还是会遇到一些诡异的问题。下面是我总结的“血泪”排查清单。5.1 问题一字体注册成功但文本不显示或仍是乱码检查1setFont调用是否正确且生效。坑点doc.setFont(‘SourceHanSans’)必须在doc.text()之前调用。有时在循环或条件分支中可能会忘记设置。调试在setFont后立即打印doc.internal.getFont()查看当前激活的字体信息是否是你注册的字体。检查2字体文件本身是否包含这些字符。用字体查看软件如FontForge打开你使用的TTF文件搜索一个你知道会用到的中文汉字看是否存在。确保你使用的字体子集文件包含了所有需要的字符。检查3字符编码问题。确保你的JavaScript源代码文件本身的编码是UTF-8。确保从API或数据库获取的中文文本在传入doc.text()时已经是正确的Unicode字符串。如果后端返回了错误的编码如GBK前端需要正确转换。5.2 问题二粗体Bold设置无效原因这是最高频的问题没有之一。确认你是否为‘bold’字重注册了独立的、真正的粗体字体文件如SourceHanSansSC-Bold.ttf仅仅注册常规体然后调用setFont(fontName, ‘bold’)是没用的。验证检查addFont的第三个参数是否为‘bold’并且setFont的第二个参数与之匹配。// 错误示例只注册了normal却想用bold doc.addFont(MyFont-Normal.ttf, MyFont, normal); doc.setFont(MyFont, bold); // 这将失败可能回退到标准字体导致乱码 // 正确示例分别注册 doc.addFont(MyFont-Normal.ttf, MyFont, normal); doc.addFont(MyFont-Bold.ttf, MyFont, bold); // 必须有一个真正的Bold文件5.3 问题三PDF文件体积异常巨大首要原因嵌入了完整的、未子集化的大字体文件。一个16MB的字体文件嵌入PDF体积立刻爆炸。解决方案字体子集化如前所述这是最有效的方法。复用字体如果同一会话中生成多个PDF确保jsPDF实例复用字体只需注册一次。压缩jsPDF生成时可以使用{ compression: true }选项启用压缩但效果远不如子集化明显。5.4 问题四在Vue/React组件中字体重复注册报错现象在单页应用SPA中切换路由或组件多次渲染时可能多次调用字体注册代码导致jsPDF内部报错如“Font already exists”。解决方案将字体注册逻辑提升到单例位置。全局注册在应用入口如main.js或App.vue注册一次。惰性注册并缓存在工具函数中设置一个标志位fontRegistered第一次注册后设为true后续调用直接跳过注册步骤。let isFontRegistered false; export async function registerFontToPDF(doc) { if (isFontRegistered) { return; } // ... 执行加载和注册字体代码 ... doc.addFileToVFS(...); doc.addFont(...); isFontRegistered true; }5.5 问题五某些特殊符号或罕见字仍显示为方框原因即使使用思源黑体也无法100%覆盖所有Unicode字符尤其是非常用字、古汉字、特殊符号。排查确认该字符是否在思源黑体的覆盖范围内可查阅其字符覆盖表。如果不在考虑使用字符集更全的字体如BabelStone Han等或者准备一个备选字体。在前端逻辑中对于确定会缺失的字符可以设计降级方案比如用图片替代或者在生成前进行字符验证和替换提示。5.6 性能问题排查表现象可能原因解决方案首次生成PDF极慢网络加载大字体文件1. 使用子集化字体。2. 实现字体缓存。3. 考虑将字体作为初始包的一部分权衡包大小。连续生成PDF卡顿每次都在重复注册字体实现字体注册状态单例管理避免重复操作。内存占用过高同时处理多个大型PDF或嵌入了过多字体1. 及时销毁jsPDF实例 (doc null)。2. 使用Web Worker将PDF生成任务移出主线程。3. 流式生成避免一次性操作超大文档。移动端崩溃字体文件过大或PDF页面元素过多1.强制子集化。2. 分页生成避免单页内容过多。3. 降低图片分辨率如果使用了图片。解决jsPDF的多语言字体问题是一个典型的“细节决定成败”的任务。从选择正确的字体文件到精准的子集化再到无误的注册与调用每一步都需要仔细对待。经过以上方案的梳理和实战坑点的排查你应该能够构建出稳定、高效、支持全球字符的前端PDF生成功能。这套方案不仅适用于中文对于日文、韩文、阿拉伯文、泰文等任何需要复杂字形的语言思路都是相通的——找到合适的字体正确地嵌入它。剩下的就是根据你的具体业务场景在性能、体验和灵活性之间找到最佳平衡点了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价