Teknisk artikkel

Sammenligne to PDF-filer i Delphi: struktur og piksler

HotPDF sammenligner to PDF-dokumenter fra Delphi gjennom THPDFDocComparison, som går gjennom objektgrafen til begge filene fra katalogen og utover, og som, når det blir bedt om det, også gjengir hvert sidepar og måler pikslene som avviker. Resultatet er en JSON-rapport som navngir hver forskjell den fant, budsjettet den brukte, og om sammenligningen kjørte til fullførelse. Begge gjennomgangene betyr noe, fordi en strukturell diff og en visuell diff svarer på ulike spørsmål

Spørsmålet bak funksjonen er som regel et utgivelsesspørsmål. En rapportmotor får en endring, resultatet blir generert på nytt, og noen må avgjøre om noe har flyttet på seg. Å åpne begge filene side om side fungerer til omtrent tre sider før oppmerksomheten svikter. Å sammenligne rå byte feiler umiddelbart, siden to kjøringer av den samme generatoren produserer forskjellige byte av grunner som ikke har noe med det en leser ser, å gjøre

Hvorfor kan PDF-er være byte-forskjellige og visuelt identiske?

To uavhengig genererte PDF-er som skrives ut identisk, skiller seg rutinemessig i sine byte, og grunnene er strukturelle snarere enn kosmetiske. Objektnumre tildeles i den rekkefølgen objektene tilfeldigvis blir skrevet. Fontdelmengder tildeler CID-er i den rekkefølgen glyfer først blir påtruffet, så en delmengde bygget under en litt annen gjennomgang produserer forskjellige innholdsstrøm-byte for den samme synlige teksten. Kryssreferanseforskyvninger endrer seg når som helst noe tidligere i strømmen endrer lengde

Dette er grunnen til at objektnumre ikke kan brukes som identitet på tvers av dokumenter. HotPDF bygger i stedet hvert øyeblikksbilde ved å gå gjennom fra katalogen, og utvider ordbøker i byte-rekkefølgen til nøklene sine og arrayer etter indeks, slik at hvert objekt navngis av stien som når det. Objekter gjennomgangen ikke kan nå fra roten, faller tilbake til en syntetisk $Unreachable[...]-sti som bærer objektnummeret og generasjonen, noe som holder foreldreløst innhold synlig i rapporten i stedet for stille fraværende

Strømmer sammenlignes ikke ved kopiering. Hver strøm bidrar med en inkrementell SHA-256-signatur, beregnet mens den opprinnelige strømposisjonen gjenopprettes etterpå, så å sammenligne to filer på hundre megabyte hver betyr ikke å materialisere to hundre megabyte to ganger

Justere sider når ett dokument har et innskudd

Å sammenligne side 1 mot side 1, side 2 mot side 2 og så videre er bare korrekt når ingenting er satt inn. Sett inn en forside, og en naiv sammenligning rapporterer hver eneste side som endret, noe som teknisk sett er sant og driftsmessig ubrukelig

HotPDF justerer sider før de sammenlignes. Den bygger en signatur per side fra tekst som kan hentes ut, faller tilbake til en strukturell signatur for sider uten tekst, og beregner deretter den lengste økende delsekvensen over de matchede målindeksene. Sider innenfor den delsekvensen er dem som bare har forskjøvet seg; sider utenfor er ekte forflytninger. Det skillet er det som gjør en diff av en 400-siders manual lesbar, fordi rapporten sier at én side ble satt inn i stedet for at fire hundre sider endret seg

Kjøre en strukturell sammenligning

Det enkleste kallet tar imot to innlastede dokumenter og en modus. cmStructural utfører objektgraf-gjennomgangen, cmRenderedImage utfører pikselsammenligningen, cmFull gjør begge deler, og de lettere modusene cmPageCount, cmPageText og cmObjectCount finnes for billige røyktester:

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;

Rapporten skiller mellom tre tilstander en boolsk verdi ikke kan. identical sier om noe avvek, comparisonComplete sier om gjennomgangen ble fullført, og comparisonBudget navngir grensen som stoppet den, hvis noe gjorde det. En sammenligning som tømmer et budsjett, rapporterer comparisonComplete=false og identical=false sammen, fordi en avkortet gjennomgang ikke har noe grunnlag for å hevde likhet. Enhver automatisering som bare leser identical, vil før eller siden behandle et budsjettstopp som en reell forskjell, så les alle tre

Hvilke grenser holder gjennomgangen avgrenset?

