ComparePdfSemantics compares annotation and AcroForm widget properties independently of page rendering; form values can change even when appearance pixels stay identical
Pass TPdfDiffPagePairs from page alignment or an explicit mapping; nil selects positional pairs, and explicit mappings must cover both documents exactly once, using -1 for a missing side
Annotation identity uses its /NM name, and form-widget identity uses the fully qualified field name; identity is scoped to the mapped page and item kind
Missing names and repeated names, including multiple widgets of one field on the same page, are reported as psckAmbiguous with an explicit reason; annotation array order does not determine identity
The supported profile compares strings, flags, colors, opacity, borders, markup, line, vertex and ink geometry, URI actions, field values, choice labels and selected options, and checkbox or radio state
CompareAppearanceStreams optionally compares annotation appearance stream content; it is disabled by default
CompareAnnotations and CompareForms select the respective item kinds; GeometryTolerance defaults to 0.001 PDF user-space points
Added, removed and changed items include before/after properties and bounds; BuildSemanticDiffOverlays returns separate before/after rectangles with page and change indexes
All indexes are zero-based and bounds remain in PDF user space, in points; apply the relevant page rotation, crop and viewport transform before painting into a bitmap
Pages are loaded with independent native handles, preserving the caller's current page and form focus and avoiding form actions
The profile excludes non-widget fields, arbitrary custom dictionaries, embedded files, script actions and non-URI destinations; XFA comparison is explicitly rejected
Default limits are 10,000 page pairs, 4,096 scanned items, 4,096 changes, 1 MiB per string, 16 MiB of total strings and 16,777,216 comparisons; exhausting a budget rejects incomplete output
Complete distinguishes a successful complete comparison from a rejected comparison; its changes may include explicitly ambiguous identities, so inspect AmbiguousCount before treating the results as a definitive item match
Actual Delphi/FPC Win32/Win64 acceptance covers 92 assertions per target, including saved/reopened documents, identical-pixel value changes, Unicode identity, page movement, ambiguity and budgets
Options := TPdfSemanticDiffOptions.Default; if not ComparePdfSemantics(BeforePdf, AfterPdf, PagePairs, Options, Report) then raise EPdfError.Create(Report.ErrorMessage); Overlays := BuildSemanticDiffOverlays(Report);
type
TPdfSemanticItemKind = (psikAnnotation, psikFormWidget);
TPdfSemanticChangeKind = (psckAdded, psckRemoved, psckChanged, psckAmbiguous);
TPdfSemanticSide = (pssBefore, pssAfter);
TPdfSemanticProperty = record
Name: string;
Value: WideString;
end;
TPdfSemanticProperties = array of TPdfSemanticProperty;
TPdfSemanticItem = record
Kind: TPdfSemanticItemKind;
Identity: WideString;
PageIndex, AnnotationIndex: Integer;
HasBounds: Boolean;
Bounds: TPdfRectangle;
Properties: TPdfSemanticProperties;
end;
TPdfSemanticPropertyChange = record
Name: string;
HasBefore, HasAfter: Boolean;
BeforeValue, AfterValue: WideString;
end;
TPdfSemanticPropertyChanges = array of TPdfSemanticPropertyChange;
TPdfSemanticChange = record
Kind: TPdfSemanticChangeKind;
ItemKind: TPdfSemanticItemKind;
Identity: WideString;
HasBefore, HasAfter: Boolean;
BeforeItem, AfterItem: TPdfSemanticItem;
Properties: TPdfSemanticPropertyChanges;
AmbiguityReason: string;
end;
TPdfSemanticChanges = array of TPdfSemanticChange;
TPdfSemanticOverlay = record
Side: TPdfSemanticSide;
PageIndex, ChangeIndex: Integer;
Kind: TPdfSemanticChangeKind;
Bounds: TPdfRectangle;
end;
TPdfSemanticOverlays = array of TPdfSemanticOverlay;
TPdfSemanticDiffOptions = record
CompareAnnotations, CompareForms, CompareAppearanceStreams: Boolean;
GeometryTolerance: Double;
MaxPagePairs, MaxItems, MaxChanges, MaxStringBytes: Integer;
MaxTotalStringBytes, MaxComparisons: Int64;
class function Default: TPdfSemanticDiffOptions; static;
end;
TPdfSemanticDiffReport = record
Complete: Boolean;
Changes: TPdfSemanticChanges;
AddedCount, RemovedCount, ChangedCount, AmbiguousCount: Integer;
ComparedItemCount: Integer;
ErrorMessage: string;
end;
// Indices are zero based. Nil PagePairs selects positional pairing. Explicit
// pairs must cover both documents exactly once; -1 denotes an absent side
// Identity is scoped to a mapped page and item kind. Missing names and repeated
// names (including repeated widgets of one field) are reported as ambiguous
// No rendering, focus changes, form actions, or positional identity guessing
// are performed. Pages are opened independently of the caller's current page
// The supported profile covers strings, flags, colors, opacity, borders,
// markup/line/vertex/ink geometry, URI actions, field values/options/checks,
// and optionally appearance streams. Arbitrary dictionaries, embedded files,
// script actions, non-URI destinations, and non-widget fields are not compared
function ComparePdfSemantics(BeforePdf, AfterPdf: TPdf;
const PagePairs: TPdfDiffPagePairs; const Options: TPdfSemanticDiffOptions;
out Report: TPdfSemanticDiffReport): Boolean;
// Bounds remain in PDF user space, in points. Transform with the destination
// page's rotation/crop/viewport rather than treating them as bitmap pixels
function BuildSemanticDiffOverlays(const Report: TPdfSemanticDiffReport):
TPdfSemanticOverlays;