HotPDF vergelijkt twee PDF-documenten vanuit Delphi via THPDFDocComparison, dat de objectgraaf van beide bestanden vanaf de catalogus naar buiten toe doorloopt en, indien gevraagd, ook elk paginapaar rendert en de verschillende pixels meet. Het resultaat is een JSON-rapport dat elk gevonden verschil benoemt, het verbruikte budget en of de vergelijking volledig is afgerond. Beide passages doen ertoe, omdat een structurele diff en een visuele diff verschillende vragen beantwoorden
De vraag achter deze functie is meestal een releasevraag. Een rapportmotor krijgt een wijziging, de uitvoer wordt opnieuw gegenereerd, en iemand moet beslissen of er iets is verschoven. Beide bestanden naast elkaar openen schaalt tot ongeveer drie pagina's voordat de aandacht faalt. Ruwe bytes vergelijken faalt onmiddellijk, omdat twee runs van dezelfde generator verschillende bytes produceren om redenen die niets te maken hebben met wat een lezer ziet
Waarom kunnen PDF's byte-verschillend en visueel identiek zijn?
Twee onafhankelijk gegenereerde PDF's die identiek afdrukken, verschillen routinematig in hun bytes, en de redenen zijn structureel, niet cosmetisch. Objectnummers worden toegekend in de volgorde waarin objecten toevallig worden geschreven. Font-subsets kennen CID's toe in de volgorde waarin glyphs voor het eerst worden aangetroffen, dus een subset die tijdens een enigszins andere doorloop is opgebouwd, produceert andere inhoudsstream-bytes voor dezelfde zichtbare tekst. Kruisverwijzingsoffsets verschuiven zodra iets stroomopwaarts van lengte verandert
Daarom kunnen objectnummers niet worden gebruikt als identiteit over documenten heen. HotPDF bouwt in plaats daarvan elke momentopname door vanaf de catalogus te doorlopen, waarbij dictionaries worden uitgebreid in de byte-volgorde van hun sleutels en arrays op index, zodat elk object wordt benoemd naar het pad dat het bereikt. Objecten die de doorloop niet vanaf de root kan bereiken, vallen terug op een synthetisch $Unreachable[...]-pad dat het objectnummer en de generatie draagt, wat verweesde inhoud zichtbaar houdt in het rapport in plaats van stilzwijgend afwezig
Streams worden niet vergeleken door te kopiëren. Elke stream draagt een incrementele SHA-256-handtekening bij, berekend terwijl de oorspronkelijke streampositie daarna wordt hersteld, dus het vergelijken van twee bestanden van honderd megabyte hoeft niet te betekenen dat tweehonderd megabyte tweemaal wordt gematerialiseerd
Pagina's uitlijnen wanneer één document een invoeging bevat
Pagina 1 vergelijken met pagina 1, pagina 2 met pagina 2, enzovoort, is alleen correct wanneer er niets is ingevoegd. Voeg een omslagpagina in en een naïeve vergelijking rapporteert elke pagina als gewijzigd, wat technisch waar is en operationeel nutteloos
HotPDF lijnt pagina's uit voordat het ze diff't. Het bouwt per pagina een handtekening op basis van extraheerbare tekst, valt terug op een structurele handtekening voor pagina's zonder tekst, en berekent vervolgens de langste stijgende deelrij over de gematchte doelindices. Pagina's binnen die deelrij zijn de pagina's die alleen zijn verschoven; pagina's erbuiten zijn echte verplaatsingen. Dat onderscheid maakt een diff van een handleiding van 400 pagina's leesbaar, omdat het rapport zegt dat er één pagina is ingevoegd in plaats van dat vierhonderd pagina's zijn gewijzigd
Een structurele vergelijking uitvoeren
De eenvoudigste aanroep neemt twee geladen documenten en een modus. cmStructural voert de doorloop van de objectgraaf uit, cmRenderedImage voert de pixelvergelijking uit, cmFull doet beide, en de lichtere modi cmPageCount, cmPageText en cmObjectCount bestaan voor goedkope smoke-checks:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
Het rapport onderscheidt drie toestanden die een boolean niet kan vastleggen. identical geeft aan of er iets verschilde, comparisonComplete geeft aan of de doorloop is voltooid, en comparisonBudget benoemt de limiet die de doorloop stopte, als dat gebeurde. Een vergelijking die een budget uitput, rapporteert comparisonComplete=false en identical=false samen, omdat een afgebroken doorloop geen basis heeft om gelijkheid te claimen. Elke automatisering die alleen identical leest, zal een budgetstop uiteindelijk als een echt verschil behandelen, dus lees alle drie
Welke limieten houden de doorloop begrensd?
De standaardwaarden in THPDFStructuralCompareLimits.Default zijn afgestemd op echte documenten, niet op vijandige documenten, en elk semantisch relevant budget heeft zijn eigen plafond: 250.000 objecten, 2.000.000 randen, diepte 128, 10.000 gerapporteerde verschillen, 64 MB per stream en in totaal 512 MB aan streambytes, 1 MB per waarde en 4.096 bytes per pad. Verhoog ze bewust wanneer u uw corpus kent, en verlaag ze wanneer u bestanden vergelijkt die van buitenaf zijn binnengekomen:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // snel falen in CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // standaard is 72
Options.ColorTolerance := 2; // negeer 1-2 niveaus afrondingsruis
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // schrijf overlay-afbeeldingen voor beoordeling
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
De renderpassage schat het pixelaantal op basis van de paginadimensies en de gevraagde DPI voordat er een bitmap wordt toegewezen, en controleert de werkelijke bitmap daarna opnieuw, zodat een misvormde paginageometrie niet langs het budget kan glippen door te liegen over zijn omvang. Een hogere DPI verhoogt getrouwheid en kosten kwadratisch: 150 DPI is vier keer zoveel pixels als 72, en de plafonds per pagina en in totaal bestaan precies omdat een batchtaak op 300 DPI zich anders in de problemen zou toewijzen
Hoe gelijk is gelijk genoeg?
Twee pagina's tellen alleen als gelijkend wanneer beide voorwaarden gelden: de verhouding gewijzigde pixels is gelijk aan of lager dan MaxChangedPixelRatio en de gelijkenis is gelijk aan of hoger dan MinimumSimilarity. Twee drempelwaarden in plaats van één, omdat een handvol catastrofaal foute pixels en een brede waas van kleine kleurverschuivingen verschillende soorten fouten zijn, en elk daarvan afzonderlijk in de ene workflow acceptabel kan zijn en in een andere diskwalificerend. Drempeltests gebruiken onafgeronde waarden; de zes decimalen in de JSON bestaan om rapporten stabiel en diffbaar te houden, niet om de vergelijking te definiëren
Gewijzigde pixels worden gegroepeerd in regio's met behulp van tegels van vaste grootte als knooppunten met vierrichtingsaangrenzing, in plaats van per-pixel flood fill. Dat houdt het geheugen begrensd en de regiolijst stabiel over runs heen. Het afkappen van het bewaarde regiodetail beïnvloedt alleen de lijst, niet het gerapporteerde regioaantal, dus een pagina met meer gewijzigde regio's dan MaxChangedRegions rapporteert nog steeds hoeveel het er waren
Eén gedrag is het waard om ronduit te vermelden, omdat het het gebruikelijke instinct omkeert. Renderfouten, toewijzingsfouten en overlayfouten worden nooit ingeslikt. Alles van dat soort wordt vastgelegd als renderError of renderBudget en dwingt renderComparisonComplete=false af, omdat een pagina die niet kon worden gerenderd een pagina is die niemand heeft vergeleken, en die als identiek rapporteren is erger dan niets rapporteren
Waar elke modus thuishoort in een pijplijn
Structurele vergelijking beantwoordt wat er is veranderd en is de juiste standaard voor regressiesuites: ze benoemt het pad, de pagina-index en de betrokken objectnummers, zodat een fout wijst naar de code die deze heeft veroorzaakt. Gerenderde vergelijking beantwoordt of iemand het zal merken, wat de vraag is bij goedkeuringen en bij het verifiëren dat een optimalisatiepassage werkelijk verliesvrij was
Ze combineren goed. Voer cmStructural uit op elke build en laat het luid falen bij onverwachte wijzigingen op objectniveau; voer cmFull met heatmaps uit vóór een release, wanneer een mens beschikbaar is om naar de overlays te kijken. Voor pijplijnen die al om andere redenen paginamarkup genereren, biedt de tekstuitvoer beschreven in het exporteren van PDF-pagina's naar SVG een derde, door mensen diffbare weergave, en de geautomatiseerde controles in preflight-rapportautomatisering behandelen conformiteitsvragen die geen van beide diffmodi bedoeld is om te beantwoorden
Vergelijken, preflight en renderen delen hetzelfde objectmodel voor geladen documenten, dus één enkele passage over een bestand kan alle drie voeden. De volledige functielijst voor Delphi en C++Builder staat op de HotPDF Delphi PDF-componentpagina