Skip to content
getnextpdf.com

Pro edition

Diff — Deep Reference

This page is the contract-level reference for the NextPDF Pro diff module, NextPDF\Pro\Diff. The module compares two PDF documents and reports text, image, and metadata changes. PdfDiffer produces a page-aligned Myers line diff. StructuredDiffer adds paragraph grouping, image comparison, and metadata comparison. DiffFormatter serializes the structured result to JSON or an HTML fragment. This page states the public API, the observable behavior contract, the resource bounds, and the failure modes. Task-oriented setup and samples live on the Diff capability page.

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 this module. The diff classes are usable whenever nextpdf/pro is installed and licensed.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
PdfDiffer::compare()string $sourcePdf, string $targetPdfExtracts per-page text, then diffs page i of the source against page i of the targetDiffResultInvalidArgumentException when a buffer lacks the %PDF header or the optional reader fails to parse; OverflowException on a resource boundStatic entry point
PdfDiffer::compareTexts()array $sourcePages, array $targetPages (list<string> each)Diffs pre-extracted page texts, bypassing extractionDiffResultOverflowException on a resource boundStatic; use when text is already available
PdfDiffer::extractText()string $contentStreamParses text-showing operators from one raw content streamstring— (fault-tolerant; unparseable input yields an empty string)Static
StructuredDiffer::__construct()?ImageDiffer $imageDiffer = null, ?MetadataDiffer $metadataDiffer = nullnull arguments construct the default differsConstructor injection for testing
StructuredDiffer::compare()string $sourcePdf, string $targetPdfRuns text, paragraph, image, and metadata comparison, then builds a summaryStructuredDiffResultPropagates InvalidArgumentException and OverflowException from the text pathOrchestrator over the whole module
DiffFormatter::toJson()StructuredDiffResult $resultPretty-printed JSON documentstringJsonException when encoding fails
DiffFormatter::toHtml()StructuredDiffResult $resultHTML fragment with summary, paragraph, and metadata sections; text values are entity-escapedstringFragment only, not a full document
DiffFormatter::toArray()StructuredDiffResult $resultSerialization array backing toJson()array<string, mixed>Stable snake_case keys
ImageDiffer::diff()string $sourcePdf, string $targetPdfHashes image XObjects and reports added, removed, and modified imageslist<ImageDiff>— (undecodable structures are skipped fail-closed)Identity is page bucket plus object number
MetadataDiffer::diff()string $sourcePdf, string $targetPdfCompares eight /Info fields (Title, Author, Subject, Keywords, Creator, Producer, CreationDate, ModDate)list<MetadataChange>— (never throws on non-conforming input)Values compared as decoded strings
DiffEngine::diff()array $sourceLines, array $targetLines, int $pageIndex = 0, int $maxLines = 10000Myers line diff over two line listslist<DiffRegion>OverflowException when combined lines exceed $maxLines or the edit distance exceeds the memory-bound capStatic; the region producer for all text paths
TextExtractor::fromContentStream()string $contentStreamTokenizes the stream and runs the text-state machinelist<TextBlock>Static
TextExtractor::fromOperations()array $operations (list<ContentStreamOp>)Runs the text-state machine over pre-parsed operationslist<TextBlock>Static
ContentStreamParser::parse()constructor takes string $dataTokenizes operators and operands; skips dictionaries and comments; fault-tolerantlist<ContentStreamOp>Unrecognized bytes are skipped, never fatal
ContentStreamOpstring $operator, list<mixed> $operandsReadonly operation value object; isTextOp() classifies text-related operators
DiffResultlist<DiffRegion> $regions, int $sourcePagesCount, int $targetPagesCountBuckets regions into $added, $removed, $modified; exposes isIdentical(), hasDifferences(), totalChanges()Readonly; Unchanged regions stay in $regions only
StructuredDiffResulttext diff, paragraphs, images, metadata changes, summaryAggregate result; hasDifferences(), isIdentical() delegate to the summaryReadonly
DiffSummaryper-category counts plus page countshasDifferences() and totalChanges() over text, image, and metadata countsReadonly
DiffRegionDiffType $type, string $text, int $pageIndex, int $lineIndex, ?string $counterpartText = nullOne line-level change$counterpartText stays null on the shipped engine
ParagraphDifftype, text, page index, start/end line, regionsConsecutive same-type regions on one page; lineCount()Readonly
ImageDifftype, page index, source hash, target hash, object idOne image change entryHashes are empty strings on the absent side
MetadataChangestring $field, ?string $sourceValue, ?string $targetValueOne field change; isAdded(), isRemoved(), isModified()null means the field is absent
TextBlocktext, x, y, font name, font size, line indexOne extracted text run with approximate positionReadonly
DiffTypeenum: Added, Removed, Modified, UnchangedString-backed change classification for textSee the Modified note in the behavior contract
ImageDiffTypeenum: Added, Removed, Modified, UnchangedString-backed change classification for images
public static function compare(string $sourcePdf, string $targetPdf): DiffResult
public static function compareTexts(array $sourcePages, array $targetPages): DiffResult
public static function extractText(string $contentStream): string
public function __construct(
?ImageDiffer $imageDiffer = null,
?MetadataDiffer $metadataDiffer = null,
)
public function compare(string $sourcePdf, string $targetPdf): StructuredDiffResult
public function toJson(StructuredDiffResult $result): string
public function toHtml(StructuredDiffResult $result): string
public function toArray(StructuredDiffResult $result): array
public static function diff(
array $sourceLines,
array $targetLines,
int $pageIndex = 0,
int $maxLines = self::MAX_DIFF_LINES,
): array

