ddn / sapp
简单且通用的PDF解析器(SAPP):使用PHP编辑、操作和签名PDF文档
Requires
- php: >=7.4
README
SAPP代表简单且通用的PDF解析器,它如其名一样:解析PDF文件。它还支持其他酷炫的功能,例如重建文档(使内容更清晰或紧凑)或数字签名文档。
SAPP之所以是通用的,是因为它不关心PDF文档的组成(例如添加页面、更新页面等)。相反,它的目标是作为一个后端来解析现有的PDF文档,并操作其上的对象,或者创建新的对象。
使用SAPP的方式可以在签名文档的函数中看到:它是一个独立的函数,用于添加和操作文档中包含的PDF对象。
SAPP的一些功能
- 支持1.4版本的PDF文档
- 支持1.5版本及以后文档的许多功能(包括交叉引用流)
- 使用增量版本工作
- 用于重建文档以简化版本(删除旧版本)
- 使用Acrobat工作流程签名文档(并获得绿色勾选标记)。
- 其他。
1. 为什么选择SAPP
我创建了SAPP,因为我想要程序化地签名文档,包括 多个签名。
我尝试了tcpdf以及FPDI,但结果并不如预期。当使用FPDI导入文档时,结果是一个包含原始文档页面的新文档,而不是原始文档。因此,如果原始文档已经签名,那些签名就会丢失。
我阅读了有关SetaPDF-Signer的介绍,但它不能免费使用。我还检查了一些命令行工具,如PortableSigner,但它(1)基于Java(我真的很不喜欢Java),(2)依赖于iText和其他库,这些库目前似乎不是免费的。
最后,我获得了PDF 1.7规范,并了解了增量PDF文档及其在文档中包含多个签名时的实用性。
2. 使用SAPP
2.1 使用composer和packagist
要在您的项目中使用SAPP,您只需使用composer
$ composer require ddn/sapp:dev-main
然后,将创建一个vendor
文件夹,然后您可以简单地包含主文件并使用类
use ddn\sapp\PDFDoc; require_once('vendor/autoload.php'); $obj = PDFDoc::from_string(file_get_contents("/path/to/my/pdf/file.pdf")); echo $obj->to_pdf_file_s(true);
2.2 获取源代码
或者您可以克隆仓库并使用composer
$ git clone https://github.com/dealfonso/sapp $ cd sapp $ composer dump-autoload $ php pdfrebuild.php examples/testdoc.pdf > testdoc-rebuilt.pdf
然后您就可以准备包含主文件并使用类了。
3. 示例
在源代码的根目录中,您可以找到两个简单的示例
pdfrebuild.php
:此示例获取PDF文件,加载它并将其重建,以使每个PDF对象按顺序排列,并减少定义文档所需文本的数量。pdfsign.php
:本例演示如何获取PDF文件并使用pkcs12(pfx)证书对其进行数字签名。pdfsigni.php
:本例演示如何获取PDF文件,使用pkcs12(pfx)证书对其进行数字签名,并在文档中添加图像以使签名可见。pdfsignx.php
:另一个示例,获取PDF文件并使用pkcs12(pfx)证书对其进行数字签名,并在文档中添加图像以使签名可见。pdfcompare.php
:本例比较两个PDF文件,并检查它们之间的差异(按对象、字段逐个检查)。
3.1. 使用pdfrebuild.php
重建PDF文件
一旦克隆了仓库并生成了自动加载内容,就可以运行示例
$ php pdfrebuild.php examples/testdoc.pdf > testdoc-rebuilt.pdf
结果是更加有序的PDF文档,可能(problably)更小。(例如,重建examples/testdoc.pdf提供一个50961字节的文档,而原始文档为51269字节)。
$ ls -l examples/testdoc.pdf
-rw-r--r--@ 1 calfonso staff 51269 5 nov 14:01 examples/testdoc.pdf
$ ls -l testdoc-rebuilt.pdf
-rw-r--r--@ 1 calfonso staff 50961 5 nov 14:22 testdoc-rebuilt.pdf
并且
$ cat examples/testdoc.pdf
...
xref
0 39
0000000000 65535 f
0000049875 00000 n
0000002955 00000 n
0000005964 00000 n
0000000022 00000 n
0000002935 00000 n
0000003059 00000 n
0000005928 00000 n
...
$ cat testdoc-rebuilt.pdf
...
xref
0 8
0000000000 65535 f
0000000009 00000 n
0000000454 00000 n
0000000550 00000 n
0000000623 00000 n
0000003532 00000 n
0000003552 00000 n
0000003669 00000 n
...
代码
use ddn\sapp\PDFDoc; require_once('vendor/autoload.php'); if ($argc !== 2) fwrite(STDERR, sprintf("usage: %s <filename>", $argv[0])); else { if (!file_exists($argv[1])) fwrite(STDERR, "failed to open file " . $argv[1]); else { $obj = PDFDoc::from_string(file_get_contents($argv[1])); if ($obj === false) fwrite(STDERR, "failed to parse file " . $argv[1]); else echo $obj->to_pdf_file_s(true); } }
3.2. 使用pdfsign.php
签署PDF文件
要签署PDF文档,可以使用脚本pdfsign.php
$ php pdfsign.php examples/testdoc.pdf caralla.p12 > testdoc-signed.pdf
现在文档已经签署。如果您想添加第二个签名,只需再次签署生成的文档即可
$ php pdfsign.php testdoc-signed.pdf user.p12 > testdoc-resigned.pdf
代码:(完整工作示例)
use ddn\sapp\PDFDoc; require_once('vendor/autoload.php'); if ($argc !== 3) fwrite(STDERR, sprintf("usage: %s <filename> <certfile>", $argv[0])); else { if (!file_exists($argv[1])) fwrite(STDERR, "failed to open file " . $argv[1]); else { // Silently prompt for the password fwrite(STDERR, "Password: "); system('stty -echo'); $password = trim(fgets(STDIN)); system('stty echo'); fwrite(STDERR, "\n"); $file_content = file_get_contents($argv[1]); $obj = PDFDoc::from_string($file_content); if ($obj === false) fwrite(STDERR, "failed to parse file " . $argv[1]); else { if (!$obj->set_signature_certificate($argv[2], $password)) { fwrite(STDERR, "the certificate is not valid"); } else { $docsigned = $obj->to_pdf_file_s(); if ($docsigned === false) fwrite(STDERR, "could not sign the document"); else echo $docsigned; } } } }
3.3. 使用pdfsignx.php
带图像签署PDF文件
要签署包含与签名关联的图像的PDF文档,可以使用脚本pdfsignx.php
$ php pdfsignx.php examples/testdoc.pdf "https://www.google.es/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" caralla.p12 > testdoc-signed.pdf
现在文档已经签署,出现了一个酷炫的图像。如果您想添加第二个签名,只需再次签署生成的文档即可。
代码:(完整工作示例)
use ddn\sapp\PDFDoc; require_once('vendor/autoload.php'); if ($argc !== 4) fwrite(STDERR, sprintf("usage: %s <filename> <image> <certfile>", $argv[0])); else { if (!file_exists($argv[1])) fwrite(STDERR, "failed to open file " . $argv[1]); else { fwrite(STDERR, "Password: "); system('stty -echo'); $password = trim(fgets(STDIN)); system('stty echo'); fwrite(STDERR, "\n"); $file_content = file_get_contents($argv[1]); $obj = PDFDoc::from_string($file_content); if ($obj === false) fwrite(STDERR, "failed to parse file " . $argv[1]); else { $signedDoc = $obj->sign_document($argv[3], $password, 0, $argv[2]); if ($signedDoc === false) { fwrite(STDERR, "failed to sign the document"); } else { $docsigned = $signedDoc->to_pdf_file_s(); if ($docsigned === false) fwrite(STDERR, "could not sign the document"); else echo $docsigned; } } } }
3.4. 使用pdfsigni.php
带图像签署PDF文件
要签署包含与签名关联的图像的PDF文档,可以使用脚本pdfsigni.php
$ php pdfsigni.php examples/testdoc.pdf "https://www.google.es/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" caralla.p12 > testdoc-signed.pdf
现在文档已经签署,出现了一个酷炫的图像。如果您想添加第二个签名,只需再次签署生成的文档即可。
与之前代码的主要区别在于,在这种情况下,签名被视为编辑文档的阶段,但只有在文档生成后才会签署。因此,可以编辑文档(例如,添加文本或图像)并延迟签名,直到最终生成文档。
代码
*省略了与签名和图像出现位置相关的代码,但可以在文件pdfsigni.php
中查看。
... $obj->set_signature_appearance(0, [ $p_x, $p_y, $p_x + $i_w, $p_y + $i_h ], $image); if (!$obj->set_signature_certificate($argv[3], $password)) { fwrite(STDERR, "the certificate is not valid"); } else { $docsigned = $obj->to_pdf_file_s(); if ($docsigned === false) fwrite(STDERR, "could not sign the document"); else echo $docsigned; } ...
3.5. 使用pdfdeflate.php
解压缩任何对象的流
这是一个演示如何操作文档中对象的示例。示例遍历文档中的任何对象,获取其压缩后的流,移除过滤器(即压缩方法),并将其恢复到文档中。这样,对象就不再压缩了(例如,您可以读取绘制元素所使用的命令的纯文本)。
代码
foreach ($obj->get_object_iterator() as $oid => $object) { if ($object === false) continue; if ($object["Filter"] == "/FlateDecode") { /* Getting the stream with raw flag set to "false" will process the stream prior to returning it */ $stream = $object->get_stream(false); if ($stream !== false) { unset($object["Filter"]); /* Setting the stream with raw flag set to "false" will process the stream, although in this case it is not important, because we removed the filter.*/ $object->set_stream($stream, false); $obj->add_object($object); } } } echo $obj->to_pdf_file_s(true);
4. 局限性
目前,主要局限性包括
- 对非零生成PDF对象的初步支持:它们不常见,但根据PDF结构的定义,它们是可能的。如果您发现一个非零生成对象,请发送文档给我,我将尝试支持它。
- 未处理加密文档。
- 当然,还有其他局限性:)
5. 未来工作
我的想法是提供对其他酷炫特性的支持
- 支持TSA时间戳
- 包括文档保护(即锁定文档)
- 文档加密(http://www.fpdf.org/en/script/script37.php)
- 尝试提供写入文本的支持
6. 属性
- 计算签名散列的机制深受tcpdf的启发。
- 读取jpg和png文件是从fpdf中获取的。