直接进入正题。在PHP项目里做“导出PDF”这个需求,我前前后后换过好几套方案,从最早用浏览器打印、到后来上wkhtmltopdf、再到试过TCPDF,最后固定下来用mpdf。今天就把mpdf这一路踩过的坑、用顺手的写法、以及怎么把它调到又快又稳的经验,一次性说清楚。文章会比较长,但每一段都是从实际项目里掏出来的东西,照着抄基本不会翻车。
1. 为什么选mpdf:几个PDF库的横向对比
1.1 市面主流PHP PDF生成方案盘点
在动手写代码之前,先聊聊方案选型。很多新手一上来就问“哪个库最好”,实际上没有最好,只有最合适的。截至我写这篇文章的时间点,PHP生态里能拿来生成PDF的主流方案就那几个:FPDF、TCPDF、Dompdf、wkhtmltopdf,以及今天的主角mpdf。
FPDF年代比较久远,轻量是真轻量,但画表格全靠手写坐标,写复杂布局能把人写到怀疑人生。TCPDF比FPDF功能强不少,支持更多PDF特性,但API风格偏底层,做一套带样式的企业合同要写几百行计算代码。Dompdf走的是“HTML转PDF”路线,对CSS支持比前两个好太多,但对Flexbox、Grid这种现代布局基本无能为力,而且渲染长文档时内存消耗很吓人。wkhtmltopdf另辟蹊径,用无头浏览器内核渲染HTML,效果最接近浏览器,问题是它依赖系统binary,在服务器上部署麻烦,字体渲染偶尔还会出现奇怪的偏移。
mpdf比较讨巧,它也是“HTML转PDF”思路,但内核针对CSS 2.1做了深度优化,同时支持CSS3的一部分特性,最关键的是它对中文支持做得很好,内置了字体处理方案。这点对国内开发者来说几乎是杀手级优势。
1.2 mpdf的核心优势与适用边界
mpdf的核心优势我总结成三点:第一,上手成本极低。你只要会写HTML和CSS,就能生成PDF,不需要学习任何底层绘制API,从零到出一个能看的PDF文件,半小时足够了。第二,对中文极其友好。它支持通过字体文件(ttf/otf)自定义字体,配合自动字体检测和字符回退(font fallback)机制,基本解决乱码和豆腐块问题。第三,输出控制能力很强。页眉页脚、页码、目录、水印、加密、压缩这些功能全是内置的,不需要额外拼轮子。
但mpdf也不是万能的。它对JavaScript完全不支持,复杂的CSS3动画、Flexbox布局会被忽略,所以高保真还原页面这事别指望它。另外,如果PDF里要放大量高清图片,内存占用会直线上升,这一点在后面性能优化部分我会详细讲。如果你的项目主要目标是生成报表、合同、订单确认单、发票这种结构化文档,那mpdf完全够用;如果要做的是高保真的产品手册、杂志排版,那还是老老实实走HTML转图片或者直接用设计工具出PDF。
我的建议是:内容以文字、表格、简单图片为主,且需要服务端批量生成,优先选mpdf。这个定位在接下来所有的代码示例里都适用。
2. 环境准备与安装:Composer一条龙
2.1 环境要求与Composer安装
我用的是mpdf的8.x版本,PHP环境要求7.4以上,8.0、8.1、8.2都测过,没问题。如果你还在跑PHP 7.2之前的版本,那得用mpdf 7.x的旧版本,但生产环境别再用那么老的PHP了,现在全是安全漏洞。
安装很简单,项目根目录执行一条命令:
composer require mpdf/mpdf2.2 环境自检与常见坑位排查
装完之后先别急着写业务代码,跑一个环境自检脚本,确认扩展和目录权限都没问题:
<?php require __DIR__ . '/vendor/autoload.php'; // 检查PHP版本 if (version_compare(PHP_VERSION, '7.4.0', '<')) { die('PHP版本过低,需要7.4及以上'); } // 检查必须的扩展 $requiredExts = ['mbstring', 'gd', 'dom']; foreach ($requiredExts as $ext) { if (!extension_loaded($ext)) { die("缺少扩展: {$ext}"); } } // 检查临时目录 $tmpDir = sys_get_temp_dir(); if (!is_writable($tmpDir)) { die("临时目录不可写: {$tmpDir}"); } // 尝试生成一个测试PDF $mpdf = new \Mpdf\Mpdf(['mode' => 'utf-8', 'format' => 'A4']); $mpdf->WriteHTML('<h1>环境正常</h1>'); $output = $mpdf->Output('', 'S'); if (strlen($output) > 1024) { echo "环境检测通过,PDF字节数: " . strlen($output); } else { echo "PDF生成异常,文件过小"; }这里有个坑:mbstring扩展必须装,mpdf内部对UTF-8字符串处理依赖它。还有,如果你在宝塔面板这类图形化管理工具里搞环境,记得检查一下PHP命令行版本和Web运行版本是不是同一个,很常见的情况是Web端PHP版本正常,CLI版本还是老古董,导致Composer装包时报版本错误。
另外一个容易忽略的点是临时目录。mpdf默认用sys_get_temp_dir()做缓存,有些虚拟主机把临时目录设成不可写,PDF生成就会莫名其妙失败。建议在生产环境显式指定一个可控制的缓存目录:
$mpdf = new \Mpdf\Mpdf([ 'tempDir' => __DIR__ . '/storage/mpdf_tmp', 'mode' => 'utf-8', ]);tempDir这个参数必须要保证目录可写,否则运行时会报“Cannot write to tempDir”之类的错,我第一次遇到这个问题时排查了好久。
3. 核心实操:从“Hello World”到真实业务
3.1 第一段可用的PDF生成代码
直接贴一段最快能跑的代码,生成一个最简单的PDF文件:
<?php require __DIR__ . '/vendor/autoload.php'; use Mpdf\Mpdf; // 初始化,A4纸,UTF-8编码 $mpdf = new Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'margin_left' => 15, 'margin_right' => 15, 'margin_top' => 16, 'margin_bottom' => 16, ]); // 写入HTML内容 $html = '<h1>测试PDF</h1><p>这是一段测试文字,Hello mpdf。</p>'; $mpdf->WriteHTML($html); // 输出到浏览器 $mpdf->Output('test.pdf', 'D');这段代码里Output()的第二个参数是关键,它有四种模式:
| 参数 | 行为 | 适用场景 |
|---|---|---|
D | 强制下载 | 导出文件给用户下载 |
I | 浏览器内联预览 | 在线预览PDF |
F | 保存到服务器文件 | 批量生成、附件存档 |
S | 返回PDF字符串 | 进一步处理、邮件发送 |
实际开发中我经常用F模式先存服务器,再返回路径给前端做下载,这样既能复用文件又避免一次请求占用过多内存。
3.2 用视图模板渲染PDF:把业务代码和样式分离
写业务代码时最忌讳把HTML全都堆在控制器里,维护起来太痛苦。我惯用的做法是“模板文件分离”,用PHP内置的ob_start()把视图渲染到缓冲区,再喂给mpdf。比如这样:
<?php // PdfService.php public function exportOrder($orderId) { $order = OrderModel::find($orderId); // 渲染视图模板 ob_start(); require resource_path('views/pdf/order.blade.php'); $html = ob_get_clean(); $mpdf = new \Mpdf\Mpdf(config('mpdf.default')); $mpdf->WriteHTML($html); $mpdf->Output("order_{$orderId}.pdf", 'D'); }对应的order.blade.php(或者你项目里的任何模板引擎),就当作普通HTML页面来写,变量用PHP原生语法输出。这样做的好处是:美工可以只改模板文件,而不会碰到业务逻辑,测试时也只需要单独打开模板调样式。
模板里有一类细节要特别提醒:CSS要内联或写在<style>标签里。mpdf支持解析<style>标签,但如果你的模板引用了外部CSS文件(<link rel="stylesheet">),需要注意路径问题,mpdf默认不会加载外部样式表,除非用WriteHTML($html, 0, true, true)这种方式并正确配置路径。为了省事,我都是把模板里用到的CSS直接写在<style>里,PDF模板不像网页那样追求复用,内联反而最稳定。
3.3 中文字体的配置:乱码问题的根治方案
mpdf默认字体里包含一些基础字符,但直接渲染中文会变成方框或乱码。要正确显示中文,需要手动加载一个中文字体。下面是经过多次实践后的推荐配置:
$mpdf = new \Mpdf\Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'mssong', 'fontDir' => [resource_path('fonts/')], 'fontdata' => [ 'mssong' => [ 'R' => 'NotoSansSC-Regular.ttf', 'B' => 'NotoSansSC-Bold.ttf', // 或者用仿宋/黑体等字体 ], ], ]);关于中文字体的选择,我个人测试经验如下:
- 思源黑体(Noto Sans SC):效果最好,清晰锐利,对商业项目来说是最安全的选择,文件体积略大。
- 微软雅黑(msyh.ttf):在Windows上直接获取方便,渲染效果也算顺眼,但它的字体文件有版权问题,商用要谨慎。
- 仿宋/楷体:适合合同、公文类场景,有正式的书面感。
- 宋体(SimSun):在DejaVu、FreeSerif缺字时作为兜底还行,但现在新项目我基本不用它做正文了。
字体文件放置的路径要和fontDir保持一致,别放错目录。另外提醒一下,同一个字体族最好R和B两种字重都放进去,否则加粗的文字在PDF里没法正常显示,看起来就没有对比效果。
配置好字体之后,HTML里直接写font-family: 'mssong';就能正常渲染中文。如果不指定字体,mpdf会走fallback逻辑,也可能会自动匹配默认字体里的CJK字符映射,但总归不够稳定,手动指定最靠谱。
3.4 页眉页脚与页码:一个必会的加分项
做合同或报告类PDF,页码几乎是刚需,页眉放公司名和文档标题也很常见。mpdf提供了内置的页码变量,不需要手工计算:
$mpdf = new \Mpdf\Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'margin_top' => 25, 'margin_bottom' => 25, ]); // 页眉 $mpdf->SetHeader('{PAGENO}'); // 页脚 $mpdf->SetFooter('第 {PAGENO} 页 / 共 {nb} 页');注意:如果你要用{nb}获取总页数,初始化MPDF对象时务必设置'format'和'margin_*'等参数,因为它们会影响分页计算。更关键的是,要提前在构造函数参数里加上'setAutoBottomMargin' => 'stretch'或者'setAutoTopMargin' => 'stretch',页眉页脚才不会和正文重叠。
如果想在页眉放公司Logo,可以用SetHeader()的数组写法:
$mpdf->SetHeader([ 'L' => ['content' => 'XX科技有限公司', 'font-size' => 9], 'C' => ['content' => date('Y-m-d'), 'font-size' => 9], 'R' => ['content' => '{PAGENO}', 'font-size' => 9], 'line' => 1, ]);这套写法几乎覆盖了所有正式文档的页眉需求,分别在左、中、右放不同内容,还自动带一条下划线。
4. 进阶玩法:用CSS布局写出好看的PDF
4.1 表格布局:一个能打的案例
mpdf对<table>的支持比Dompdf好很多,可以认认真真做一份看上去“正规”的对账单或库存表。下面这段代码是我做订单明细时的常用模板结构:
<style> .invoice-box { width: 100%; border-collapse: collapse; font-family: 'mssong'; font-size: 10pt; } .invoice-box table { width: 100%; border: 1px solid #ddd; } .invoice-box th { background-color: #f2f2f2; padding: 8px; border-bottom: 1px solid #ddd; text-align: left; } .invoice-box td { padding: 8px; border-bottom: 1px solid #eee; } .text-right { text-align: right; } .total-row td { font-weight: bold; background-color: #fafafa; } </style> <div class="invoice-box"> <table> <thead> <tr> <th>商品名称</th> <th>单价</th> <th>数量</th> <th>小计</th> </tr> </thead> <tbody> <?php foreach ($items as $item): ?> <tr> <td><?= htmlspecialchars($item['name']) ?></td> <td class="text-right">¥<?= number_format($item['price'], 2) ?></td> <td class="text-right"><?= $item['qty'] ?></td> <td class="text-right">¥<?= number_format($item['price'] * $item['qty'], 2) ?></td> </tr> <?php endforeach; ?> </tbody> <tfoot> <tr class="total-row"> <td colspan="3">合计</td> <td class="text-right">¥<?= number_format($total, 2) ?></td> </tr> </tfoot> </table> </div>这里有个非常实用的经验:在表格里尽量避免使用margin和padding之外的高级布局方案。mpdf的表格引擎对colspan、rowspan支持良好,但对position: absolute、float等属性支持是个谜,有时候样式会失效。所以,做PDF模板时使用最传统的表格布局反而是最稳的方案。
另外注意,给table定义了border-collapse: collapse之后,如果发现边框冲突,可以给每个单元格单独设border,通常在th和td上都补一层样式就解决了。我之前遇到过一次表格边框时有时无的问题,排查下来发现是没有给tbody里的每个td单独指定边框样式,加了border: 1px solid #ddd解决。
4.2 水印的处理:给商业文档加个身份标识
很多场景需要给PDF加水印,比如“预付款合同”、“内部资料”等。mpdf加上水印的方法非常简单:
$mpdf->SetWatermarkText('内部资料', 0.15, 'F'); $mpdf->showWatermarkText = true;如果你想用图片水印(比如公司Logo),也很简单:
$mpdf->SetWatermarkImage('/path/to/logo.png', 0.2, 'F', 'F'); $mpdf->showWatermarkImage = true;这里有两个参数容易被忽略:透明度系数和模式。透明度的取值范围是0到1,值越小越淡,我一般用0.15到0.25之间,太淡了没效果,太深了影响阅读。第三个参数'F'代表重复铺满整页,默认是单次居中。如果只需要出现在某一页,那就别在构造函数里设,而是用WriteHTML的页面级别控制,比如在指定页插入再关闭。
水印还有一个坑:如果文本水印包含中文,一定要确保所用的默认字体支持这个汉字。没配中文字体就直接写中文水印,结果会是一堆方框,别问我是怎么知道的。
4.3 分页控制:别让表格被拦腰截断
长表格的分页是个隐蔽的问题,很多新手做出来的PDF在跨页时表头没了,或者某一行被裁到上一页底部。mpdf有个属性专门控制这个:
$html = '<style>.avoid-break { page-break-inside: avoid; }</style>'; $html .= '<table class="avoid-break">...</table>';更细一点的做法是给每一行加上page-break-inside: avoid;,避免单行被截断成两半:
<style> tr { page-break-inside: avoid; } thead { display: table-header-group; } </style>display: table-header-group;会强制每页都重复渲染表头,这样跨页表格不至于让人看了标题忘了列名。这是做长报表必备的一个小技巧,我在做导出对账明细时深有体会,不加这行跨页后的表格看起来莫名其妙。
如果你还想让某些内容强制换到新的一页,直接用page-break-before: always;或者<pagebreak />标记。
5. 性能与资源:批量导出和超大PDF的优化方案
5.1 内存问题:一次导出10000行数据的实战调优
批量导出报表时,最头疼的就是内存爆掉。mpdf加载HTML后会把中间文档树保存在内存里,数据量一大,几百MB内存就没了。这里我给出一套可行的优化策略,按严格程度递进:
第一级:设置合理的执行时间和内存限制
set_time_limit(300); ini_set('memory_limit', '512M');这种方式简单粗暴,但治标不治本。对大多数中小型项目够用,但如果你要导出几万行数据,还是得继续往下看。
第二级:分批写HTML,避免一个超长字符串
不要一次性把几万行数据拼成一个完整的HTML字符串再WriteHTML。改成循环分批写入:
$mpdf = new \Mpdf\Mpdf(config('mpdf')); $batchSize = 1000; // 先写表头 $headerHtml = '<table><thead><tr><th>ID</th><th>名称</th></tr></thead><tbody>'; $mpdf->WriteHTML($headerHtml); // 分批写数据 foreach (array_chunk($data, $batchSize) as $chunk) { $rowHtml = ''; foreach ($chunk as $item) { $rowHtml .= "<tr><td>{$item['id']}</td><td>{$item['name']}</td></tr>"; } $mpdf->WriteHTML($rowHtml); } $mpdf->WriteHTML('</tbody></table>');分批写的好处是整个HTML文档树不会一次性压进内存,每WriteHTML一次就渲染一次。注意:分批写的方案要求HTML结构必须闭合合法,表格后半部分的</tbody></table>要在最后补上。mpdf内部会等待表格闭合后再输出,所以中间写单行的tr是可以的。
第三级:关掉额外的智能处理
mpdf有些功能默认开启,但在大数据量下非常占用资源,可以按需关闭:
$mpdf = new \Mpdf\Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'percentSubset' => 0, // 不自动子集化字体,虽然文件稍大但速度快 'autoLangToFont' => false, // 关闭自动语言检测 ]);autoLangToFont这个选项会扫描每个字符并做语言推断,在包含大量混合语言文本时性能开销很大。如果你确定文档就是纯中文+英文+数字,直接关掉即可。
5.2 输出压缩与文件体积
mpdf生成的PDF默认会做一些压缩优化,但如果你觉得文件偏大,可以调整:
$mpdf->PDFA = false; // PDF/A模式占用体积大,非必须别开 $mpdf->shrink_tables_to_fit = 1; // 自动缩放表格 $mpdf->use_kwt = true; // keep with table,避免表格行被拆开关于图片,尽量用JPG而不是PNG。同样的视觉质量,JPG体积能小一个数量级。如果需要透明的Logo,可以单独用PNG,但整体背景图最好压缩成JPG上传。还有一个冷知识:图片在放入HTML前先等比缩放成目标尺寸,不要在HTML里用宽高拉伸。比如你在PDF里显示宽度只有200px,却传了2000px的图,mpdf虽然能显示但内部会保存原图数据,文件体积直接膨胀。我在做产品图册导出时就踩过这个坑,优化前一个PDF 30MB,把图片先缩放到目标尺寸后直接降到4MB。
5.3 批量生成的并发控制
如果业务场景是后台一次性导出几百个PDF,需要注意并发问题。mpdf不是线程安全的,在同一个PHP进程里连续实例化多个Mpdf对象没问题,但如果你用多进程或任务队列并行处理,就要保证每个进程独立设置自己的tempDir,避免多个进程同时写同一个缓存目录造成文件冲突。
我通常的做法是给每个任务分配一个独立子目录:
$tmpDir = storage_path("mpdf_tmp/" . uniqid('', true)); if (!is_dir($tmpDir)) { mkdir($tmpDir, 0755, true); } $mpdf = new \Mpdf\Mpdf(['tempDir' => $tmpDir]);任务结束后清理掉这个目录,既安全又干净。
6. 常见问题与排查实录
6.1 问题速查表:按症状定位
为了让你能快速对照,我整理了一张问题排查表。这些都是在各种项目里真实遇到过的:
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 中文乱码或方框 | 未配置中文字体 | 按2.1节配置fontDir和fontdata |
| 生成的PDF空白 | HTML内容为空或未调用WriteHTML | 检查模板变量是否赋值,输出前先var_dump |
| 表格跨页被截断 | 缺少page-break规则 | 给tr加page-break-inside: avoid,给thead加display: table-header-group |
| CSS样式不生效 | 使用了mpdf不支持的CSS特性 | 尽量用table布局,避免flex/grid/绝对定位 |
| 下载文件名乱码 | 文件名包含中文,未做URL编码 | 使用rawurlencode()处理文件名 |
| 内存耗尽 | 数据量过大或HTML一次性拼太长 | 分批WriteHTML,调大内存限制 |
| 页脚页码显示为0 | 未开启SetFooter或变量名写错 | 确认使用{PAGENO}和{nb},并设置页码初始值 |
| 输出到浏览器直接乱码 | PHP文件自身有BOM头 | 去掉BOM,或输出PDF前不要有任何输出 |
| 批量生成时文件互相覆盖 | 同一文件名并发写入 | 文件名加时间戳或uniqid |
| 图片不显示 | 路径错误或远程图片被禁止 | 使用绝对路径或file_get_contents读取后嵌入 |
6.2 几个容易被忽略的“小”细节
先说出PDF文件前不能有任何输出。任何一个空格、BOM、UTF-8 BOM头,都会导致Output()出来的PDF打不开,报“文件已损坏”或“不是有效的PDF文件”。用Output('','D')时尤其要检查控制器里有没有提前输出过调试信息。
再说htmlspecialchars的使用。凡是用户输入的内容,或者库表里取出来的字符串,在拼到HTML模板时一定要过一遍htmlspecialchars(),否则用户传一个<script>或者带特殊字符的内容进去,mpdf轻则解析错乱重则出现跨页异常。这个跟网页防XSS是一个道理,但在PDF里它体现为乱码和布局错乱。
还有一个关于“远程图片”的细节。mpdf默认可以用<img src="https://...">,但出于安全考虑很多生产环境的allow_url_fopen被关了,导致远程图片加载失败。稳妥做法是先把图片下载到本地(或者用file_get_contents),转成Base64再嵌进去:
$imgData = base64_encode(file_get_contents($imageUrl)); $imgSrc = 'data:image/jpeg;base64,' . $imgData;不过要注意,Base64嵌入会把文件体积放大三分之一,大量图片时务必配合前面的压缩步骤。
6.3 调试利器:把HTML先输出到浏览器
每次调样式都要重新生成PDF、下载、打开、翻页,效率太低了。我常用的调试手法是:先别走Output,直接把HTML渲染成普通网页在浏览器里看布局。做法是封一个debug方法:
public function debugView($viewName, $data = []) { ob_start(); extract($data); include $viewName; return ob_get_clean(); }然后在浏览器里直接渲染这个模板,用F12调整CSS,等样式差不多了再交给mpdf测试。虽然浏览器渲染和mpdf渲染不是100%一致,但90%的场景下布局思路是一致的,至少能快速发现表格列宽、字体大小这些常规问题。这个习惯能帮你节省大量重复下载PDF的时间。
7. 内部集成场景:接口、队列与二次开发
7.1 把PDF生成封装成独立服务类
真实项目里,不要把new \Mpdf\Mpdf()散落在各处,最好统一封装成一个服务类。我平时这样写:
<?php namespace App\Services; class PdfExportService { protected $config; public function __construct(array $config = []) { $this->config = array_merge([ 'mode' => 'utf-8', 'format' => 'A4', 'default_font' => 'mssong', 'fontDir' => [storage_path('fonts')], 'fontdata' => [ 'mssong' => [ 'R' => 'NotoSansSC-Regular.ttf', 'B' => 'NotoSansSC-Bold.ttf', ], ], ], $config); } public function make($html, $options = []) { $mpdf = new \Mpdf\Mpdf(array_merge($this->config, $options)); $mpdf->WriteHTML($html); return $mpdf; } public function download($html, $filename) { $mpdf = $this->make($html); $mpdf->Output($filename, 'D'); } public function save($html, $filePath) { $mpdf = $this->make($html); $mpdf->Output($filePath, 'F'); return $filePath; } }这样一个服务类可以被控制器直接调用,也能放到队列任务里用。在接口场景中,前端通过AJAX请求导出接口,后端生成PDF后返回文件URL,比直接返回二进制流更稳定,也不容易触发代理超时。
7.2 队列化:把大导出丢到后台去
当一次导出花费超过3秒,我建议直接上队列。前端点击“导出”后,把这个任务推送到队列,后台异步生成PDF,生成完成后通知用户下载。这样页面的响应速度体验会好很多。代码结构大致这样:
public function queueExport($params) { // 推送任务 dispatch(new ExportPdfJob($params)); // 告知用户已受理 return response()->json(['code' => 0, 'msg' => '导出任务已受理,完成后通知下载']); } // 任务里执行 public function handle() { $html = $this->buildHtml($this->params); $filePath = app(PdfExportService::class)->save($html, $this->targetPath); // 记录到数据库或发通知 }队列化之后,配合前面的性能优化,百万级的CSV、Excel、PDF导出都不在话下。
7.3 与前端联调:预览、直下还是传URL?
PDF的最终展示方式,我见到过三种:
- 直接D下载:适合用户明确需要保存的场景,方法最简单,但用户体验一般。
- I内联预览:适合浏览器里先看看再决定是否下载,
Output($filename, 'I')即可,但需要注意浏览器PDF插件版本。 - F保存到服务器,前端拿URL:适合先异步生成再通知下载,或者文件还要存到OSS/CDN的场景。
第三种在现在的Web架构下最常用,因为很多项目的前端和后端已经是分离的,通过Nginx动态读取或上传OSS都能很好地控制访问权限。用OSS的话,直接先生成临时文件再上传,再把OSS URL返回给前端,都是顺理成章的事。
8. 扩展阅读与个人心得
mpdf本身还提供了一些高端特性,比如PDF/A归档模式(适合长期保存的正式文件)、数字签名支持(需要配合php openssl扩展,可以给PDF加签名)、附件嵌入(可以在PDF里附加XML或其他文件)。如果业务需要达到打印店级别或电子发票级别的合规要求,这些功能是绕不开的。
顺手也提一句,网上经常有人问“用mpdf怎么给PDF模板里的指定区域填变量”,其实mpdf没有非常成熟的表单域绑定功能,它更适合“动态生成整篇HTML文档再转PDF”。如果需求是做“套打”或“填空式打印”,建议改为生成HTML表单再由浏览器打印,或者用PDF表单库FPDI去填写已有模板文件,这条路更省力。
我个人在实际项目里最满意的一次优化,是把原来需要8秒才能导出的对账报表压缩到了1.5秒以内。核心改动只有三件事:分批WriteHTML替代一个大字符串、图片先做服务端压缩、关闭自动语言检测。所以如果你现在正被PDF导出慢、内存大折磨,优先从这三个方向动手,基本立竿见影。
最后再分享一个小技巧。mpdf模板块的面包屑调试信息默认在Mpdf\Debug里可以开,遇到非常诡异的排版问题(比如某行莫名的分页),可以在构造函数里设置'debug' => true,然后在输出时看生成的日志,能定位到具体是哪个容器触发了换页。这个功能生产环境记得关掉,否则调试信息会漏进PDF里。
好了,关于PHP使用mpdf生成PDF这件事,能踩的坑和该用的招,这篇文章基本都覆盖到了。按着流程走一遍,再结合自己的业务场景调一调,剩下的大概率只是细节上的美化问题了。祝各位导出PDF一次成功,不用再和乱码、内存、表格截断这三座大山搏斗。