PdfDiffer::compare() extracts per-page text, then diffs page i of the source against page i of the target. When page counts differ, the missing side is treated as empty text for the surplus pages. Within each page pair, text splits on newlines and a Myers line diff runs per page. The engine emits Added, Removed, and Unchanged regions. A changed line surfaces as a Removed plus an Added region; the shipped engine never emits Modified text regions. The Modified case and the DiffResult::$modified bucket serve caller-constructed results, since the DiffResult constructor is public. totalChanges() counts added, removed, and modified regions; unchanged regions are excluded.

Extraction has two paths:

  • Optional Artisan reader present. When the optional NextPDF\Parser\PdfReader class is installed, page content streams are read through it for page-accurate text. The trailer’s page count drives the loop. A page that fails to read contributes empty text instead of aborting the comparison.
  • Fallback. A bounded byte-level scanner locates stream/endstream pairs by strpos, inflates FlateDecode data with a hard 50 MB output cap, and reverse-filters a PNG predictor when the stream dictionary requests one through /DecodeParms per ISO 32000-2:2020 §7.4.4.4. A malformed or unsupported predictor leaves the decoded bytes unchanged. The fallback concatenates all recovered text into a single page bucket, so page-level alignment is only page-accurate on the reader path.

Both paths parse the §9.4 text-showing operators Tj, TJ, and '. The state machine tracks BT/ET, Tm (origin only), Td/TD, T*, and Tf.

StructuredDiffer::compare() runs the text diff, groups consecutive same-type regions on the same page into paragraphs (unchanged runs included), then runs image and metadata comparison and assembles a DiffSummary. Summary paragraph counts cover added, removed, and modified paragraphs only.

Image comparison enumerates PDF objects structurally. A stream body’s extent is governed by its /Length entry per §7.3.8.2, so binary bytes that merely resemble object syntax never register as phantom objects. Compressed object streams (/Type /ObjStm) are decoded per §7.5.7 so image XObjects nested inside them are visible. Each detected image is content-hashed with the non-cryptographic xxh128 function; identity is the pair of page bucket and object number. Images with no owning page in stream order are attributed to page 0.

