跳转到内容

文档:DParts、split / merge 与厂商扩展

Document 模块处理整份 PDF 文档,而不是页面内容。它建立由规范约束的工作流程,用于附加元数据的文档部件层次结构。它按页码范围将 PDF 拆分为多个片段,也将多份 PDF 合并为一份,并在文档目录中注册开发者扩展。

Terminal window
composer require nextpdf/core:^3

此模块位于页面内容之上。Graphics 与 Content 发出操作符,而 Document 则在结构层级运作,包括页面树、文档目录和文档部件树。

文档部件DPart)是 PDF 中的逻辑分区。ISO 32000-2 定义了一个 DPart 层次结构,其节点携带文档部件元数据(DPM)。受规范约束的工作流程(例如制药、法律或归档)可以将元数据关联到某个页码子范围,而不是整份文档——§14.12。DPart 是不可变的 readonly 节点:叶节点引用一段连续的页码索引;中间节点则将子 DPart 节点组织成树。DPartRoot 是由 Writer 序列化的树根。叶节点的 /Start/End 条目是指向页面对象的间接引用,而不是页码索引整数——§14.12。DPart::resolveWithPageObjects() 会根据 Writer 提供的页码索引→对象编号映射表执行解析,并返回 /Start(以及可选的 /End)的引用形式。只有在测试路径中没有可用映射表时,它才会回退为整数形式。

PdfMergerPdfSplitter 是文档组合的接口层。PdfMerger 会合并多份输入 PDF 的页面对象,对对象重新编号以避免冲突,并重建单一的页面树与交叉引用表。它生成的页面树是平衡的 Pages 节点,包含 KidsCount,并遵循 PDF 为页面树节点定义的可继承属性模型——§7.7.3。PdfSplitter 执行相反操作:它将页码范围提取为独立的 SplitDocument 对象。PageRange 是两者共用的值对象。它以 1 为起始基准,会验证自身边界,并提供 contains()count()toArray()

VendorExtensionRegistryExtensionsDictionaryDeveloperExtensionEntry 用于对文档目录中的开发者扩展字典建模,也就是引擎用来声明超出基础规范的厂商扩展等级的机制。对于同一厂商前缀的冲突性重复注册,此注册表会拒绝并抛出 VendorExtensionRegistryConflictExceptionCollectionDictionaryCollectionSort 用于对 PDF collection(便携式收藏/文件集)的目录项建模。

类别主要方法角色
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()不可变的文档部件节点(@since 1.12.0
DPartRootisEmpty(), write()Writer 序列化的 DPart 树根(@since 1.12.0
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()支持对象重新编号的多份 PDF 合并(@since 1.9.0
PdfSplittersplit(), splitEvery(), extractPages()按页码范围拆分为 SplitDocument@since 1.9.0
PageRangecontains(int $page), count(), toArray()以 1 为起始基准的页码范围值对象
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()组合结果对象
VendorExtensionRegistry扩展注册开发者扩展注册表(@since 2.2.0
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()不可变的扩展字典构造器(@since 2.0.0
CollectionDictionarytoPdfDictionary()便携式收藏的目录项(@since 2.0.0

执行 composer docs:generate-api-php -- --module=Document 可取得完整的 PHPDoc 表格。

将 PDF 拆分为单页文件并查看结果。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) {
$segment = $result->document($index);
file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);
}
$singlePage = $splitter->extractPages(
file_get_contents('/srv/in/report.pdf'),
new PageRange(2, 4),
);

在明确的输入预算内合并多份 PDF,然后在写出合并结果前先检查其有效性。

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;
use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */
$sources = array_map(
static fn (string $path): string => file_get_contents($path),
glob('/srv/batch/*.pdf') ?: [],
);
$merger = new PdfMerger();
try {
// Bound the merge: at most 50 files, 100 MB total.
$merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);
} catch (PageLayoutException $e) {
throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);
}
if (!$merged->isValid()) {
throw new \RuntimeException('Merged document failed structural validation.');
}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);
  • PdfMerger::merge()PdfSplitter::split() 通过 ResourceGuard 强制应用输入边界。数量或大小超限的输入会抛出异常,不会静默截断。请根据你的工作负载有意调整 maxFiles / maxTotalBytes
  • 空文件列表或空范围列表会抛出 PageLayoutException——这些是配置错误,而不是空结果。
  • PageRange 以 1 为起始基准且包含端点。叶节点 DPartpages 列表则是以 0 为起始基准的页码索引。这两种抽象并不使用同一个索引基准。跨越两者时请显式转换。
  • DPartreadonly 的。要建立不同的树,需要构建新节点,而不是修改既有节点。只有在页面对象映射表为空时,resolveWithPageObjects() 才会返回整数索引的回退形式。请勿在正式环境输出中依赖该路径。
  • 对于重复的厂商前缀,VendorExtensionRegistry 会抛出 VendorExtensionRegistryConflictException。每个前缀只能注册一次。

拆分与合并的复杂度与页数呈线性关系,主要受解析与对象重新编号限制,而不是模块自身的记录开销。默认参考工作负载位于 1500 ms 墙钟时间 / 64 MB 峰值预算内。大型合并主要受输入总字节数限制。maxTotalBytes 防护机制用于让峰值内存保持有界。其可重现性配置文件为 structural:合并或拆分后的 PDF 会带有全新的 trailer 与 /ID,因此两次执行在结构上相等,但并非逐字节相同。

PdfMerger::merge()PdfSplitter::split() 会消耗不可信的 PDF 字节。两者在解析之前都会让输入通过 ResourceGuard::assertSize() / assertCount(),以此限制解压缩或对象数量放大型的拒绝服务攻击。请针对部署环境将 maxFilesmaxTotalBytesmaxBytes 参数调得更严格,而不是依赖默认值。请将每一份输入 PDF 都视为恶意输入。当来源由用户提供时,请在受限制的 worker 中执行批量组合。关于信任边界,请参阅 /modules/core/security/ 中的引擎威胁模型。

此模块所建立的 DPart 树遵循 ISO 32000-2 §14.12 的文档部件模型,其叶节点的 /Start/End 条目按照同一条款,以指向页面对象的间接引用发出。合并输出使用 §7.7.3 所定义的页面树节点结构。这些是由 src/Document/ 生成、并由 tests/Unit/Document/DPartTestDPartRootTestDPartPageRefTestDocumentPdfMergerDeepTestDocumentPageRangeParseDeepTest)验证的实现事实。它们并非端到端 PDF 2.0 一致性声明。整份文档的一致性由 /modules/core/conformance/ 中所述的 oracle 与 golden 套件验证。