简介:面向需要使用PHP生成PDF并支持中文输出的开发者,这份资料包以FPDF 1.81核心代码为基础,整合了chinese.php中文字体处理脚本、ttf2pt1字体格式转换工具及已转换好的黑体字体定义文件simhei.php,可直接在FPDF中调用并输出中文PDF。资源共47个文件,包含20个php核心代码与示例、20个字符映射表(map)、字体转换程序zip包、afm/ttf/z等字体相关格式以及测试用jpg和pdf样例,整体压缩包大小约10.4MB,结构清晰便于学习和二次开发。已有419人学习浏览,适合初识FPDF的PHP开发者或需要完善中文PDF输出功能的技术人员。借助example.php可一键生成中文样例,帮助快速理解字体映射与转换流程,省去自行配置中文字体的麻烦。
1. 标题没说的真相:FPDF 默认一个中文字都打不出来
先说结论:FPDF 是 PHP 生态里最老牌的 PDF 生成库,资料一搜一大把,但“可输出中文”这件小事,能卡住一半以上第一次碰它的人。默认装好 FPDF 后,你用Cell()写一句“你好”,生成的 PDF 里要么是四个方框,要么是一串乱码,原因是它的内置字体只覆盖拉丁字符集,压根不认识汉字。
这篇文章要解决的,就是这条链路:怎么给 FPDF 接入中文字体、怎么正确写出 UTF-8 中文、怎么处理中文特有的对齐和自动换行,以及最常见的翻车点。适合这些读者:用 PHP 做合同、发票、报表、标签、证书等技术方案的开发者,以及被领导一句“PDF 里怎么是乱码”追着跑的项目维护者。看完之后,你能直接照着代码跑通一份中文 PDF,并且知道字体、编码、宽度这些参数背后到底在发生什么。
2. FPDF 输出中文的原理:单字节引擎与字体注册
2.1 为什么默认字体遇到中文就“集体失踪”
FPDF 内部维护的字体信息是一张单字节映射表,每个字符在 PDF 内容流里用一个字节表示。拉丁字符集正好一个字节一个字符,但中文在 UTF-8 编码下每个汉字占用三个字节,FPDF 在计算字符宽度和写入内容流时,会把这仨字节当成三个独立的“拉丁字符”处理,于是你看到的要么是方框(缺字形),要么是形如ä½ å¥½的乱码。
这不是 FPDF 有 bug,而是它的设计目标本来就是轻量、单字节、面向 Western 文本。官方文档里有一句话被反复引用但很少人细看:要使用非拉丁文字,必须自己注册包含对应字形的 TrueType 字体。记住关键词:字体文件、注册、显式声明,三个缺一不可。
2.2 中文字体输出的三要素:字形文件、AddFont 注册、SetFont 生效
要让 FPDF 输出中文,链路是固定的。第一步,准备一个.ttf字体文件,这个文件里必须真实包含汉字字形——拿一个纯英文 TTF 注册一百遍也没用。第二步,调用$pdf->AddFont('别名', '', '字体文件路径.ttf')把字体加载进当前 PDF 对象。第三步,$pdf->SetFont('别名', '', 14)让字体生效。
这里有个细节容易忽略:AddFont的第二个参数是字体样式,粗体B、斜体I、粗斜体BI,它要求你给每个样式单独准备一个变体文件并分别注册。如果只注册了常规体,却在SetFont里写'B',FPDF 会直接抛异常。常见做法是正文用常规体,标题用另一个粗体字体文件注册成不同的别名,而不是依赖同一个字体的伪粗体。
2.3 字体选型与体积参数:为什么字体文件越大越容易出事儿
中文字体和拉丁字体最大的区别是体积。一个完整的思源黑体或思源宋体字体文件动辄二三十 MB,FPDF 每注册一个字体,会把这个字体的宽度表读进内存,文档里每页还要引用字体描述信息。体积过大的两个后果:一是 PHP 内存消耗肉眼可见地涨,二是生成的 PDF 文件被字体数据撑大好几倍。
所以给 FPDF 配中文字体时,我一般不会直接丢一个全家桶字体进去,而是先用字体工具把常用的简体中文字符切一个子集出来。子集覆盖 GB2312 的常用汉字范围(大约 6763 个字符)就够日常办公文档用了,体积能压到 3~6 MB,内存和文件大小都可控。下表是我常用的选型参考:
| 用途 | 字体方案 | 子集范围 | 字号建议 | 备注 |
|---|---|---|---|---|
| 正文 | 宋体风格 | GB2312 常用字 + ASCII | 10~12pt | 阅读舒适,行高需微调 |
| 标题 | 黑体风格 | GB2312 常用字 + ASCII | 14~18pt | 粗壮醒目,可用独立注册名 |
| 标签/票据 | 等宽中文字体 | 精简子集 | 8~10pt | 对齐要求高,宽度表要准确 |
| 临时验证 | 系统中文字体 | 不切子集 | 任意 | 仅本地测试,不建议上生产 |
一个反直觉的点:子集切得好不好,直接影响 FPDF 能否正常显示字符。如果在SetFont之后写了一个子集里没有的字符,FPDF 不会报错,而是显示一个空白或方框。所以生产环境里的字体子集,必须把业务数据里可能出现的人名、地名、生僻字都覆盖进去,这就是为什么我常说“字体子集是中文 PDF 里最容易翻车的隐性参数”。
3. 用 FPDF 在本地跑通最小中文示例:命令、代码与参数
3.1 环境准备与目录结构
FPDF 不需要安装扩展,只要 PHP 版本不低于 5.6,没有特别依赖。不过处理中文时强烈建议安装mbstring扩展,后面做宽度计算和字符串截断会频繁用到。目录结构按常见做法拆成三块:FPDF 库本体放lib/fpdf/,字体文件放fonts/,业务脚本放根目录。
# 以 Debian/Ubuntu 系为例,安装 mbstring sudo apt install php-mbstring # 确认扩展已启用 php -m | grep mbstring需要说明的是,FPDF 的Output方法支持五种目标:I(浏览器输出)、D(下载)、F(存文件)、S(返回字符串)、E(邮件附件 inline)。调试阶段用I最直观,生产环境一般用F或S。下面这段是最小的中文输出完整代码,你可以直接存成demo.php运行。
<?php require_once __DIR__ . '/lib/fpdf/fpdf.php'; // 创建 A4 页面对象,单位为毫米(mm) $pdf = new FPDF(); $pdf->AddPage(); // 注册中文字体:别名 SongSub, 常规样式, 字体文件路径 $pdf->AddFont('SongSub', '', __DIR__ . '/fonts/SourceHanSerifCN-Subset.ttf'); // 切换当前字体为 SongSub,字号 14pt $pdf->SetFont('SongSub', '', 14); // 输出一行中文:Cell(宽度, 行高, 文本, 边框, 换行, 对齐) $pdf->Cell(0, 10, '你好,这是 FPDF 生成的第一份中文 PDF。', 0, 1, 'L'); // 保存到本地文件,第二个参数是目标路径 $pdf->Output('F', __DIR__ . '/output/chinese_demo.pdf');代码逻辑拆开看:Cell的第一个参数0表示宽度占满整行——在毫米单位下,A4 的可用宽度是 210 减去左右页边距;第二个参数10是这一行的高度,单位同样是毫米;第三个参数是文本内容;0表示不画边框;1表示输出完后换行,下一行从左边界继续;最后一个'L'是左对齐。AddFont的完整签名是AddFont($family, $style='', $file=''),如果第三个参数省略,FPDF 会去font目录里找$family.php这种预编译描述文件;给绝对路径的.ttf是最省心的做法,FPDF 会自动用内置解析器读取 TTF 的宽度表。执行之后,output/chinese_demo.pdf里应该能看到一行正常的中文,没有任何方框和乱码。
3.2 SetFont 的三个参数:别名、样式、字号
SetFont($family, $style, $size)决定了文本渲染用的字体。重点说样式参数:它接受的合法值是空字符串、B、I、BI,对应常规、粗体、斜体和粗斜体。注意,FPDF 不会自动为中文生成粗体——拉丁字体可以做字形加粗,但 TTF 中文子集一般没有专门的 Bold 变体,你写了'B'它会试着加载名称为别名B的字体文件,找不到就抛错。
因此我的习惯是,中文文档要标题效果,就再准备一个黑体风格的字体文件,用不同的别名注册,比如SongSub管正文、HeiSub管标题,然后靠切换SetFont的别名实现视觉层级,而不是靠样式参数硬撑。字号参数是 pt 值,FPDF 已经做了解析,14和'14'(字符串)都合法。另一个值得写的参数是行高:中文字符全角,字形高度占比和拉丁字体明显不同,同样 10 毫米行高,中文经常显得上下拥挤,常见的调法是改成 5.5~6 毫米,这个按你自己的视觉审美微调。
3.3 页面尺寸、边距与坐标系
FPDF 默认页面是 A4,纵向,左右边距 10 毫米。注意Cell(0, ...)里宽度的0是从当前左边距一直延伸到右边距的可用宽度,这是它最常用的用法。如果你是做面单、小票这类窄幅输出,要改用下面这套配置:
// 宽度 80mm,高度 60mm,横向,边距左右各 5mm $pdf = new FPDF('P', 'mm', array(80, 60)); $pdf->SetMargins(5, 5, 5); $pdf->AddPage();自定义页面尺寸时,单位必须和后续用到宽高的地方保持一致,FPDF 支持的三种单位mm、pt、in存在换算关系:1 英寸等于 72 磅,约等于 25.4 毫米。混用单位是新手常踩的坑——用了毫米初始化页面,却在SetFont里把字号写成18(这个其实没事,字号本就是 pt),又在Cell里填 20 当宽度,结果排版完全对不上。这个坐标系问题在下一章处理中文对齐时还会再次出现。
4. 中文文本布局:宽度计算、对齐与自动换行的四个参数
4.1 GetStringWidth 在中文面前失灵,手写一个宽度计算函数
FPDF 的GetStringWidth按字节取宽度,对一个 UTF-8 编码的汉字,它返回的是三个字节对应的宽度之和,和实际渲染出来的汉字宽度差别很大,导致右对齐时位置肉眼可见地偏。这属于 FPDF 单字节模型的硬伤,绕不过去,只能在业务层补一个按字符数累加宽度的函数。
/** * 计算中英文混排字符串的实际渲染宽度(近似,用于对齐定位) * @param FPDF $pdf 已经 SetFont 的 PDF 实例 * @param string $text UTF-8 文本 * @return float 宽度值,单位与 PDF 对象当前单位一致 */ function smartTextWidth($pdf, $text) { $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY); $width = 0.0; foreach ($chars as $char) { // 单个字符取宽:ASCII 半个字宽,中文全角一个字宽 $width += $pdf->GetStringWidth($char); } return $width; }逻辑说明:preg_split('//u', $text)用空模式配合u修饰符,会把 UTF-8 字符串按单字切分,避免把汉字的多个字节拆开。GetStringWidth对单个中文字符返回的是当前字体下这个字的宽度,对 ASCII 单字符返回半个字宽左右的值,累加起来就得到近似真实宽度。这个函数的精度足够支撑右对齐和居中的定位计算,比直接拿GetStringWidth($text)算全文精度高得多。
4.2 用 MultiCell 做中文自动换行:宽度与行高怎么配合
实际业务里的中文字段长度不可控,Cell的优点是单行精准定位,缺点是内容超宽就直接溢出到页边距外,不会自动断行。处理长文本的标准做法是用MultiCell,它按给定宽度自动换行,碰到换行符也会断行。它的核心参数和Cell一致,但多了一个行高语义:第三个参数是每行的高度,FPDF 每换一行就把 y 坐标下移这么多。
// 一段可能超过可用宽度的中文简介 $intro = '这是一段比较长的中文文本,包含标点、数字 123 和英文,用来演示 MultiCell 的自动换行行为。'; $pdf->SetFont('SongSub', '', 12); $pdf->MultiCell(0, 6, $intro, 0, 'L');参数说明:第一个参数0表示宽度沿用整行可用宽度,同样支持具体的毫米数值用来做侧边栏这类布局;第二个参数6是每行高度,我按 12pt 字号的视觉习惯取 6 毫米,这个值在中文全文显示时通常比英文紧凑;第三个参数0是不画边框;'L'左对齐。注意MultiCell的换行依据是宽度,不是中文字符的“自然断词点”,中文每个字都可以换行,表现反而比英文更平滑;英文长单词超宽时不会断开,整行会溢出,这个是 FPDF 的另一个已知局限,只能在上游先做字符截断。
4.3 表格场景下把 Cell 和 MultiCell 混用的对齐问题
做发票、名册这类带边框的页面时,一个常见的需求是“单元格内文本自动换行但表格边框不破”。实际写的时候,我会用固定宽度的MultiCell负责文本,用Rect画外框,而不是指望Cell一次搞定。
$colX = 10; // 列起始 x 坐标 $colW = 40; // 列宽 40mm $lineH = 6; // 行高 6mm $y = $pdf->GetY(); // 画单元格边框:Rect(x, y, 宽, 高) $pdf->Rect($colX, $y, $colW, $lineH); // 文本输出:宽度固定,高度用行高,自动换行 $pdf->SetXY($colX + 2, $y + 1); // 左边距 2mm,上下各留 1mm 内白 $pdf->MultiCell($colW - 4, 5, $cellText, 0, 'L');这里有个状态机的细节:SetXY之后,MultiCell会改变GetY()的位置,画下一行外框前必须重新读取GetY()而不是用原始$y累加,否则行高累计误差会越来越大。内白靠$colX + 2和$colW - 4手工实现,这是 FPDF 没有原生 padding 参数下的标准做法。另外MultiCell的换行行高我给的是5而不是边框行的6,目的是让换行文本之间更紧凑,避免看着像行间距过大——这种数值差异就是排中文版式里真正吃时间的部分。
5. FPDF 中文输出避坑指南:五个高频故障与排查思路
5.1 输出 PDF 全是方框,一个汉字都看不见
现象:PDF 能生成,页面内容完整,但所有汉字显示成空心的方框。
原因:字体文件被注册了,但字体文件本身不包含汉字字形,或者AddFont用的字体文件路径错误导致 FPDF 悄悄加载了内置的拉丁字体。最常见的情况是拿一个只含 128 个字符的迷你字体当中文用。
解决:确认.ttf文件的真实字符覆盖,用字体查看器或fc-query这类命令检查它是否包含 CJK 统一表意文字。代码里做一件事:AddFont后立即SetFont并调用$pdf->Text(10, 10, '测'),然后用 PDF 查看器检查这个“测”是否正常显示,如果这一步都不行,问题就在字体文件本身。
5.2 中文正常,但标点符号变成了半角甚至消失
现象:正文汉字全部正常,但逗号、引号、顿号看着很怪,有的偏窄,有的像被截了一刀。
原因:中文字体子集在切分时,经常只保留了汉字和 ASCII,把全角标点(U+3000~U+303F 区间)漏掉了;FPDF 找不到字形就回退到一个空字符,标点就“消失”或错位。
解决:切子集时不要只按“常用汉字列表”切,把 GB2312 全角标点区和常用符号区一起保留。我在生产里是把汉字区、全角标点区、ASCII 可见字符三个区间打包切进一个子集,这样中文排版才完整。
5.3 文本正确但右对齐时整体偏右好几毫米
现象:用Cell($w, $h, $text, 0, 0, 'R')做右对齐,中文行看起来明显没有贴到右边线,位置时准时不准。
原因:就是第 4 章讲的GetStringWidth按字节计算导致的。FPDF 内部做'R'对齐时,用的是当前字体的字节宽度表,对 UTF-8 中文计算出来的宽度大于真实渲染宽度,于是右对齐为了“塞开”内容,把位置推过了。
解决:不依赖Cell的对齐参数做中文右对齐,改用Cell的左对齐 + 手工计算 x 坐标。先算出文本真实宽度,再用“右边距 - 文本宽度”作为 x 起点输出。
$rightEdge = $pdf->GetPageWidth() - $pdf->GetX(); // 这里以左边距为参考做简化,精确值直接取 lMargin + w - txtW $txtW = smartTextWidth($pdf, $title); $pdf->SetX($pdf->lMargin + $pdf->w - $pdf->rMargin - $txtW); $pdf->Cell($txtW, 8, $title, 0, 1, 'L');5.4 AddFont 报错:无法打开字体文件或不是有效的 TrueType
现象:AddFont直接抛异常,提示字体文件不存在,或者“not a valid TrueType font”,而且报错信息不给出具体文件。
原因:路径问题占了七成,FPDF 的AddFont接收相对路径时,是相对于 FPDF 类所在目录解析的,业务脚本里写的fonts/xxx.ttf经常指向错误的位置;另外某些系统自带的.ttc合集文件,FPDF 1.8.x 解析不了。
解决:一律用绝对路径传参,并在调用前加一个is_file()断言。.ttc文件优先转成单.ttf再用,这是最省时间的办法。
5.5 打开 PDF 提示字体未嵌入,打印或转 PDF/A 变样
现象:用浏览器打开正常,但交给印刷系统或做长期归档校验时,报告“字体未嵌入”,部分环境渲染成替换字体,版式完全错乱。
原因:FPDF 对 TTF 的处理是读取字形宽度并引用字体文件,但生成的 PDF 内嵌字体信息的方式取决于 TTF 是否包含有效的嵌入许可标志。一些精简后的子集字体(subsetting)工具会把OS/2表中的嵌入权限位改掉,FPDF 会读这个位来决定是否内嵌。
解决:切子集时选择“允许嵌入”的选项,生成后用 PDF 检查工具确认字体是否内嵌。如果内嵌失败,换一个未修改过嵌入标志的字体直接注册,别在这种玄学问题上跟 FPDF 硬刚太久。
下表把上面几条串成一个速查:
| 现象 | 常见根因 | 首选排查动作 |
|---|---|---|
| 全篇方框 | 字体无中文字形 | 检查 TTF 字符覆盖 |
| 标点异常 | 子集漏了全角标点区 | 重新切子集包含标点区 |
| 右对齐偏移 | GetStringWidth 按字节算宽 | 用 smartTextWidth 手工定坐标 |
| AddFont 报错 | 路径或 .ttc 格式不兼容 | 转绝对路径 + 转 .ttf |
| 字体未嵌入 | 嵌入许可位被改 | 检查工具链嵌入选项 |
6. 进阶:字体复用、批量生成与输出校验的习惯
如果只有一两份 PDF,前面的代码已经够了。但真实项目里往往要连续生成几百份合同或单据,这时候有两个值得养成的习惯。
第一个习惯是复用 PDF 对象与字体注册。AddFont是一次性操作,同一个字体文件在同一个 PHP 进程里注册一次就行;多份文档用同一个页面模板时,我会建一个工厂对象,把AddPage()、AddFont、SetFont、固定的SetMargins都封装进去,循环里只调用数据填充方法,避免每份都重新读一遍几十 MB 的字体宽度表。内存和耗时的节省是实打实的。
function makeInvoicePdf($orderNo, $amount, $customerName) { $pdf = new InvoiceTemplate('P', 'mm', 'A4'); // 封装好的模板类 $pdf->AddPage(); $pdf->SetFont('SongSub', '', 12); $pdf->Cell(0, 6, '订单号:' . $orderNo, 0, 1); $pdf->Cell(0, 6, '客户:' . $customerName, 0, 1); // ... 更多数据行 return $pdf->Output('S'); // 返回字符串,由调用方统一写文件 }第二个习惯是批量生成后抽检——找个命令行工具批量提取 PDF 文本和页数,确认页数没少、中文没乱码,再交给下游。某次我用 FPDF 做了四十多份报价单,文件名和内容都检查过了,唯独页面总数比预期少了两页,后来发现是数据里有一条超长文本把版式撑破了,MultiCell溢出到页面外直接不渲染。那之后我习惯生成完先跑一轮文本提取与页数统计,付出几分钟,省掉的是交付后整段返工的血泪教训。希望这篇笔记能帮你把 FPDF 中文这条路一次走通。
本文还有配套的精品资源,点击获取