如何使用tFPDF或PHP代码为PDF添加EN16931标准XML附件
解决方案
tFPDF 原生未提供PDF附件操作能力,无需替换现有PDF生成组件,按以下两种方案实现即可,均兼容EN 16931电子发票的XML附件挂载要求。
纯PHP实现(优先推荐)
直接扩展tFPDF类实现符合PDF 1.4+规范的嵌入式附件挂载,不需要依赖任何外部工具,对现有业务代码侵入极小:
- 自定义子类继承原有tFPDF,新增附件注册逻辑,重写PDF结构输出方法,在文档尾部写入附件节点,同时将XML标记为文档关联附件(EN 16931校验强制要求项,普通挂载的注释类附件无法通过校验)。
- 可直接复用的扩展类代码:
class TFPDFWithAttachment extends tFPDF { protected $attachments = []; protected $attachmentNameTreeObj = 0; /** * 添加待挂载的附件 * @param string $filePath 附件本地路径 * @param string $fileName 附件在PDF中显示的文件名 * @param string $description 附件描述 */ public function addAttachment(string $filePath, string $fileName = '', string $description = 'EN 16931 Invoice XML'): void { if (!is_file($filePath)) { throw new InvalidArgumentException("Attachment file not found: {$filePath}"); } if (!$fileName) { $fileName = basename($filePath); } $content = file_get_contents($filePath); $this->attachments[] = [ 'name' => $fileName, 'desc' => $description, 'content' => $content, 'size' => strlen($content), 'obj' => 0, 'fsObj' => 0 ]; } /** * 输出附件相关PDF对象 */ protected function _putAttachments(): void { if (empty($this->attachments)) { return; } foreach ($this->attachments as $idx => $att) { // 写入嵌入式文件流对象 $this->_newobj(); $this->attachments[$idx]['obj'] = $this->n; $this->out('<< /Type /EmbeddedFile /Subtype /text/xml /Length ' . $att['size'] . ' >>'); $this->out('stream'); $this->out($att['content']); $this->out('endstream'); $this->out('endobj'); // 写入文件规范对象 $this->_newobj(); $this->attachments[$idx]['fsObj'] = $this->n; $this->out(sprintf( '<< /Type /Filespec /F (%s) /UF (%s) /EF << /F %d 0 R >> /Desc (%s) >>', $this->_escapeStr($att['name']), $this->_escapeStr($att['name']), $this->attachments[$idx]['obj'], $this->_escapeStr($att['desc']) )); $this->out('endobj'); } // 写入嵌入式文件名称树 $this->_newobj(); $this->attachmentNameTreeObj = $this->n; $nameEntries = []; foreach ($this->attachments as $att) { $nameEntries[] = sprintf('(%s) %d 0 R', $this->_escapeStr($att['name']), $att['fsObj']); } $this->out('<< /Names [' . implode(' ', $nameEntries) . '] >>'); $this->out('endobj'); } /** * 重写资源输出方法,追加附件节点 */ protected function _putresources(): void { parent::_putresources(); $this->_putAttachments(); } /** * 重写文档目录输出,关联附件 */ protected function _putcatalog(): void { parent::_putcatalog(); if (!empty($this->attachments)) { $afList = []; foreach ($this->attachments as $att) { $afList[] = $att['fsObj'] . ' 0 R'; } // 标记为关联附件,满足EN 16931校验要求 $this->out('/AF [' . implode(' ', $afList) . ']'); $this->out('/Names << /EmbeddedFiles ' . $this->attachmentNameTreeObj . ' 0 R >>'); } } /** * PDF字符串特殊字符转义 */ protected function _escapeStr(string $str): string { return str_replace(['\\', '(', ')', "\r", "\n"], ['\\\\', '\(', '\)', '', ''], $str); } }
- 使用方式和原tFPDF完全一致,仅需把原代码中实例化tFPDF的代码替换为实例化
TFPDFWithAttachment,在调用Output()方法前,新增一行调用addAttachment()传入XML文件路径即可,生成的PDF无需二次处理即可通过EN 16931合规校验。 - 注意事项:确保待挂载的XML文件为UTF-8编码,不要对XML内容做额外压缩或编码转换,否则会导致发票校验失败。
Linux命令行实现方案
如果不想修改现有tFPDF生成逻辑,可以在PDF生成完成后,通过系统命令行工具追加附件,适合业务逻辑已经稳定、不想改动核心代码的场景:
- 优先选用pdftk工具实现,操作简单、兼容性好:
- 先通过系统包管理器安装pdftk,Debian/Ubuntu系执行
apt install pdftk-java,RHEL/CentOS系执行dnf install pdftk即可。 - 在PHP中通过exec调用命令完成附件挂载:
- 先通过系统包管理器安装pdftk,Debian/Ubuntu系执行
// tFPDF先生成无附件的临时PDF $tempPdf = '/tmp/invoice_temp.pdf'; $pdf->Output($tempPdf, 'F'); // 待挂载的EN 16931 XML文件路径 $xmlPath = '/tmp/invoice_en16931.xml'; // 最终输出的带附件PDF路径 $finalPdf = '/tmp/invoice_final.pdf'; // 执行挂载命令 exec(sprintf( 'pdftk %s attach_files %s output %s', escapeshellarg($tempPdf), escapeshellarg($xmlPath), escapeshellarg($finalPdf) )); // 处理完成后删除临时文件 unlink($tempPdf);
- 注意:pdftk默认挂载的附件不会自动添加EN 16931要求的文档关联标记,挂载完成后需要用qpdf工具补充关联标记,如果对合规性要求高,优先使用纯PHP扩展方案,不需要额外处理标记问题。
- 备选工具:如果服务器无法安装pdftk,也可以使用mupdf工具包自带的mutool命令实现附件挂载,资源占用更低,命令格式为
mutool clean -attach invoice.xml input.pdf output.pdf。
内容的提问来源于stack exchange,提问作者Volker
相关产品推荐
相关产品推荐

