Skip to content
getnextpdf.com

Pro edition

Diff

NextPDF\Pro\Diff compares two PDF documents and reports what changed. The quick path produces a page-aligned text diff; the structured path adds image and metadata change detection and formats the result as JSON or HTML.

This capability ships in NextPDF Pro (nextpdf/pro) and activates with a Pro-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

No runtime capability flag gates the Diff classes; they are present whenever the Pro package is installed.

Terminal window
composer require nextpdf/pro:^3

PdfDiffer::compare() extracts text per page from each document, splits it into lines, and runs a Myers line diff per page pair, producing added, removed, and modified regions. Text extraction parses the text-showing operators of ISO 32000-2:2020 §9.4 (Tj, TJ, ').

StructuredDiffer builds on that: it groups text regions into paragraph- level changes, compares embedded images, compares metadata, and produces a StructuredDiffResult with an aggregate summary. DiffFormatter serializes that result to a JSON string or an HTML report fragment.

When the optional Artisan PDF reader is installed, text extraction uses it for page-accurate content; otherwise a bounded byte-level fallback scans content streams directly.

The differ compares extracted text and structure, not rendered pixels. A structural comparison is deterministic, cheap, and maps to the editorial changes a reviewer cares about. A pixel diff would instead flag antialiasing and font-hinting noise as content. Because a PDF stores glyphs and positioning, not ready-to-read characters, every comparison first reconstructs text from the content stream. That extraction step is why the Artisan reader sharpens accuracy, why the bounded FlateDecode fallback trades coverage for safety, and why scanned pages barely diff. Page alignment stays index-based for predictability, so an inserted page reads as a clear downstream shift.

Design background: Why the text in a PDF is not really text.

  • Input. Raw PDF bytes for source and target. A buffer not starting with %PDF raises InvalidArgumentException.
  • Output (quick path). DiffResult with added, removed, modified region lists plus isIdentical(), hasDifferences(), totalChanges().
  • Output (structured path). StructuredDiffResult with paragraph diffs, image diffs, metadata changes, and a DiffSummary.
  • Report output. DiffFormatter emits a JSON string or an HTML fragment. It does not produce a visual side-by-side redline PDF.
  • Resource bounds. Decompressed content-stream size is capped to guard against decompression bombs; the byte-level scanner avoids catastrophic regex backtracking on crafted input.
  • Determinism. For identical inputs the diff regions and the formatted output are stable.
TypeKindKey members
NextPDF\Pro\Diff\PdfDifferfinal classstatic compare(string $sourcePdf, string $targetPdf): DiffResult, static compareTexts(array $sourcePages, array $targetPages): DiffResult, static extractText(string $contentStream): string
NextPDF\Pro\Diff\StructuredDifferfinal class__construct(?ImageDiffer $imageDiffer = null, ?MetadataDiffer $metadataDiffer = null), compare(string $sourcePdf, string $targetPdf): StructuredDiffResult
NextPDF\Pro\Diff\DiffFormatterfinal classtoJson(StructuredDiffResult $result): string, toHtml(StructuredDiffResult $result): string
NextPDF\Pro\Diff\DiffResultfinal readonly classarray $added, array $removed, array $modified, isIdentical(): bool, hasDifferences(): bool, totalChanges(): int
NextPDF\Pro\Diff\StructuredDiffResultfinal readonly classtext diff, paragraphs, images, metadata changes, summary
NextPDF\Pro\Diff\DiffTypeenumAdded, Removed, Modified, Unchanged
<?php
declare(strict_types=1);
use NextPDF\Pro\Diff\PdfDiffer;
$diff = PdfDiffer::compare(
file_get_contents('v1.pdf'),
file_get_contents('v2.pdf'),
);
if ($diff->hasDifferences()) {
echo $diff->totalChanges(), " text changes detected\n";
}
<?php
declare(strict_types=1);
use NextPDF\Pro\Diff\DiffFormatter;
use NextPDF\Pro\Diff\StructuredDiffer;
function reviewReport(string $oldPdf, string $newPdf): string
{
$result = (new StructuredDiffer())->compare($oldPdf, $newPdf);
// JSON for machine consumption; toHtml() for a review UI fragment.
return (new DiffFormatter())->toJson($result);
}
  • The diff is page-aligned by index. Inserting a page early shifts all later pages and reports large downstream changes — this is expected for an index-aligned comparison.
  • Image comparison detects added, removed, and modified embedded images; it is not a perceptual visual-diff and does not pixel-render pages.
  • Scanned image-only PDFs produce little or no text diff because no OCR is performed.
  • Without the optional Artisan reader, extraction uses the bounded fallback; heavily compressed documents may yield reduced text coverage.

Text extraction is linear in document bytes; the Myers diff is near-linear for similar documents and quadratic in the worst case per page pair. The decompression cap bounds memory. See performance_budget.

The byte-level fallback uses strpos-based scanning instead of unbounded regex to avoid catastrophic backtracking on crafted PDFs, and bounds decompression output. Diffing does not execute embedded scripts. See the Core security model.

ClaimSpec clauseStatus
Tj text operator parsed for extractionISO 32000-2:2020 §9.4Verified (unit suite)
TJ array text operator parsed for extractionISO 32000-2:2020 §9.4Verified (unit suite)
Visual side-by-side redline PDF outputNot supported (JSON/HTML only)

No Core equivalent exists for document comparison. The optional Artisan reader improves extraction accuracy when installed but is not required.

This is a content-change detector. It is not a forensic difference analyzer and does not produce evidentiary or tamper-attribution reports; those concerns are out of scope for this module.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.