Pro edition
Diff — Deep Reference
At a glance
Section titled “At a glance”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.
Availability & licensing
Section titled “Availability & licensing”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.
Public API surface
Section titled “Public API surface”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
PdfDiffer::compare() | string $sourcePdf, string $targetPdf | Extracts per-page text, then diffs page i of the source against page i of the target | DiffResult | InvalidArgumentException when a buffer lacks the %PDF header or the optional reader fails to parse; OverflowException on a resource bound | Static entry point |
PdfDiffer::compareTexts() | array $sourcePages, array $targetPages (list<string> each) | Diffs pre-extracted page texts, bypassing extraction | DiffResult | OverflowException on a resource bound | Static; use when text is already available |
PdfDiffer::extractText() | string $contentStream | Parses text-showing operators from one raw content stream | string | — (fault-tolerant; unparseable input yields an empty string) | Static |
StructuredDiffer::__construct() | ?ImageDiffer $imageDiffer = null, ?MetadataDiffer $metadataDiffer = null | null arguments construct the default differs | — | — | Constructor injection for testing |
StructuredDiffer::compare() | string $sourcePdf, string $targetPdf | Runs text, paragraph, image, and metadata comparison, then builds a summary | StructuredDiffResult | Propagates InvalidArgumentException and OverflowException from the text path | Orchestrator over the whole module |
DiffFormatter::toJson() | StructuredDiffResult $result | Pretty-printed JSON document | string | JsonException when encoding fails | — |
DiffFormatter::toHtml() | StructuredDiffResult $result | HTML fragment with summary, paragraph, and metadata sections; text values are entity-escaped | string | — | Fragment only, not a full document |
DiffFormatter::toArray() | StructuredDiffResult $result | Serialization array backing toJson() | array<string, mixed> | — | Stable snake_case keys |
ImageDiffer::diff() | string $sourcePdf, string $targetPdf | Hashes image XObjects and reports added, removed, and modified images | list<ImageDiff> | — (undecodable structures are skipped fail-closed) | Identity is page bucket plus object number |
MetadataDiffer::diff() | string $sourcePdf, string $targetPdf | Compares 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 = 10000 | Myers line diff over two line lists | list<DiffRegion> | OverflowException when combined lines exceed $maxLines or the edit distance exceeds the memory-bound cap | Static; the region producer for all text paths |
TextExtractor::fromContentStream() | string $contentStream | Tokenizes the stream and runs the text-state machine | list<TextBlock> | — | Static |
TextExtractor::fromOperations() | array $operations (list<ContentStreamOp>) | Runs the text-state machine over pre-parsed operations | list<TextBlock> | — | Static |
ContentStreamParser::parse() | constructor takes string $data | Tokenizes operators and operands; skips dictionaries and comments; fault-tolerant | list<ContentStreamOp> | — | Unrecognized bytes are skipped, never fatal |
ContentStreamOp | string $operator, list<mixed> $operands | Readonly operation value object; isTextOp() classifies text-related operators | — | — | — |
DiffResult | list<DiffRegion> $regions, int $sourcePagesCount, int $targetPagesCount | Buckets regions into $added, $removed, $modified; exposes isIdentical(), hasDifferences(), totalChanges() | — | — | Readonly; Unchanged regions stay in $regions only |
StructuredDiffResult | text diff, paragraphs, images, metadata changes, summary | Aggregate result; hasDifferences(), isIdentical() delegate to the summary | — | — | Readonly |
DiffSummary | per-category counts plus page counts | hasDifferences() and totalChanges() over text, image, and metadata counts | — | — | Readonly |
DiffRegion | DiffType $type, string $text, int $pageIndex, int $lineIndex, ?string $counterpartText = null | One line-level change | — | — | $counterpartText stays null on the shipped engine |
ParagraphDiff | type, text, page index, start/end line, regions | Consecutive same-type regions on one page; lineCount() | — | — | Readonly |
ImageDiff | type, page index, source hash, target hash, object id | One image change entry | — | — | Hashes are empty strings on the absent side |
MetadataChange | string $field, ?string $sourceValue, ?string $targetValue | One field change; isAdded(), isRemoved(), isModified() | — | — | null means the field is absent |
TextBlock | text, x, y, font name, font size, line index | One extracted text run with approximate position | — | — | Readonly |
DiffType | enum: Added, Removed, Modified, Unchanged | String-backed change classification for text | — | — | See the Modified note in the behavior contract |
ImageDiffType | enum: Added, Removed, Modified, Unchanged | String-backed change classification for images | — | — | — |
Entry-point signatures
Section titled “Entry-point signatures”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,): arrayBehavior contract
Section titled “Behavior contract”Page alignment and line diff
Section titled “Page alignment and line diff”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 paths
Section titled “Extraction paths”Extraction has two paths:
- Optional Artisan reader present. When the optional
NextPDF\Parser\PdfReaderclass 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/endstreampairs bystrpos, inflates FlateDecode data with a hard 50 MB output cap, and reverse-filters a PNG predictor when the stream dictionary requests one through/DecodeParmsper 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.
Structured comparison
Section titled “Structured comparison”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.
Report output
Section titled “Report output”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.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- 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
%PDFfails withInvalidArgumentExceptionbefore 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
OverflowExceptiononce 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 usesstrpos, 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.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
Tj and TJ text-showing operators are parsed for extraction | ISO 32000-2:2020 | §9.4 |
Fallback stream data starts after the CRLF or LF that follows the stream keyword | ISO 32000-2:2020 | §7.3.8.1 |
Image-scan stream extents are governed by the dictionary /Length entry | ISO 32000-2:2020 | §7.3.8.2 |
Object-stream members are located through the /N pair table and /First offset | ISO 32000-2:2020 | §7.5.7 |
PNG predictor reversal follows the /DecodeParms Predictor parameter | ISO 32000-2:2020 | §7.4.4.4 |
| Metadata values decode literal and hexadecimal string forms | ISO 32000-2:2020 | §7.3.4.2, §7.3.4.3 |
| Visual side-by-side redline PDF output | — | Not 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.
Development notes
Section titled “Development notes”- 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 innextpdf/pro3.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
OverflowExceptionwhen 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
StructuredDifferwith stub differs in tests to isolate the text path from image and metadata scanning.
Publication boundary
Section titled “Publication boundary”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.
See also
Section titled “See also”- Diff (capability) — install, quick start, and production samples.
- Converter — Deep Reference
- Filter — Deep Reference