Technisch artikel

Twee PDF-bestanden vergelijken in Delphi: structuur en pixels

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