You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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工具实现,操作简单、兼容性好:
    1. 先通过系统包管理器安装pdftk,Debian/Ubuntu系执行apt install pdftk-java,RHEL/CentOS系执行dnf install pdftk即可。
    2. 在PHP中通过exec调用命令完成附件挂载:
// 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:18:20