HotPDF jämför två PDF-dokument från Delphi via THPDFDocComparison, som går igenom objektgrafen för båda filerna utåt från katalogen och, när det begärs, också renderar varje sidpar och mäter de pixlar som skiljer sig. Resultatet är en JSON-rapport som namnger varje skillnad den hittade, den budget den förbrukade, och om jämförelsen kördes klart. Båda passen spelar roll, eftersom en strukturell diff och en visuell diff besvarar olika frågor
Frågan bakom funktionen är oftast en releasefråga. En rapportmotor får en ändring, utdata regenereras, och någon måste avgöra om något förändrats. Att öppna båda filerna sida vid sida fungerar upp till ungefär tre sidor innan uppmärksamheten sviktar. Att jämföra rådata misslyckas omedelbart, eftersom två körningar av samma generator producerar olika byte av skäl som inte har något med det en läsare ser att göra
Varför kan PDF-filer vara byte-olika men visuellt identiska?
Två oberoende genererade PDF-filer som skrivs ut identiskt skiljer sig regelmässigt åt i sina byte, och skälen är strukturella snarare än kosmetiska. Objektnummer tilldelas i den ordning objekten råkar skrivas. Typsnittsundergrupper allokerar CID:er i den ordning glyfer först påträffas, så en undergrupp byggd under en något annorlunda traversering producerar andra innehållsströmsbyte för samma synliga text. Korsreferensoffsetar förskjuts närhelst något uppströms ändrar längd
Det är därför objektnummer inte kan användas som identitet mellan dokument. HotPDF bygger istället varje ögonblicksbild genom att traversera från katalogen, och expanderar ordböcker i byteordningen för deras nycklar och arrayer efter index, så att varje objekt namnges av den sökväg som når det. Objekt som traverseringen inte kan nå från roten faller tillbaka på en syntetisk $Unreachable[...]-sökväg som bär objektnumret och generationen, vilket håller föräldralöst innehåll synligt i rapporten istället för tyst frånvarande
Strömmar jämförs inte genom kopiering. Varje ström bidrar med en inkrementell SHA-256-signatur, beräknad medan den ursprungliga strömpositionen återställs efteråt, så att jämföra två hundra megabyte stora filer innebär inte att materialisera tvåhundra megabyte två gånger
Att justera sidor när ett dokument har en infogning
Att jämföra sida 1 mot sida 1, sida 2 mot sida 2 och så vidare är korrekt bara när inget har infogats. Infoga en försättssida och en naiv jämförelse rapporterar varje sida som ändrad, vilket är tekniskt sant och praktiskt oanvändbart
HotPDF justerar sidor innan de diffas. Den bygger en signatur per sida från extraherbar text, faller tillbaka på en strukturell signatur för sidor utan text, och beräknar sedan den längsta växande delsekvensen över de matchade målindexen. Sidor inom den delsekvensen är de som bara förskjutits; sidor utanför den är verkliga förflyttningar. Distinktionen är det som gör en diff av en 400-sidig manual läsbar, eftersom rapporten säger att en sida infogades snarare än att fyrahundra sidor ändrades
Att köra en strukturell jämförelse
Det enklaste anropet tar två inlästa dokument och ett läge. cmStructural utför objektgrafgenomgången, cmRenderedImage utför pixeljämförelsen, cmFull gör båda, och de lättare lägena cmPageCount, cmPageText och cmObjectCount finns för billiga rökprov:
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 skiljer mellan tre tillstånd som en boolean inte kan. identical anger om något skilde sig, comparisonComplete anger om genomgången avslutades, och comparisonBudget namnger den gräns som stoppade den om en sådan gjorde det. En jämförelse som förbrukar en budget rapporterar comparisonComplete=false och identical=false tillsammans, eftersom en trunkerad genomgång inte har någon grund för att hävda likhet. All automation som bara läser identical kommer förr eller senare att behandla ett budgetstopp som en verklig skillnad, så läs alla tre
Vilka gränser håller genomgången begränsad?
Standardvärdena i THPDFStructuralCompareLimits.Default är dimensionerade för verkliga dokument snarare än för fientliga sådana, och varje semantiskt relevant budget har sitt eget tak: 250 000 objekt, 2 000 000 kanter, djup 128, 10 000 rapporterade skillnader, 64 MB per ström och 512 MB strömbyte totalt, 1 MB per värde och 4 096 byte per sökväg. Höj dem medvetet när du känner ditt underlag, och sänk dem när du jämför filer som kommit utifrån:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // misslyckas snabbt i CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // standard är 72
Options.ColorTolerance := 2; // ignorera avrundningsbrus på 1-2 nivåer
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // skriv överlagringsbilder för granskning
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Det renderade passet uppskattar pixelantalet från sidans dimensioner och den begärda DPI:n innan någon bitmapp allokeras, och kontrollerar sedan den faktiska bitmappen igen efteråt, så att en missbildad sidgeometri inte kan smyga sig förbi budgeten genom att ljuga om sin storlek. Att höja DPI höjer trohet och kostnad kvadratiskt: 150 DPI är fyra gånger så många pixlar som 72, och taken per sida och totalt existerar just för att en batchjobb på 300 DPI annars allokerar sig rakt in i trubbel
Hur lik är tillräckligt lik?
Två sidor räknas som lika bara när båda villkoren gäller: den ändrade pixelandelen är vid eller under MaxChangedPixelRatio och likheten är vid eller över MinimumSimilarity. Två tröskelvärden istället för ett, eftersom en handfull katastrofalt felaktiga pixlar och en bred svepning av små färgförskjutningar är olika typer av fel, och endera för sig kan vara acceptabel i ett arbetsflöde och diskvalificerande i ett annat. Tröskeltesterna använder oavrundade värden; de sex decimalerna i JSON:en finns för att hålla rapporterna stabila och diffbara, inte för att definiera jämförelsen
Ändrade pixlar grupperas i regioner med hjälp av rutor med fast storlek som noder med fyrvägsangränsning, snarare än pixel-för-pixel-flödesfyllning. Det håller minnet begränsat och regionlistan stabil mellan körningar. Att trunkera den bevarade regiondetaljen påverkar bara listningen, inte det rapporterade regionantalet, så en sida med fler ändrade regioner än MaxChangedRegions rapporterar ändå hur många de var
Ett beteende är värt att nämna rakt ut eftersom det vänder på den vanliga instinkten. Renderarfel, allokeringsfel och överlagringsfel sväljs aldrig. Allt av det slaget registreras som renderError eller renderBudget och tvingar fram renderComparisonComplete=false, eftersom en sida som misslyckades med att renderas är en sida ingen jämförde, och att rapportera den som identisk är värre än att inte rapportera något alls
Var hör varje läge hemma i en pipeline?
Strukturell jämförelse besvarar vad som ändrades och är rätt standard för regressionssviter: den namnger sökvägen, sidindexet och de inblandade objektnumren, så ett fel pekar på den kod som orsakade det. Renderad jämförelse besvarar om någon kommer att märka det, vilket är frågan för godkännanden och för att verifiera att ett optimeringspass verkligen var förlustfritt
De kombineras väl. Kör cmStructural på varje bygge och låt det misslyckas högljutt vid oväntade ändringar på objektnivå; kör cmFull med värmekartor före en release, när en människa finns tillgänglig för att granska överlagringarna. För pipelines som redan skriver ut sidmarkup av andra skäl ger textutdatan som beskrivs i export av PDF-sidor till SVG en tredje, mänskligt diffbar vy, och de automatiserade kontrollerna i preflight-rapportautomation täcker konformitetsfrågor som ingetdera diff-läget är avsett att besvara
Jämförelse, preflight och rendering delar samma objektmodell för inlästa dokument, så ett enda genomlopp av en fil kan mata alla tre. Den fullständiga funktionslistan för Delphi och C++Builder finns på sidan för HotPDF Delphi PDF-komponent