跳轉到

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 擴充)

相關資源