Pro 版本
Diff — 深度参考
本页是 NextPDF Pro 比对模块 NextPDF\Pro\Diff 的契约级参考。该模块比较两份 PDF 文档并报告文本、图像和元数据的变更。PdfDiffer 生成页面对齐的 Myers 行级比对。StructuredDiffer 增加段落归并、图像比较和元数据比较。DiffFormatter 会将结构化结果序列化为 JSON 或一段 HTML 片段。本页阐明公共 API、可观察的行为契约、资源边界和失败模式。面向任务的设置和示例位于 Diff 能力页。
可用性与授权
标题为“可用性与授权”的章节此能力随 NextPDF Pro(nextpdf/pro)发行,并通过 Pro 级授权信封激活。没有该权益的部署不会加载此能力的类。比较版本并获取授权。
没有任何运行时能力标记对本模块进行门控。只要安装并授权了 nextpdf/pro,diff 类即可使用。
公共 API 接口面
标题为“公共 API 接口面”的章节| 符号 | 参数 | 默认行为 | 返回 | 抛出或失败于 | 说明 |
|---|---|---|---|---|---|
PdfDiffer::compare() | string $sourcePdf, string $targetPdf | 逐页提取文本,然后将源文件的第 i 页与目标文件的第 i 页进行比对 | DiffResult | 当缓冲区缺少 %PDF 头,或可选读取器解析失败时抛出 InvalidArgumentException;触及资源边界时抛出 OverflowException | 静态入口点 |
PdfDiffer::compareTexts() | array $sourcePages, array $targetPages(各为 list<string>) | 比对预先提取的页面文本,跳过提取步骤 | DiffResult | 触及资源边界时抛出 OverflowException | 静态;当文本已就绪时使用 |
PdfDiffer::extractText() | string $contentStream | 从单个原始内容流中解析文本显示运算符 | string | —(容错;无法解析的输入返回空字符串) | 静态 |
StructuredDiffer::__construct() | ?ImageDiffer $imageDiffer = null, ?MetadataDiffer $metadataDiffer = null | null 参数会构造默认的比对器 | — | — | 用于测试的构造函数注入 |
StructuredDiffer::compare() | string $sourcePdf, string $targetPdf | 运行文本、段落、图像和元数据比较,然后构建摘要 | StructuredDiffResult | 从文本路径传播 InvalidArgumentException 和 OverflowException | 整个模块的编排器 |
DiffFormatter::toJson() | StructuredDiffResult $result | 美化打印的 JSON 文档 | string | 编码失败时抛出 JsonException | — |
DiffFormatter::toHtml() | StructuredDiffResult $result | 包含摘要、段落和元数据区块的 HTML 片段;文本值经过实体转义 | string | — | 仅为片段,而非完整文档 |
DiffFormatter::toArray() | StructuredDiffResult $result | 支撑 toJson() 的序列化数组 | array<string, mixed> | — | 稳定的 snake_case 键 |
ImageDiffer::diff() | string $sourcePdf, string $targetPdf | 对图像 XObject 计算哈希,并报告新增、删除和修改的图像 | list<ImageDiff> | —(无法解码的结构以 fail-closed 方式跳过) | 标识为页面桶加对象编号 |
MetadataDiffer::diff() | string $sourcePdf, string $targetPdf | 比较八个 /Info 字段(Title、Author、Subject、Keywords、Creator、Producer、CreationDate、ModDate) | list<MetadataChange> | —(对不符合规范的输入从不抛出) | 以解码后的字符串比较值 |
DiffEngine::diff() | array $sourceLines, array $targetLines, int $pageIndex = 0, int $maxLines = 10000 | 对两个行列表执行 Myers 行级比对 | list<DiffRegion> | 当合并行数超过 $maxLines 或编辑距离超过内存界限上限时抛出 OverflowException | 静态;所有文本路径的区域生产者 |
TextExtractor::fromContentStream() | string $contentStream | 对流进行词法分析并运行文本状态机 | list<TextBlock> | — | 静态 |
TextExtractor::fromOperations() | array $operations(list<ContentStreamOp>) | 对预解析的操作运行文本状态机 | list<TextBlock> | — | 静态 |
ContentStreamParser::parse() | 构造函数接受 string $data | 对运算符和操作数进行词法分析;跳过字典和注释;容错 | list<ContentStreamOp> | — | 无法识别的字节会被跳过,绝不致命 |
ContentStreamOp | string $operator, list<mixed> $operands | 只读的操作值对象;isTextOp() 对文本相关运算符进行分类 | — | — | — |
DiffResult | list<DiffRegion> $regions, int $sourcePagesCount, int $targetPagesCount | 将区域分入 $added、$removed、$modified;暴露 isIdentical()、hasDifferences()、totalChanges() | — | — | 只读;Unchanged 区域仅保留在 $regions 中 |
StructuredDiffResult | 文本比对、段落、图像、元数据变更、摘要 | 聚合结果;hasDifferences()、isIdentical() 委托给摘要 | — | — | 只读 |
DiffSummary | 各类别计数加页面计数 | 对文本、图像和元数据计数执行 hasDifferences() 和 totalChanges() | — | — | 只读 |
DiffRegion | DiffType $type, string $text, int $pageIndex, int $lineIndex, ?string $counterpartText = null | 单个行级变更 | — | — | 在发行版引擎中 $counterpartText 始终为 null |
ParagraphDiff | 类型、文本、页面索引、起止行、区域 | 一页上类型相同的连续区域;lineCount() | — | — | 只读 |
ImageDiff | 类型、页面索引、源哈希、目标哈希、对象 id | 单个图像变更条目 | — | — | 缺失一侧的哈希为空字符串 |
MetadataChange | string $field, ?string $sourceValue, ?string $targetValue | 单个字段变更;isAdded()、isRemoved()、isModified() | — | — | null 表示该字段不存在 |
TextBlock | 文本、x、y、字体名、字号、行索引 | 单个带有近似位置的提取文本块 | — | — | 只读 |
DiffType | 枚举:Added、Removed、Modified、Unchanged | 以字符串为底的文本变更分类 | — | — | 参见行为契约中的 Modified 说明 |
ImageDiffType | 枚举:Added、Removed、Modified、Unchanged | 以字符串为底的图像变更分类 | — | — | — |
入口点签名
标题为“入口点签名”的章节public static function compare(string $sourcePdf, string $targetPdf): DiffResult
public static function compareTexts(array $sourcePages, array $targetPages): DiffResult
public static function extractText(string $contentStream): stringpublic function __construct( ?ImageDiffer $imageDiffer = null, ?MetadataDiffer $metadataDiffer = null,)
public function compare(string $sourcePdf, string $targetPdf): StructuredDiffResultpublic function toJson(StructuredDiffResult $result): string
public function toHtml(StructuredDiffResult $result): string
public function toArray(StructuredDiffResult $result): arraypublic static function diff( array $sourceLines, array $targetLines, int $pageIndex = 0, int $maxLines = self::MAX_DIFF_LINES,): array行为契约
标题为“行为契约”的章节页面对齐与行级比对
标题为“页面对齐与行级比对”的章节PdfDiffer::compare() 会逐页提取文本,然后将源文件的第 i 页与目标文件的第 i 页进行比对。当页数不同时,多出的页面会把缺失的一侧当作空文本处理。在每一对页面内部,文本会按换行符切分,并对每一页运行 Myers 行级比对。引擎会发出 Added、Removed 和 Unchanged 区域。一处被修改的行会表现为一个 Removed 加一个 Added 区域;发行版引擎绝不会发出 Modified 文本区域。由于 DiffResult 的构造函数是公开的,Modified 情形和 DiffResult::$modified 桶服务于调用方自行构造的结果。totalChanges() 会统计新增、删除和修改的区域;不变的区域被排除在外。
提取路径
标题为“提取路径”的章节提取有两条路径:
- 存在可选的 Artisan 读取器。 当安装了可选的
NextPDF\Parser\PdfReader类时,页面内容流会通过它读取,以获得页面级精确的文本。尾部(trailer)的页数驱动循环。读取失败的页面会贡献空文本,而不是中止比较。 - 回退。 一个有界的字节级扫描器通过
strpos定位stream/endstream配对,在硬性 50 MB 输出上限下对 FlateDecode 数据进行解压,并在流字典依照 ISO 32000-2:2020 §7.4.4.4 通过/DecodeParms请求 PNG 预测器时对其进行逆向过滤。格式错误或不受支持的预测器会使解码后的字节保持不变。回退会将所有还原出的文本拼接进单个页面桶,因此仅在读取器路径上页面级对齐才是页面级精确的。
两条路径都会解析 §9.4 的文本显示运算符 Tj、TJ 和 '。状态机会跟踪 BT/ET、Tm(仅原点)、Td/TD、T* 和 Tf。
结构化比较
标题为“结构化比较”的章节StructuredDiffer::compare() 会运行文本比对,将同一页上类型相同的连续区域归并为段落(包括未变更的连续段),然后运行图像与元数据比较并组装一份 DiffSummary。摘要的段落计数只涵盖新增、删除和修改的段落。
图像比较会结构化地枚举 PDF 对象。流体的范围依照 §7.3.8.2 由其 /Length 条目决定,因此仅仅形似对象语法的二进制字节绝不会被登记为幻影对象。压缩对象流(/Type /ObjStm)会依照 §7.5.7 解码,使嵌套其中的图像 XObject 可见。每个检测到的图像都会用非密码学的 xxh128 函数进行内容哈希;标识为页面桶与对象编号的组合。在流顺序中没有归属页面的图像会归到第 0 页。
元数据比较会尽可能通过尾部解析出真正的 /Info 字典,因此内容流中作为诱饵的字段记号不会被误认为文档元数据。字段值会按 PDF 字符串解码:字面量形式依照 §7.3.4.2,十六进制形式依照 §7.3.4.3。若没有可解析的尾部,搜索会回退到整个输入。日期以解码后的字符串进行比较,而非解析后的时间戳。
报告输出
标题为“报告输出”的章节DiffFormatter::toJson() 返回美化打印的 JSON,并以 JSON_THROW_ON_ERROR 编码,因此编码失败会抛出 JsonException,而不是返回 false。toHtml() 返回一个 <div class="nextpdf-diff"> 片段;段落文本和元数据值会经过 HTML 实体转义。没有可视的并排红线 PDF 输出。对于相同的输入,区域和格式化输出是确定性的。
边界情形与失败模式
标题为“边界情形与失败模式”的章节- 页面对齐是位置式的。单个被插入或删除的页面会使其后所有页面的对齐发生偏移,并放大下游的变更计数。
- 在回退提取路径上,所有文本都落在页面索引 0。将读取器提取的文档与来自回退路径的预期进行比对会产生不同的页面归属。
- 不以
%PDF开头的源或目标缓冲区会在任何比较之前以InvalidArgumentException失败。 - 单对页面中合并行数超过 10,000 会以
OverflowException失败(行数界限)。 - 当两页文本共享的行过少、以致 Myers 编辑距离超过内存界限上限时,会以
OverflowException失败。正当的修订会共享大部分行而不受影响;对抗性的低共性输入会触发该界限。 - 回退流解压后的输出大于 50 MB 会以
OverflowException失败(解压炸弹界限)。扫描器使用strpos而非无界正则表达式,因此精心构造的输入无法触发灾难性回溯。 "文本显示运算符在 3.1.0 中会被词法分析,但不产生文本块;仅通过"显示的文本不会参与比对。- 扫描得到的纯图像 PDF 产生很少或没有文本差异。不运行 OCR。
- 图像变更检测是结构性的,而非感知性的。它不会栅格化页面,且当字节不同时,以相同像素重新编码的图像会被报告为已修改。
- 在不同修订之间页面桶或对象编号发生变化的图像会被报告为一对删除加新增,而非修改。
- 以 FlateDecode 以外的过滤器压缩的对象流会以 fail-closed 方式跳过;其成员图像不会被比较。
- 本模块不执行任何密码学操作,因此不存在 FIPS 模式特定行为。图像哈希仅用于变更检测,不承载任何完整性或证据分量。
一致性
标题为“一致性”的章节| 声明 | 标准 | 条款 |
|---|---|---|
解析 Tj 和 TJ 文本显示运算符用于提取 | ISO 32000-2:2020 | §9.4 |
回退流数据从 stream 关键字之后的 CRLF 或 LF 之后开始 | ISO 32000-2:2020 | §7.3.8.1 |
图像扫描的流范围由字典 /Length 条目决定 | ISO 32000-2:2020 | §7.3.8.2 |
对象流成员通过 /N 配对表和 /First 偏移定位 | ISO 32000-2:2020 | §7.5.7 |
PNG 预测器逆向遵循 /DecodeParms Predictor 参数 | ISO 32000-2:2020 | §7.4.4.4 |
| 元数据值解码字面量和十六进制字符串形式 | ISO 32000-2:2020 | §7.3.4.2, §7.3.4.3 |
| 可视的并排红线 PDF 输出 | — | 不支持(仅 JSON/HTML) |
所有条款均为释义;NextPDF 不复制规范性文本。这些是能力声明,而非认证;NextPDF 不持有任何认证,也不授予任何认证。文本还原会从文本显示运算符重建行文本。它并不运行完整的 §9.4 文本状态机,因此比对是内容层级的,而非几何层级的。
开发说明
标题为“开发说明”的章节- 在 Pro 包中的可用性:
PdfDiffer、DiffEngine、TextExtractor及其值对象自 1.8.0 起;StructuredDiffer、DiffFormatter、ImageDiffer、MetadataDiffer及其值对象自 2.2.0 起。全部在nextpdf/pro3.1.0 中为当前状态。 - 当页面文本已就绪时优先使用
PdfDiffer::compareTexts();它完全跳过提取及其失败模式。 - 可选的 Artisan 读取器会提升提取精度和页面归属。它在运行时被检测,且从不为必需。
- 在比对不受信任的输入时捕获
OverflowException;这些界限是有意的 fail-closed 拒绝,而非瞬时错误。 DiffFormatter::toHtml()会输出类名(diff-added、diff-removed、diff-modified、diff-unchanged),但不含样式表;请自行提供 CSS。- 在测试中用桩比对器构造
StructuredDiffer,以将文本路径与图像和元数据扫描隔离开。
发布边界
标题为“发布边界”的章节本页仅记录外部可观察的行为和受支持的公共 API 接口面。内部命名空间路径、辅助类、机制表、运行手册文件名和工单前缀不在范围之内。
另请参阅
标题为“另请参阅”的章节- Diff(能力) — 安装、快速上手和生产示例。
- Converter — 深度参考
- Filter — 深度参考