HotPDF sammenligner to PDF-dokumenter fra Delphi via THPDFDocComparison, som gennemløber objektgrafen for begge filer fra kataloget og udad og, når det bedes om det, også renderer hvert sidepar og måler de pixels, der er forskellige. Resultatet er en JSON-rapport, der navngiver hver forskel, den fandt, det budget, den forbrugte, og om sammenligningen kørte til ende. Begge gennemløb har betydning, fordi en strukturel diff og en visuel diff besvarer forskellige spørgsmål
Spørgsmålet bag funktionen er som regel et udgivelsesspørgsmål. En rapportmotor får en ændring, outputtet regenereres, og nogen skal afgøre, om noget flyttede sig. At åbne begge filer side om side skalerer til omkring tre sider, før opmærksomheden svigter. At sammenligne rå bytes fejler øjeblikkeligt, da to kørsler af den samme generator producerer forskellige bytes af grunde, der intet har at gøre med, hvad en læser ser
Hvorfor kan PDF-filer være byteforskellige og visuelt identiske?
To uafhængigt genererede PDF'er, der udskrives identisk, adskiller sig rutinemæssigt i deres bytes, og grundene er strukturelle frem for kosmetiske. Objektnumre tildeles i den rækkefølge, objekter tilfældigvis bliver skrevet. Skrifttype-subsets allokerer CID'er i den rækkefølge, glyffer først bliver mødt, så et subset bygget under en let anderledes gennemløbsrækkefølge producerer forskellige indholdsstrøm-bytes for den samme synlige tekst. Krydsreference-forskydninger flytter sig, hver gang noget opstrøms ændrer længde
Det er derfor, objektnumre ikke kan bruges som identitet på tværs af dokumenter. HotPDF bygger i stedet hvert øjebliksbillede ved at gennemløbe fra kataloget, udfolde dictionaries i byte-rækkefølgen af deres nøgler og arrays efter indeks, så hvert objekt navngives ved den sti, der når frem til det. Objekter, som gennemløbet ikke kan nå fra roden, falder tilbage til en syntetisk $Unreachable[...]-sti, der bærer objektnummeret og generationen, hvilket holder forældreløst indhold synligt i rapporten i stedet for stiltiende fraværende
Streams sammenlignes ikke ved kopiering. Hver stream bidrager med en inkrementel SHA-256-signatur, beregnet mens den oprindelige streamposition genoprettes bagefter, så sammenligning af to hundrede-megabyte-filer ikke betyder at materialisere to hundrede megabyte to gange
Justering af sider, når et dokument har en indsættelse
At sammenligne side 1 mod side 1, side 2 mod side 2 og så videre er kun korrekt, når intet er blevet indsat. Indsæt en forside, og en naiv sammenligning rapporterer hver side som ændret, hvilket er teknisk korrekt og operationelt ubrugeligt
HotPDF justerer sider, før de diffes. Den bygger en signatur pr. side ud fra udtrækbar tekst, falder tilbage til en strukturel signatur for sider uden tekst, og beregner derefter den længste stigende delfølge over de matchede målindekser. Sider inden for denne delfølge er dem, der blot forskød sig; sider uden for den er reelle flytninger. Denne skelnen er det, der gør en diff af en manual på 400 sider læsbar, fordi rapporten siger, at én side blev indsat, frem for at fire hundrede sider ændrede sig
Kørsel af en strukturel sammenligning
Det simpleste kald tager to indlæste dokumenter og en tilstand. cmStructural udfører objektgrafgennemløbet, cmRenderedImage udfører pixelsammenligningen, cmFull gør begge dele, og de lettere tilstande cmPageCount, cmPageText og cmObjectCount findes til billige smoke-tjek:
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 skelner mellem tre tilstande, som en boolean ikke kan. identical siger, om noget var forskelligt, comparisonComplete siger, om gennemløbet blev færdigt, og comparisonBudget navngiver den grænse, der stoppede det, hvis en gjorde. En sammenligning, der opbruger et budget, rapporterer comparisonComplete=false og identical=false sammen, fordi et afkortet gennemløb ikke har noget grundlag for at hævde lighed. Enhver automatisering, der kun læser identical, vil før eller siden behandle et budgetstop som en reel forskel, så læs alle tre
Hvilke grænser holder gennemløbet afgrænset?
Standardværdierne i THPDFStructuralCompareLimits.Default er dimensioneret til rigtige dokumenter frem for til fjendtlige, og hvert semantisk relevant budget har sit eget loft: 250.000 objekter, 2.000.000 kanter, dybde 128, 10.000 rapporterede forskelle, 64 MB pr. stream og 512 MB stream-bytes i alt, 1 MB pr. værdi og 4.096 bytes pr. sti. Hæv dem bevidst, når du kender dit korpus, og sænk dem, når du sammenligner filer, der er ankommet udefra:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // fejl hurtigt i CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // standard er 72
Options.ColorTolerance := 2; // ignorer 1-2-niveaus afrundingsstøj
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // skriv overlay-billeder til gennemsyn
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Rendering-gennemløbet estimerer pixelantallet ud fra sidedimensionerne og den ønskede DPI, før noget bitmap allokeres, og gentjekker det faktiske bitmap bagefter, så en misdannet sidegeometri ikke kan snige sig forbi budgettet ved at lyve om sin størrelse. At hæve DPI hæver troskab og omkostning kvadratisk: 150 DPI er fire gange så mange pixels som 72, og lofterne pr. side og i alt findes netop, fordi et batch-job ved 300 DPI ellers vil allokere sig selv ind i problemer
Hvor lig er lig nok?
To sider tæller kun som ens, når begge betingelser er opfyldt: forholdet af ændrede pixels er på eller under MaxChangedPixelRatio, og ligheden er på eller over MinimumSimilarity. To tærskler i stedet for én, fordi en håndfuld katastrofalt forkerte pixels og en bred vask af små farveskift er forskellige fejltyper, og hver for sig kan være acceptabel i én arbejdsgang og diskvalificerende i en anden. Tærskeltests bruger uafrundede værdier; de seks decimaler i JSON'en findes for at holde rapporter stabile og diffbare, ikke for at definere sammenligningen
Ændrede pixels grupperes i regioner ved hjælp af fastbrede fliser som knuder med firevejs-nabolag, frem for pixel-for-pixel flood fill. Det holder hukommelsen afgrænset og regionslisten stabil på tværs af kørsler. At afkorte den bevarede regionsdetalje påvirker kun listen, ikke det rapporterede regionsantal, så en side med flere ændrede regioner end MaxChangedRegions stadig rapporterer, hvor mange der var
Én adfærd er værd at nævne klart, fordi den vender det sædvanlige instinkt om. Fejl i rendereren, allokeringsfejl og overlay-fejl bliver aldrig slugt. Alt af den slags registreres som renderError eller renderBudget og tvinger renderComparisonComplete=false igennem, fordi en side, der ikke kunne renderes, er en side, ingen sammenlignede, og at rapportere den som identisk er værre end at rapportere intet
Hvor hver tilstand hører hjemme i en pipeline
Strukturel sammenligning besvarer, hvad der ændrede sig, og er det rigtige standardvalg til regressionssuiter: den navngiver stien, sideindekset og de involverede objektnumre, så en fejl peger på den kode, der producerede den. Renderingssammenligning besvarer, om nogen vil bemærke det, hvilket er spørgsmålet for godkendelser og for at verificere, at et optimeringsgennemløb virkelig var tabsfrit
De kombinerer sig godt. Kør cmStructural på hvert build, og lad det fejle højlydt ved uventede ændringer på objektniveau; kør cmFull med heatmaps før en udgivelse, når et menneske er til rådighed til at se på overlays. For pipelines, der allerede udsender sidemarkup af andre grunde, giver tekstoutputtet beskrevet i eksport af PDF-sider til SVG et tredje, menneskeligt diffbart syn, og de automatiserede tjek i preflight-rapportautomatisering dækker konformitetsspørgsmål, som ingen af diff-tilstandene er tiltænkt at besvare
Sammenligning, preflight og rendering deler den samme indlæste-dokument-objektmodel, så ét gennemløb af en fil kan fodre alle tre. Den komplette funktionsliste til Delphi og C++Builder findes på HotPDF Delphi PDF-komponentsiden