PDF 差異比較引擎¶
在合約管理、法規遵循審查、文件版本控制等場景中,人工逐頁比對兩個 PDF 版本既耗時又易出錯。NextPDF Pro 的 DiffEngine 提供視覺差異與結構差異雙模式分析,精確定位每一處變更並輸出含邊界框座標的差異報告。
兩種比較模式¶
| 模式 | 分析對象 | 輸出內容 | 適用場景 |
|---|---|---|---|
| 視覺差異 | 頁面渲染結果(像素比較) | 變更區域截圖 + 邊界框座標 | 快速確認外觀變化 |
| 結構差異 | PDF 物件樹與內容串流 | 新增/刪除/修改的文字與物件清單 | 精確追蹤內容變更 |
快速開始¶
use NextPDF\Pro\Diff\DiffEngine;
use NextPDF\Pro\Diff\DiffMode;
use NextPDF\Pro\Diff\DiffOptions;
use NextPDF\Pro\Diff\DiffReport;
$originalPdf = file_get_contents('/contracts/contract-v1.pdf');
$revisedPdf = file_get_contents('/contracts/contract-v2.pdf');
$engine = new DiffEngine(
DiffOptions::create(mode: DiffMode::Structural)
);
/** @var DiffReport $report */
$report = $engine->compare(
original: $originalPdf,
revised: $revisedPdf,
);
echo 'Total changes: ' . $report->getTotalChangeCount();
echo 'Changed pages: ' . implode(', ', $report->getChangedPageNumbers());
foreach ($report->getChanges() as $change) {
printf(
"Page %d | Type: %-10s | Text: %s\n",
$change->getPageNumber(),
$change->getType()->value, // 'added' | 'removed' | 'modified'
$change->getTextSnippet(50), // 前 50 個字元
);
}
視覺差異模式¶
視覺差異模式將每一頁渲染為點陣圖,逐像素比對後標記變更區域:
use NextPDF\Pro\Diff\DiffEngine;
use NextPDF\Pro\Diff\DiffMode;
use NextPDF\Pro\Diff\DiffOptions;
use NextPDF\Pro\Diff\Visual\VisualDiffReport;
use NextPDF\Pro\Diff\Visual\ChangeHighlightColor;
$options = DiffOptions::create(mode: DiffMode::Visual)
->withRenderDpi(150) // 比對解析度(越高越精準,越慢)
->withChangeHighlight(
added: ChangeHighlightColor::Green,
removed: ChangeHighlightColor::Red,
modified: ChangeHighlightColor::Orange,
)
->withMinChangeAreaPx(50) // 忽略小於 50px² 的差異(消除渲染雜訊)
->withOutputAnnotatedPdf(true); // 產生標記差異的 PDF 輸出
$engine = new DiffEngine($options);
/** @var VisualDiffReport $report */
$report = $engine->compare($originalPdf, $revisedPdf);
// 儲存標記差異的 PDF(紅色刪除、綠色新增)
file_put_contents('/output/diff-annotated.pdf', $report->getAnnotatedPdf());
// 取得每頁差異截圖
foreach ($report->getPageDiffs() as $pageDiff) {
if ($pageDiff->hasChanges()) {
$pageDiff->saveComparisonImage(
path: sprintf('/output/page-%d-diff.png', $pageDiff->getPageNumber()),
layout: 'side-by-side', // 'side-by-side' | 'overlay' | 'changed-only'
);
}
}
邊界框座標輸出¶
foreach ($report->getChanges() as $change) {
$bbox = $change->getBoundingBox();
printf(
"Page %d | BBox: x=%.2f y=%.2f w=%.2f h=%.2f | Type: %s\n",
$change->getPageNumber(),
$bbox->getX(), // PDF 座標系原點在左下角
$bbox->getY(),
$bbox->getWidth(),
$bbox->getHeight(),
$change->getType()->value,
);
}
結構差異模式與 ContentStreamParser¶
結構差異模式使用 ContentStreamParser 解析 PDF 內容串流,提取文字操作符並建立可比較的文字物件樹:
use NextPDF\Pro\Diff\ContentStreamParser;
use NextPDF\Pro\Diff\ContentStreamParser\ParsedTextObject;
// 獨立使用 ContentStreamParser 解析頁面文字結構
$parser = new ContentStreamParser();
$parsedOriginal = $parser->parsePage($originalPdf, pageNumber: 1);
/** @var list<ParsedTextObject> $textObjects */
$textObjects = $parsedOriginal->getTextObjects();
foreach ($textObjects as $obj) {
printf(
"Text: %-40s | Font: %-15s | Size: %4.1f | Pos: (%.1f, %.1f)\n",
$obj->getText(),
$obj->getFontName(),
$obj->getFontSize(),
$obj->getX(),
$obj->getY(),
);
}
結構差異報告¶
use NextPDF\Pro\Diff\DiffEngine;
use NextPDF\Pro\Diff\DiffMode;
use NextPDF\Pro\Diff\DiffOptions;
use NextPDF\Pro\Diff\Structural\StructuralDiffReport;
use NextPDF\Pro\Diff\Structural\ChangeType;
$engine = new DiffEngine(
DiffOptions::create(mode: DiffMode::Structural)
->withIgnoreWhitespace(true) // 忽略空白字元差異
->withIgnoreFontChanges(false) // 偵測字型變更(合約場景重要)
->withIgnoreColorChanges(false) // 偵測顏色變更
->withTextMatchThreshold(0.85), // 文字相似度閾值(用於判斷「修改」vs「刪除+新增」)
);
/** @var StructuralDiffReport $report */
$report = $engine->compare($originalPdf, $revisedPdf);
// 分類取得變更
$added = $report->getChangesByType(ChangeType::Added);
$removed = $report->getChangesByType(ChangeType::Removed);
$modified = $report->getChangesByType(ChangeType::Modified);
printf(
"Summary: +%d added, -%d removed, ~%d modified\n",
count($added),
count($removed),
count($modified),
);
複合模式:視覺 + 結構同時執行¶
use NextPDF\Pro\Diff\DiffEngine;
use NextPDF\Pro\Diff\DiffMode;
use NextPDF\Pro\Diff\CompositeDiffReport;
$engine = new DiffEngine(
DiffOptions::create(mode: DiffMode::Composite) // 同時執行兩種模式
);
/** @var CompositeDiffReport $report */
$report = $engine->compare($originalPdf, $revisedPdf);
$visualReport = $report->getVisualReport();
$structuralReport = $report->getStructuralReport();
// 交叉驗證:結構偵測到變更但視覺未顯示差異(可能為詮釋資料變更)
$metadataOnlyChanges = $report->getMetadataOnlyChanges();
差異報告輸出格式¶
JSON 格式¶
$json = $report->toJson(pretty: true);
// 輸出結構:
// {
// "summary": { "added": 3, "removed": 1, "modified": 5, "pages_affected": 4 },
// "changes": [
// {
// "page": 2,
// "type": "modified",
// "original_text": "Payment due within 30 days",
// "revised_text": "Payment due within 14 days",
// "bbox": { "x": 72.0, "y": 445.2, "width": 280.0, "height": 14.0 },
// "confidence": 0.97
// }
// ]
// }
HTML 格式(供人工審閱)¶
$html = $report->toHtml(
includePagePreviews: true,
highlightChanges: true,
cssTheme: 'legal', // 'default' | 'legal' | 'minimal'
);
file_put_contents('/output/diff-report.html', $html);
效能建議¶
| 文件規模 | 建議模式 | 預計時間 |
|---|---|---|
| ≤ 20 頁 | 任意模式 | < 2 秒 |
| 21–100 頁 | Structural(較快) | 2–15 秒 |
| 101–500 頁 | Structural,啟用快取 | 15–120 秒 |
| > 500 頁 | 非同步佇列 + 分頁並行 | 依頁數而定 |
// 大型文件:僅比較指定頁碼範圍
$options = DiffOptions::create(mode: DiffMode::Structural)
->withPageRange(startPage: 1, endPage: 50) // 只比較第 1–50 頁
->withConcurrency(workers: 4); // 並行處理(需 PHP pcntl 擴充)