在 WMS(仓储管理系统)中有一个工单模块,需要将客户在网页上手写的签名嵌入到 PDF 工单中。签名数据以 base64 格式存储在数据库中,PDF 生成使用 mPDF 6.0。
听起来很简单对吧?base64 data URI 放 <img src> 里,或者存成临时文件用绝对路径引用——任何一个方案都应该能工作。但事实是,这三种常见方式在 mPDF 6.0 面前全都失效了。
问题背景
工单签名的工作流:
用户在浏览器 Canvas 上手写
→ 签名库输出 data:image/png;base64,...
→ 存入数据库 TEXT 列
→ print.php 查询 DB → 嵌入 PDF → mPDF Output
签名数据是这种格式:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...
目标是在 mPDF 6.0 生成的 PDF 中正确显示这个签名图片。
排查:四种方案逐一失效
方案一:Base64 data URI 直接嵌入
最直觉的方案——HTML 的 <img> 天然支持 data URI,mPDF 的 WriteHTML 也号称支持标准 HTML:
$html .= '<img src="' . $signatureDataUri . '" style="width:250px; height:75px;" />';
$mpdf->WriteHTML($html);
结果:PDF 生成成功,但签名位置显示 14×16 像素的小红叉。mPDF 的 _getImage() 方法对 data URI 完全无响应,不报错、不记录、直接回退到 noImageFile 占位图。
方案二:$mpdf->Image() 方法
绕过 WriteHTML,使用 mPDF 提供的 Image() 方法:
$mpdf->Image($signatureDataUri, 10, 20, 60);
结果:同样失败,PDF 中没有嵌入任何图片数据。
方案三:保存为临时 PNG 文件 + 绝对路径
怀疑是 data URI 的问题,改为先保存到临时文件,再通过绝对文件路径引用:
$tmpPath = '/tmp/sig_' . uniqid() . '.png';
file_put_contents($tmpPath, base64_decode($base64Data));
// 方式 A:WriteHTML img 标签
$html .= '<img src="' . $tmpPath . '" ... />';
// 方式 B:Image() 方法
$mpdf->Image($tmpPath, 10, 20, 60);
两种方式都试了。文件确实存在、路径正确、PHP 的 file_exists 返回 true。结果——
结果:PDF 依然没有嵌入图片。用 strings 检查生成的 PDF,找不到任何 PNG 文件头标记。
方案四:改为 JPEG 文件
既然 PNG 不行,那 JPEG 呢?用外部工具把同一张图转为 JPEG:
$tmpPath = '/tmp/sig_' . uniqid() . '.jpg';
$mpdf->Image($tmpPath, 10, 20, 60);
结果:✅ PDF 正确显示了签名图片,文件头标记也出现在 PDF 二进制数据中。
根因分析
| 图片格式 | WriteHTML <img> | $mpdf->Image() | 说明 |
|---|---|---|---|
| PNG data URI | ❌ 红叉 | ❌ 红叉 | mPDF 不处理 data URI |
| PNG 文件路径 | ❌ 红叉 | ❌ 红叉 | 文件存在但拒绝嵌入 |
| JPEG data URI | ❌ 红叉 | ❌ 红叉 | 同上,mPDF 6.0 不支持 data URI |
| JPEG 文件路径 | ✅ 成功 | ✅ 成功 | 唯一可行的路径 |
结论:mPDF 6.0 对 PNG 格式存在兼容性问题,无论是 data URI 还是文件路径都无法正确渲染。JPEG 格式正常工作。
最终方案
由于签名库输出的是 image/png 格式,需要在生成 PDF 时将 PNG 转换为 JPEG:
function saveSigImage($dataUri, $prefix) {
$commaPos = strpos($dataUri, ',');
$base64 = substr($dataUri, $commaPos + 1);
$imgData = base64_decode($base64);
// 用 GD 库将 PNG 转换为 JPEG
$src = @imagecreatefromstring($imgData);
$path = sys_get_temp_dir() . '/' . $prefix . '_' . md5($imgData) . '.jpg';
imagejpeg($src, $path, 85);
imagedestroy($src);
return $path; // 返回绝对路径给 mPDF
}
关键细节:
- 使用
md5($imgData)生成缓存文件名,相同签名不重复转换 - 临时文件存放在
/tmp/,不需要持久化 - 容器需安装 PHP GD 扩展并启用 JPEG 支持
容器依赖配置(Dockerfile):
RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd
为什么这是一个值得记录的坑
| 问题 | 原因 |
|---|---|
| 不报错 | mPDF 对无法加载的图片静默回退到占位图,没有日志、没有异常 |
| 不直观 | PNG 作为通用图片格式,很少人会想到它是导致 PDF 渲染失败的根因 |
| data URI 不支持 | 现代浏览器和很多 HTML→PDF 引擎都支持 data URI,但 mPDF 6.0 确实不支持 |
| 旧版本包袱 | mPDF 6.0(2016 年发布)的图片处理引擎与新版差异很大,Stack Overflow 上的答案多针对新版 |
如果系统使用新版 mPDF(v7+)或改用 TCPDF,这个问题可能不存在。但在遗留系统升级受限的场景下——就像这个跑在 PHP 5.6 上的 WMS——这个排查过程可以帮助少走弯路。