“我在页面上显示得好好的表格导出 PDF 就变成乱码了”如果你用过任何前端表格组件的 PDF 导出功能大概率遇到过这个场景表格里明明都是正常的中文数据一丝不差可一旦点击「导出 PDF」打开文件一看——中文部分变成了方块、问号或者干脆一片空白。第一反应往往是这是不是组件的 Bug但如果你换一台电脑再打开同一个 PDF它可能又正常了。或者你把同一份数据换成纯英文导出就完全没有问题。这些现象指向同一个答案这不是 Bug而是 PDF 这种文件格式的「天性」决定的。要理解这件事我们需要从一个最基本的问题说起浏览器和 PDF对「字体」的理解完全不同。网页和 PDF两种完全不同的渲染哲学网页是怎么显示文字的当你在浏览器中打开一个网页看到一行中文时浏览器在幕后做的事情其实非常「将就」先看 CSS 里指定了什么字体比如font-family: Microsoft YaHei去用户的操作系统里找这个字体如果找到了就用它来渲染如果没找到就顺着font-family的备选列表往下找全都没找到就用系统默认字体兜底这个过程是实时的、动态的、借力的。浏览器从来不把字体打包在网页里Web Font 是另一回事它依赖的是用户电脑上已有的字体资源。所以你在开发机上看到中文正常显示并不是你的代码里做了什么特别的事情——那只是因为你的操作系统自带了中文字体。Windows 有微软雅黑和宋体macOS 有苹方和华文Linux 通常也装了文泉驿或思源黑体。浏览器只是在借用这些已有的资源。PDF 是怎么显示文字的PDF 的设计初衷就和网页完全不同。PDFPortable Document Format便携式文档格式是 Adobe 在 1993 年提出的它的核心承诺只有一句话在任何设备、任何操作系统、任何软件上打开显示效果完全一致。要兑现这个承诺PDF 就不能像网页那样借用外部环境的东西。它必须是一个完全自足的文件——打开它所需要的一切信息都必须打包在这个文件内部。用一个比喻来说网页像去餐厅吃饭餐具是餐厅的食材是餐厅的你只负责点菜和吃。换一家餐厅餐具可能就不一样了。PDF像自带便当饭盒、筷子、食物全都是你自己准备好的。不管你在哪里打开吃的东西都一样。字体就是这份便当里的餐具。如果你没有把餐具放进去打开便当的人就得自己找餐具——能找到什么就用什么找不到就只能用手抓。拆解 PDF 内部发生了什么让我们放大一个最小的场景看看当 PDF 中出现一个字符「你」的时候PDF 阅读器到底在做什么。要在屏幕上画出「你」这个字PDF 阅读器需要知道两件事第一字符编码映射。PDF 需要知道文字中的「你」这个字符对应字体文件中的哪一个「字形Glyph」。字体文件内部有成千上万个字形每个字形有一个编号。这个映射关系叫做 CMapCharacter Map。第二字形轮廓数据。找到对应的字形编号后PDF 阅读器需要知道这个字的笔画长什么样。在字体文件中每个字形都是一组数学描述——贝塞尔曲线、控制点、笔画粗细等等。阅读器根据这些数据把字画出来。所以整个过程是这样的字符「你」 → 查编码映射表 → 找到字形编号 → 读取字形轮廓数据 → 渲染成像素显示在屏幕上如果 PDF 文件中嵌入了字体这两步信息都在文件内部阅读器可以顺利地画出正确的字形。如果没有嵌入字体呢事情就变得棘手了。导出引擎在生成 PDF 时必须在文件中写入一个「字体引用」——告诉阅读器应该用什么字体来渲染文字。当没有嵌入字体时引擎通常会回退到 PDF 内置的 14 种标准字体之一比如 Helvetica。问题是这些标准字体只包含拉丁字母、数字和基本标点它们的字形表里根本没有中文字符。于是 PDF 阅读器读到的指令就变成了「请用 Helvetica 这个字体画出你这个字」。阅读器翻开 Helvetica 的字形表一查——没有「你」。它没有其他字形数据可用只能显示为空白、方块或者问号。一个常见的误解是「我的电脑装了宋体为什么 PDF 阅读器不去用它」原因在于PDF 文件中告诉阅读器的是「用 Helvetica 画」而不是「去系统里找宋体」。阅读器忠实地执行文件中的指令不会自作主张去系统字体库里搜索替代品。浏览器能显示中文是因为它直接调用了操作系统的字体接口PDF 能不能显示中文完全取决于文件内部有没有对应的字体数据。这是两条独立的路径互不影响。为什么偏偏中文容易出问题你可能会注意到英文文档导出 PDF 很少遇到乱码为什么一到中文就出问题答案藏在 PDF 的历史规范里。PDF 格式在诞生时内建了 14 种「标准字体」Standard 14 Fonts包括我们熟悉的 Helvetica、Times-Roman、Courier 等。这些字体有几个特点它们不需要嵌入PDF 规范假设所有阅读器都自带这些字体它们只覆盖基本拉丁字符——英文字母、数字、基本标点每个字体文件的大小通常只有几十 KB因为拉丁字母总共不到 200 个字符中文的情况完全不同常用汉字约3,500 个GB2312 标准常见字符全集超过20,000 个GBK / Unicode CJK 区一个完整的中文字体文件通常515 MB这个量级决定了中文字体不可能像拉丁字体那样被默认内置。没有任何 PDF 阅读器会预装几万个汉字的字形数据作为标准兜底。所以中文字体必须由文档的创建者主动提供——也就是嵌入到 PDF 文件中。这就是中文 PDF 乱码的根本原因PDF 的标准字体不覆盖中文字符而你又没有把中文字体嵌入进去阅读器自然无法正确渲染。解决方案注册字体到底在做什么理解了上面的原理解决方案就水到渠成了。注册字体的本质所谓的注册字体本质操作只有一件事把一个字体文件交给导出引擎告诉它「导出 PDF 的时候请把这个字体嵌入进去。」这个操作并不神秘。你提供的字体文件通常是.ttf或.otf格式包含了前面提到的两样东西编码映射表和字形轮廓数据。导出引擎拿到这些数据后在生成 PDF 时会把你文档中实际用到的字符对应的字形数据写入 PDF 文件。这样PDF 就变成了一份自带餐具的便当在任何设备上都能正确显示。子集化为什么不用把整个字体塞进去一个自然的担忧是一个中文字体文件动辄几 MB 甚至十几 MB嵌入之后 PDF 文件岂不是会变得巨大这就要提到一个关键技术字体子集化Font Subsetting。子集化的逻辑很简单你文档中用了哪些字符就只嵌入那些字符的字形数据。举个例子你的表格中出现了 500 个不同的汉字。导出引擎在嵌入字体时不会把整个字体文件的 20,000 个字形全部放进去而是只提取这 500 个字的字形数据打包成一个迷你字体嵌入 PDF。效果非常显著实际使用的字符数完整字体大小子集化后的大小~50 个字~8 MB~50-80 KB~500 个字~8 MB~300-500 KB~5,000 个字~8 MB~1.5-3 MB可以看到即使你加载了一个 8 MB 的完整字体文件最终 PDF 中实际增加的体积可能只有几十到几百 KB完全在可接受的范围内。大多数支持字体嵌入的 PDF 导出引擎包括 SpreadJS 的导出模块都会自动执行子集化你不需要手动处理。如果不注册字体会怎样有人可能会问如果我不注册字体PDF 阅读器总能想办法显示出来吧答案是大概率不行。我们来分析几种情况第一种导出引擎回退到 PDF 标准字体。这是最常见的情况。未注册字体时导出引擎默认使用 Helvetica 等标准字体。这些字体里没有中文字形PDF 阅读器拿到的指令是「用 Helvetica 画你字」——画不出来直接显示为空白、方块或问号。这种情况在任何电脑上打开都是乱码跟用户系统装了什么字体无关。第二种导出引擎恰好引用了某个中文字体的名称。有些引擎在未嵌入字体时可能会根据 CSS 中的font-family在 PDF 中写入一个字体名称比如SimSun。此时 PDF 阅读器会尝试在本地系统中查找同名字体。如果找到了理论上可以渲染——但这依赖于 PDF 中同时具备正确的编码映射表CMap。没有嵌入字体时CMap 往往也是缺失或不完整的阅读器即使找到了字体文件也不知道字符「你」应该对应哪个字形编号结果仍然可能乱码。第三种某些 PDF 阅读器会尝试智能替换。部分阅读器如 Adobe Acrobat在检测到缺少字形时会尝试用本地系统字体做降级替换。这种情况下内容可能以替代字体勉强显示出来。但这取决于阅读器的具体实现和用户的操作不是所有阅读器都支持替换后的排版效果也无法保证。所以结论是不注册字体导出的 PDF即使在你自己的电脑上大概率也是乱码。浏览器能正常显示中文是因为浏览器直接调用了操作系统提供的字体服务而 PDF 是独立的自足文件它的渲染能力完全取决于文件内部携带的字体数据。这两者走的是完全不同的路径。这也引出了一个关键认知注册字体不是为了让 PDF 在别人的电脑上能看而是为了让 PDF 在任何电脑上都能看——包括你自己的。在 SpreadJS 中如何注册字体SpreadJS 作为领先的类 Excel 表格控件也支持 PDF 导出的能力所以也会存在上述的字体问题那么在理解了原理之后解决这个问题其实很简单。SpreadJS 的 PDF 导出模块提供了PDFFontsManager.registerFont方法用来注册字体。注册之后导出引擎会在生成 PDF 时自动完成字体嵌入和子集化。基本用法如下// 1. 获取字体文件的二进制数据ArrayBuffer const response await fetch(/fonts/SimSun.ttf); const fontBuffer await response.arrayBuffer(); // 2. 注册字体给它一个名称标识 GC.Spread.Sheets.PDF.PDFFontsManager.registerFont(SimSun, fontBuffer); // 3. 在导出时指定使用这个字体 const options { fontFaces: [ { name: SimSun, source: fontBuffer } ] }; spread.savePDF(function (blob) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download export.pdf; a.click(); }, options);总结回顾一下全文的核心逻辑PDF 是自足式文档不像网页可以借用系统字体必须自带字体数据PDF 标准字体只覆盖拉丁字符中文字符不在其中所以中文字体必须主动嵌入注册字体就是把字体文件交给导出引擎让它在生成 PDF 时完成嵌入子集化技术确保文件不会膨胀只嵌入实际用到的字符这是 PDF 格式规范的共同要求不是任何特定组件的问题1.一张速查表现象原因解决方法PDF 中中文变成乱码/方块未嵌入中文字体注册字体自己电脑打开也是乱码PDF 内部字体引用指向不含中文字形的标准字体注册字体注册字体后 PDF 文件变大字体数据被嵌入子集化自动优化通常无需手动处理纯英文内容没有问题PDF 标准字体已覆盖拉丁字符无需额外操作部分特殊字符仍显示异常注册的字体不包含该字符换用覆盖范围更广的字体如 Noto Sans SC最后多说几句回到文章开头的那个场景表格在浏览器里显示得完完整整导出 PDF 就乱码了。现在你知道了——这不是谁的代码写得有问题而是两种完全不同的技术体系在底层逻辑上的差异。浏览器的世界是「借用」的哲学操作系统有什么字体就用什么实时渲染用完即走。它的灵活性很高但代价是显示效果依赖环境——换一台电脑结果可能不同。PDF 的世界是「自足」的哲学所有渲染所需的信息都要打包带走。它的代价是创建者需要多做一步嵌入字体但换来的是一个承诺——不管谁打开、在哪里打开、用什么设备打开你看到的内容和作者设计的完全一样。这个承诺听起来理所当然实现起来却需要每一个细节都被妥善处理。字体就是那个容易被忽略的细节。理解了这层原因之后「注册字体」就不再是一个莫名其妙的额外步骤而是一个自然且必要的动作——就像寄快递时确认包裹里放了说明书一样多花几秒钟换来的是收件人拿到手就能用。希望这篇文章能帮你少踩一个坑。如果下次遇到 PDF 相关的问题记住这个底层思路PDF 的一切显示问题都可以从「文件内部到底携带了什么」这个角度去排查。
为什么 PDF 导出中文会乱码?——从一个字符说起
发布时间:2026/6/3 8:36:43
“我在页面上显示得好好的表格导出 PDF 就变成乱码了”如果你用过任何前端表格组件的 PDF 导出功能大概率遇到过这个场景表格里明明都是正常的中文数据一丝不差可一旦点击「导出 PDF」打开文件一看——中文部分变成了方块、问号或者干脆一片空白。第一反应往往是这是不是组件的 Bug但如果你换一台电脑再打开同一个 PDF它可能又正常了。或者你把同一份数据换成纯英文导出就完全没有问题。这些现象指向同一个答案这不是 Bug而是 PDF 这种文件格式的「天性」决定的。要理解这件事我们需要从一个最基本的问题说起浏览器和 PDF对「字体」的理解完全不同。网页和 PDF两种完全不同的渲染哲学网页是怎么显示文字的当你在浏览器中打开一个网页看到一行中文时浏览器在幕后做的事情其实非常「将就」先看 CSS 里指定了什么字体比如font-family: Microsoft YaHei去用户的操作系统里找这个字体如果找到了就用它来渲染如果没找到就顺着font-family的备选列表往下找全都没找到就用系统默认字体兜底这个过程是实时的、动态的、借力的。浏览器从来不把字体打包在网页里Web Font 是另一回事它依赖的是用户电脑上已有的字体资源。所以你在开发机上看到中文正常显示并不是你的代码里做了什么特别的事情——那只是因为你的操作系统自带了中文字体。Windows 有微软雅黑和宋体macOS 有苹方和华文Linux 通常也装了文泉驿或思源黑体。浏览器只是在借用这些已有的资源。PDF 是怎么显示文字的PDF 的设计初衷就和网页完全不同。PDFPortable Document Format便携式文档格式是 Adobe 在 1993 年提出的它的核心承诺只有一句话在任何设备、任何操作系统、任何软件上打开显示效果完全一致。要兑现这个承诺PDF 就不能像网页那样借用外部环境的东西。它必须是一个完全自足的文件——打开它所需要的一切信息都必须打包在这个文件内部。用一个比喻来说网页像去餐厅吃饭餐具是餐厅的食材是餐厅的你只负责点菜和吃。换一家餐厅餐具可能就不一样了。PDF像自带便当饭盒、筷子、食物全都是你自己准备好的。不管你在哪里打开吃的东西都一样。字体就是这份便当里的餐具。如果你没有把餐具放进去打开便当的人就得自己找餐具——能找到什么就用什么找不到就只能用手抓。拆解 PDF 内部发生了什么让我们放大一个最小的场景看看当 PDF 中出现一个字符「你」的时候PDF 阅读器到底在做什么。要在屏幕上画出「你」这个字PDF 阅读器需要知道两件事第一字符编码映射。PDF 需要知道文字中的「你」这个字符对应字体文件中的哪一个「字形Glyph」。字体文件内部有成千上万个字形每个字形有一个编号。这个映射关系叫做 CMapCharacter Map。第二字形轮廓数据。找到对应的字形编号后PDF 阅读器需要知道这个字的笔画长什么样。在字体文件中每个字形都是一组数学描述——贝塞尔曲线、控制点、笔画粗细等等。阅读器根据这些数据把字画出来。所以整个过程是这样的字符「你」 → 查编码映射表 → 找到字形编号 → 读取字形轮廓数据 → 渲染成像素显示在屏幕上如果 PDF 文件中嵌入了字体这两步信息都在文件内部阅读器可以顺利地画出正确的字形。如果没有嵌入字体呢事情就变得棘手了。导出引擎在生成 PDF 时必须在文件中写入一个「字体引用」——告诉阅读器应该用什么字体来渲染文字。当没有嵌入字体时引擎通常会回退到 PDF 内置的 14 种标准字体之一比如 Helvetica。问题是这些标准字体只包含拉丁字母、数字和基本标点它们的字形表里根本没有中文字符。于是 PDF 阅读器读到的指令就变成了「请用 Helvetica 这个字体画出你这个字」。阅读器翻开 Helvetica 的字形表一查——没有「你」。它没有其他字形数据可用只能显示为空白、方块或者问号。一个常见的误解是「我的电脑装了宋体为什么 PDF 阅读器不去用它」原因在于PDF 文件中告诉阅读器的是「用 Helvetica 画」而不是「去系统里找宋体」。阅读器忠实地执行文件中的指令不会自作主张去系统字体库里搜索替代品。浏览器能显示中文是因为它直接调用了操作系统的字体接口PDF 能不能显示中文完全取决于文件内部有没有对应的字体数据。这是两条独立的路径互不影响。为什么偏偏中文容易出问题你可能会注意到英文文档导出 PDF 很少遇到乱码为什么一到中文就出问题答案藏在 PDF 的历史规范里。PDF 格式在诞生时内建了 14 种「标准字体」Standard 14 Fonts包括我们熟悉的 Helvetica、Times-Roman、Courier 等。这些字体有几个特点它们不需要嵌入PDF 规范假设所有阅读器都自带这些字体它们只覆盖基本拉丁字符——英文字母、数字、基本标点每个字体文件的大小通常只有几十 KB因为拉丁字母总共不到 200 个字符中文的情况完全不同常用汉字约3,500 个GB2312 标准常见字符全集超过20,000 个GBK / Unicode CJK 区一个完整的中文字体文件通常515 MB这个量级决定了中文字体不可能像拉丁字体那样被默认内置。没有任何 PDF 阅读器会预装几万个汉字的字形数据作为标准兜底。所以中文字体必须由文档的创建者主动提供——也就是嵌入到 PDF 文件中。这就是中文 PDF 乱码的根本原因PDF 的标准字体不覆盖中文字符而你又没有把中文字体嵌入进去阅读器自然无法正确渲染。解决方案注册字体到底在做什么理解了上面的原理解决方案就水到渠成了。注册字体的本质所谓的注册字体本质操作只有一件事把一个字体文件交给导出引擎告诉它「导出 PDF 的时候请把这个字体嵌入进去。」这个操作并不神秘。你提供的字体文件通常是.ttf或.otf格式包含了前面提到的两样东西编码映射表和字形轮廓数据。导出引擎拿到这些数据后在生成 PDF 时会把你文档中实际用到的字符对应的字形数据写入 PDF 文件。这样PDF 就变成了一份自带餐具的便当在任何设备上都能正确显示。子集化为什么不用把整个字体塞进去一个自然的担忧是一个中文字体文件动辄几 MB 甚至十几 MB嵌入之后 PDF 文件岂不是会变得巨大这就要提到一个关键技术字体子集化Font Subsetting。子集化的逻辑很简单你文档中用了哪些字符就只嵌入那些字符的字形数据。举个例子你的表格中出现了 500 个不同的汉字。导出引擎在嵌入字体时不会把整个字体文件的 20,000 个字形全部放进去而是只提取这 500 个字的字形数据打包成一个迷你字体嵌入 PDF。效果非常显著实际使用的字符数完整字体大小子集化后的大小~50 个字~8 MB~50-80 KB~500 个字~8 MB~300-500 KB~5,000 个字~8 MB~1.5-3 MB可以看到即使你加载了一个 8 MB 的完整字体文件最终 PDF 中实际增加的体积可能只有几十到几百 KB完全在可接受的范围内。大多数支持字体嵌入的 PDF 导出引擎包括 SpreadJS 的导出模块都会自动执行子集化你不需要手动处理。如果不注册字体会怎样有人可能会问如果我不注册字体PDF 阅读器总能想办法显示出来吧答案是大概率不行。我们来分析几种情况第一种导出引擎回退到 PDF 标准字体。这是最常见的情况。未注册字体时导出引擎默认使用 Helvetica 等标准字体。这些字体里没有中文字形PDF 阅读器拿到的指令是「用 Helvetica 画你字」——画不出来直接显示为空白、方块或问号。这种情况在任何电脑上打开都是乱码跟用户系统装了什么字体无关。第二种导出引擎恰好引用了某个中文字体的名称。有些引擎在未嵌入字体时可能会根据 CSS 中的font-family在 PDF 中写入一个字体名称比如SimSun。此时 PDF 阅读器会尝试在本地系统中查找同名字体。如果找到了理论上可以渲染——但这依赖于 PDF 中同时具备正确的编码映射表CMap。没有嵌入字体时CMap 往往也是缺失或不完整的阅读器即使找到了字体文件也不知道字符「你」应该对应哪个字形编号结果仍然可能乱码。第三种某些 PDF 阅读器会尝试智能替换。部分阅读器如 Adobe Acrobat在检测到缺少字形时会尝试用本地系统字体做降级替换。这种情况下内容可能以替代字体勉强显示出来。但这取决于阅读器的具体实现和用户的操作不是所有阅读器都支持替换后的排版效果也无法保证。所以结论是不注册字体导出的 PDF即使在你自己的电脑上大概率也是乱码。浏览器能正常显示中文是因为浏览器直接调用了操作系统提供的字体服务而 PDF 是独立的自足文件它的渲染能力完全取决于文件内部携带的字体数据。这两者走的是完全不同的路径。这也引出了一个关键认知注册字体不是为了让 PDF 在别人的电脑上能看而是为了让 PDF 在任何电脑上都能看——包括你自己的。在 SpreadJS 中如何注册字体SpreadJS 作为领先的类 Excel 表格控件也支持 PDF 导出的能力所以也会存在上述的字体问题那么在理解了原理之后解决这个问题其实很简单。SpreadJS 的 PDF 导出模块提供了PDFFontsManager.registerFont方法用来注册字体。注册之后导出引擎会在生成 PDF 时自动完成字体嵌入和子集化。基本用法如下// 1. 获取字体文件的二进制数据ArrayBuffer const response await fetch(/fonts/SimSun.ttf); const fontBuffer await response.arrayBuffer(); // 2. 注册字体给它一个名称标识 GC.Spread.Sheets.PDF.PDFFontsManager.registerFont(SimSun, fontBuffer); // 3. 在导出时指定使用这个字体 const options { fontFaces: [ { name: SimSun, source: fontBuffer } ] }; spread.savePDF(function (blob) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download export.pdf; a.click(); }, options);总结回顾一下全文的核心逻辑PDF 是自足式文档不像网页可以借用系统字体必须自带字体数据PDF 标准字体只覆盖拉丁字符中文字符不在其中所以中文字体必须主动嵌入注册字体就是把字体文件交给导出引擎让它在生成 PDF 时完成嵌入子集化技术确保文件不会膨胀只嵌入实际用到的字符这是 PDF 格式规范的共同要求不是任何特定组件的问题1.一张速查表现象原因解决方法PDF 中中文变成乱码/方块未嵌入中文字体注册字体自己电脑打开也是乱码PDF 内部字体引用指向不含中文字形的标准字体注册字体注册字体后 PDF 文件变大字体数据被嵌入子集化自动优化通常无需手动处理纯英文内容没有问题PDF 标准字体已覆盖拉丁字符无需额外操作部分特殊字符仍显示异常注册的字体不包含该字符换用覆盖范围更广的字体如 Noto Sans SC最后多说几句回到文章开头的那个场景表格在浏览器里显示得完完整整导出 PDF 就乱码了。现在你知道了——这不是谁的代码写得有问题而是两种完全不同的技术体系在底层逻辑上的差异。浏览器的世界是「借用」的哲学操作系统有什么字体就用什么实时渲染用完即走。它的灵活性很高但代价是显示效果依赖环境——换一台电脑结果可能不同。PDF 的世界是「自足」的哲学所有渲染所需的信息都要打包带走。它的代价是创建者需要多做一步嵌入字体但换来的是一个承诺——不管谁打开、在哪里打开、用什么设备打开你看到的内容和作者设计的完全一样。这个承诺听起来理所当然实现起来却需要每一个细节都被妥善处理。字体就是那个容易被忽略的细节。理解了这层原因之后「注册字体」就不再是一个莫名其妙的额外步骤而是一个自然且必要的动作——就像寄快递时确认包裹里放了说明书一样多花几秒钟换来的是收件人拿到手就能用。希望这篇文章能帮你少踩一个坑。如果下次遇到 PDF 相关的问题记住这个底层思路PDF 的一切显示问题都可以从「文件内部到底携带了什么」这个角度去排查。