Standardverdiene i THPDFStructuralCompareLimits.Default er dimensjonert for virkelige dokumenter snarere enn for fiendtlige, og hvert semantisk relevante budsjett har sitt eget tak: 250 000 objekter, 2 000 000 kanter, dybde 128, 10 000 rapporterte forskjeller, 64 MB per strøm og 512 MB strømbyte totalt, 1 MB per verdi og 4 096 byte per sti. Hev dem bevisst når du kjenner korpuset ditt, og senk dem når du sammenligner filer som kom utenfra:

var
  Limits: THPDFStructuralCompareLimits;
  Options: THPDFRenderedCompareOptions;
begin
  Limits := THPDFStructuralCompareLimits.Default;
  Limits.MaxDifferences := 200;        // feil raskt i CI
  Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // standard er 72
  Options.ColorTolerance := 2;         // ignorer avrundingsstøy på 1-2 nivåer
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // skriv overleggsbilder for gjennomgang

  Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
    Limits, Options);
end;

Den gjengitte gjennomgangen anslår pikselantallet fra sidedimensjonene og den forespurte DPI-en før noe bitmap blir allokert, og dobbeltsjekker det faktiske bitmapet etterpå, slik at en feilformet sidegeometri ikke kan snike seg forbi budsjettet ved å lyve om størrelsen sin. Å heve DPI hever nøyaktighet og kostnad kvadratisk: 150 DPI gir fire ganger så mange piksler som 72, og takene per side og totalt eksisterer nettopp fordi en batch-jobb på 300 DPI ellers vil allokere seg selv rett inn i trøbbel

Hvor lik er lik nok?

To sider regnes som like bare når begge betingelser holder: andelen endrede piksler er lik eller under MaxChangedPixelRatio, og likheten er lik eller over MinimumSimilarity. To terskler i stedet for én, fordi en håndfull katastrofalt gale piksler og et bredt lag med små fargeforskyvninger er forskjellige typer feil, og hver av dem alene kan være akseptabel i én arbeidsflyt og diskvalifiserende i en annen. Terskeltestene bruker ikke-avrundede verdier; de seks desimalene i JSON-en finnes for å holde rapportene stabile og diffbare, ikke for å definere sammenligningen

Endrede piksler grupperes i regioner ved bruk av fastdimensjonerte fliser som noder med fireveis nabolagsforhold, i stedet for piksel-for-piksel-fyll. Det holder minnebruken avgrenset og regionlisten stabil på tvers av kjøringer. Å avkorte den beholdte regiondetaljen påvirker bare listingen, ikke det rapporterte regionantallet, så en side med flere endrede regioner enn MaxChangedRegions, rapporterer likevel hvor mange det var

Én oppførsel er verdt å nevne rett ut, fordi den snur den vanlige intuisjonen på hodet. Gjengivelsesfeil, allokeringsfeil og overleggsfeil blir aldri svelget. Alt av den typen blir registrert som renderError eller renderBudget og tvinger frem renderComparisonComplete=false, fordi en side som ikke klarte å bli gjengitt, er en side ingen sammenlignet, og å rapportere den som identisk, er verre enn å ikke rapportere noe

Hvor hver modus hører hjemme i en pipeline

Strukturell sammenligning svarer på hva som endret seg og er det riktige standardvalget for regresjonstestsuiter: den navngir stien, sideindeksen og objektnumrene som er involvert, slik at en feil peker mot koden som produserte den. Gjengitt sammenligning svarer på om noen vil legge merke til det, som er spørsmålet for godkjenninger og for å verifisere at en optimaliseringsgjennomgang virkelig var tapsfri

De utfyller hverandre godt. Kjør cmStructural på hvert bygg og la den feile høylytt ved uventede endringer på objektnivå; kjør cmFull med heatmaps før en utgivelse, når et menneske er tilgjengelig for å se på overleggene. For pipeliner som allerede produserer sidemarkup av andre grunner, gir tekstutdataen beskrevet i eksport av PDF-sider til SVG en tredje, menneskelig diffbar visning, og de automatiserte sjekkene i automatisering av preflight-rapporter dekker samsvarsspørsmål som ingen av de to diff-modusene er ment å svare på

Sammenligning, preflight og gjengivelse deler den samme objektmodellen for innlastede dokumenter, så én enkelt gjennomgang av en fil kan mate alle tre. Den komplette funksjonslisten for Delphi og C++Builder finnes på HotPDF Delphi PDF-komponentsiden