Metadata comparison resolves the real /Info dictionary through the trailer when possible, so a decoy field token inside a content stream is not mistaken for document metadata. Field values are decoded as PDF strings: the literal form per §7.3.4.2 and the hexadecimal form per §7.3.4.3. Without a resolvable trailer, the search falls back to the whole input. Dates are compared as decoded strings, not parsed timestamps.

DiffFormatter::toJson() returns pretty-printed JSON and encodes with JSON_THROW_ON_ERROR, so an encoding failure raises JsonException instead of returning false. toHtml() returns a <div class="nextpdf-diff"> fragment; paragraph text and metadata values pass through HTML entity escaping. There is no visual side-by-side redline PDF output. For identical inputs, regions and formatted output are deterministic.

  • Page alignment is positional. A single inserted or deleted page shifts alignment for all subsequent pages and inflates downstream change counts.
  • On the fallback extraction path, all text lands on page index 0. Diffing a reader-extracted document against expectations from the fallback path yields different page attribution.
  • A source or target buffer not starting with %PDF fails with InvalidArgumentException before any comparison.
  • More than 10,000 combined lines in one page pair fails with OverflowException (line-count bound).
  • Two page texts sharing too few lines fail with OverflowException once the Myers edit distance exceeds the memory-bound cap. Legitimate revisions share most lines and stay unaffected; adversarial low-commonality inputs trip the bound.
  • Decompressed fallback stream output larger than 50 MB fails with OverflowException (decompression-bomb bound). The scanner uses strpos, not unbounded regex, so crafted input cannot trigger catastrophic backtracking.
  • The " text-showing operator is tokenized but produces no text block in 3.1.0; text shown only through " does not participate in the diff.
  • Scanned, image-only PDFs produce little or no text diff. No OCR runs.
  • Image change detection is structural, not perceptual. It does not rasterize pages, and an image re-encoded with identical pixels reports as modified when its bytes differ.
  • An image whose page bucket or object number changes between revisions reports as a removed-plus-added pair, not as modified.
  • Object streams compressed with filters other than FlateDecode are skipped fail-closed; their member images are not compared.
  • No cryptographic operation occurs in this module, so no FIPS-mode-specific behavior exists. The image hash is for change detection only and carries no integrity or evidentiary weight.
ClaimStandardClause
Tj and TJ text-showing operators are parsed for extractionISO 32000-2:2020§9.4
Fallback stream data starts after the CRLF or LF that follows the stream keywordISO 32000-2:2020§7.3.8.1
Image-scan stream extents are governed by the dictionary /Length entryISO 32000-2:2020§7.3.8.2
Object-stream members are located through the /N pair table and /First offsetISO 32000-2:2020§7.5.7
PNG predictor reversal follows the /DecodeParms Predictor parameterISO 32000-2:2020§7.4.4.4
Metadata values decode literal and hexadecimal string formsISO 32000-2:2020§7.3.4.2, §7.3.4.3
Visual side-by-side redline PDF outputNot supported (JSON/HTML only)

All clauses are paraphrased. These are capability statements. Text recovery reconstructs line text from text-showing operators. It does not run the full §9.4 text-state machine, so the diff is content-level, not geometry-level.

  • Availability within the Pro package: PdfDiffer, DiffEngine, TextExtractor, and their value objects since 1.8.0; StructuredDiffer, DiffFormatter, ImageDiffer, MetadataDiffer, and theirs since 2.2.0. All are current in nextpdf/pro 3.1.0.
  • Prefer PdfDiffer::compareTexts() when page text is already available; it skips extraction and its failure modes entirely.
  • The optional Artisan reader improves extraction accuracy and page attribution. It is detected at runtime and is never required.
  • Catch OverflowException when diffing untrusted input; the bounds are deliberate fail-closed rejections, not transient errors.
  • DiffFormatter::toHtml() emits class names (diff-added, diff-removed, diff-modified, diff-unchanged) but no stylesheet; supply your own CSS.
  • Construct StructuredDiffer with stub differs in tests to isolate the text path from image and metadata scanning.

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.