文档:DParts、split / merge 与厂商扩展
Document 模块处理整份 PDF 文档,而不是页面内容。它建立由规范约束的工作流程,用于附加元数据的文档部件层次结构。它按页码范围将 PDF 拆分为多个片段,也将多份 PDF 合并为一份,并在文档目录中注册开发者扩展。
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)的引用形式。只有在测试路径中没有可用映射表时,它才会回退为整数形式。
PdfMerger 与 PdfSplitter 是文档组合的接口层。PdfMerger 会合并多份输入 PDF 的页面对象,对对象重新编号以避免冲突,并重建单一的页面树与交叉引用表。它生成的页面树是平衡的 Pages 节点,包含 Kids 与 Count,并遵循 PDF 为页面树节点定义的可继承属性模型——§7.7.3。PdfSplitter 执行相反操作:它将页码范围提取为独立的 SplitDocument 对象。PageRange 是两者共用的值对象。它以 1 为起始基准,会验证自身边界,并提供 contains()、count() 与 toArray()。
VendorExtensionRegistry、ExtensionsDictionary 与 DeveloperExtensionEntry 用于对文档目录中的开发者扩展字典建模,也就是引擎用来声明超出基础规范的厂商扩展等级的机制。对于同一厂商前缀的冲突性重复注册,此注册表会拒绝并抛出 VendorExtensionRegistryConflictException。CollectionDictionary 与 CollectionSort 用于对 PDF collection(便携式收藏/文件集)的目录项建模。
API 接口
标题为“API 接口”的章节| 类别 | 主要方法 | 角色 |
|---|---|---|
DPart | isLeaf(), hasMetadata(), resolveWithPageObjects(), write() | 不可变的文档部件节点(@since 1.12.0) |
DPartRoot | isEmpty(), write() | Writer 序列化的 DPart 树根(@since 1.12.0) |
PdfMerger | merge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append() | 支持对象重新编号的多份 PDF 合并(@since 1.9.0) |
PdfSplitter | split(), splitEvery(), extractPages() | 按页码范围拆分为 SplitDocument(@since 1.9.0) |
PageRange | contains(int $page), count(), toArray() | 以 1 为起始基准的页码范围值对象 |
MergeResult / SplitResult | isValid(), count(), document(), totalOutputSize() | 组合结果对象 |
VendorExtensionRegistry | 扩展注册 | 开发者扩展注册表(@since 2.2.0) |
ExtensionsDictionary | withEntry(), entries(), isEmpty(), toPdfDictionary() | 不可变的扩展字典构造器(@since 2.0.0) |
CollectionDictionary | toPdfDictionary() | 便携式收藏的目录项(@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 为起始基准且包含端点。叶节点DPart的pages列表则是以 0 为起始基准的页码索引。这两种抽象并不使用同一个索引基准。跨越两者时请显式转换。DPart是readonly的。要建立不同的树,需要构建新节点,而不是修改既有节点。只有在页面对象映射表为空时,resolveWithPageObjects()才会返回整数索引的回退形式。请勿在正式环境输出中依赖该路径。- 对于重复的厂商前缀,
VendorExtensionRegistry会抛出VendorExtensionRegistryConflictException。每个前缀只能注册一次。
拆分与合并的复杂度与页数呈线性关系,主要受解析与对象重新编号限制,而不是模块自身的记录开销。默认参考工作负载位于 1500 ms 墙钟时间 / 64 MB 峰值预算内。大型合并主要受输入总字节数限制。maxTotalBytes 防护机制用于让峰值内存保持有界。其可重现性配置文件为 structural:合并或拆分后的 PDF 会带有全新的 trailer 与 /ID,因此两次执行在结构上相等,但并非逐字节相同。
安全性说明
标题为“安全性说明”的章节PdfMerger::merge() 与 PdfSplitter::split() 会消耗不可信的 PDF 字节。两者在解析之前都会让输入通过 ResourceGuard::assertSize() / assertCount(),以此限制解压缩或对象数量放大型的拒绝服务攻击。请针对部署环境将 maxFiles、maxTotalBytes 与 maxBytes 参数调得更严格,而不是依赖默认值。请将每一份输入 PDF 都视为恶意输入。当来源由用户提供时,请在受限制的 worker 中执行批量组合。关于信任边界,请参阅 /modules/core/security/ 中的引擎威胁模型。
一致性
标题为“一致性”的章节此模块所建立的 DPart 树遵循 ISO 32000-2 §14.12 的文档部件模型,其叶节点的 /Start 与 /End 条目按照同一条款,以指向页面对象的间接引用发出。合并输出使用 §7.7.3 所定义的页面树节点结构。这些是由 src/Document/ 生成、并由 tests/Unit/Document/(DPartTest、DPartRootTest、DPartPageRefTest、DocumentPdfMergerDeepTest、DocumentPageRangeParseDeepTest)验证的实现事实。它们并非端到端 PDF 2.0 一致性声明。整份文档的一致性由 /modules/core/conformance/ 中所述的 oracle 与 golden 套件验证。
另请参阅
标题为“另请参阅”的章节- Core 模块
- Writer 模块——序列化 DPart 树与页面树。
- Metadata 模块——与 DPM 搭配的 XMP。
- Navigation 模块
- 一致性概览
- 引擎安全模型