HotPDF porovnává dva dokumenty PDF z Delphi pomocí THPDFDocComparison, který projde graf objektů obou souborů od katalogu směrem ven a na požádání také vykreslí každou dvojici stránek a změří pixely, které se liší. Výsledkem je zpráva JSON, jež pojmenuje každý nalezený rozdíl, spotřebovaný rozpočet a informaci, zda porovnání proběhlo do konce. Na obou průchodech záleží, protože strukturální a vizuální porovnání odpovídají na různé otázky
Otázka za touto funkcí je obvykle otázkou vydání. Generátor sestav dostane změnu, výstup se přegeneruje a někdo musí rozhodnout, jestli se něco pohnulo. Otevření obou souborů vedle sebe funguje asi do tří stránek, než selže pozornost. Porovnání syrových bajtů selže okamžitě, protože dva běhy stejného generátoru produkují odlišné bajty z důvodů, které nemají nic společného s tím, co vidí čtenář
Proč mohou být PDF bajtově odlišné a přitom vizuálně shodné?
Dva nezávisle vygenerované PDF soubory, které se tisknou identicky, se běžně liší ve svých bajtech, a důvody jsou strukturální, nikoli kosmetické. Čísla objektů se přidělují v pořadí, v jakém se objekty náhodou zapíšou. Podmnožiny fontů přidělují CID v pořadí, v jakém jsou glyfy poprvé nalezeny, takže podmnožina vytvořená během mírně odlišného průchodu produkuje pro stejný viditelný text odlišné bajty obsahového proudu. Posuny referenčních tabulek se mění pokaždé, když se výše v souboru změní délka čehokoli
Proto nelze čísla objektů použít jako identitu napříč dokumenty. HotPDF místo toho sestavuje každý snímek průchodem od katalogu, přičemž slovníky rozvíjí podle bajtového pořadí jejich klíčů a pole podle indexu, takže každý objekt je pojmenován cestou, která k němu vede. Objekty, ke kterým se průchod nedostane od kořene, spadají do syntetické cesty $Unreachable[...] nesoucí číslo objektu a generaci, což udrží osiřelý obsah viditelný ve zprávě místo toho, aby tiše zmizel
Proudy se neporovnávají kopírováním. Každý proud přispívá inkrementálním podpisem SHA-256, počítaným při zpětném obnovení původní pozice v proudu, takže porovnání dvou stovkamegabajtových souborů neznamená dvojí materializaci dvou set megabajtů
Zarovnání stránek, když jeden dokument obsahuje vloženou stránku
Porovnávání stránky 1 se stránkou 1, stránky 2 se stránkou 2 a tak dále je správné pouze tehdy, když nic nebylo vloženo. Vložte titulní stránku a naivní porovnání ohlásí jako změněnou úplně každou stránku, což je technicky pravda, ale provozně k ničemu
HotPDF stránky před porovnáním zarovná. Sestaví podpis každé stránky z extrahovatelného textu, u stránek bez textu se vrátí ke strukturálnímu podpisu a poté spočítá nejdelší rostoucí podposloupnost přes spárované indexy cíle. Stránky uvnitř této podposloupnosti jsou ty, které se pouze posunuly; stránky mimo ni jsou skutečné přesuny. Tento rozdíl dělá porovnání 400stránkové příručky čitelným, protože zpráva řekne, že byla vložena jedna stránka, místo aby tvrdila, že se změnilo čtyři sta stránek
Spuštění strukturálního porovnání
Nejjednodušší volání přebírá dva načtené dokumenty a režim. cmStructural provádí průchod grafem objektů, cmRenderedImage provádí pixelové porovnání, cmFull dělá obojí a lehčí režimy cmPageCount, cmPageText a cmObjectCount existují pro levné kouřové testy:
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;
Zpráva rozlišuje tři stavy, které booleovská hodnota vyjádřit nedokáže. identical říká, zda se něco lišilo, comparisonComplete říká, zda průchod doběhl do konce, a comparisonBudget pojmenuje limit, který jej případně zastavil. Porovnání, které vyčerpá rozpočet, hlásí společně comparisonComplete=false a identical=false, protože přerušený průchod nemá žádný podklad pro tvrzení o shodě. Jakákoli automatizace, která čte pouze identical, bude nakonec zastavení kvůli rozpočtu považovat za skutečný rozdíl, takže čtěte všechny tři hodnoty
Jaké limity udržují průchod omezený?
Výchozí hodnoty v THPDFStructuralCompareLimits.Default jsou dimenzované pro skutečné dokumenty, ne pro nepřátelské, a každý sémanticky relevantní rozpočet má vlastní strop: 250 000 objektů, 2 000 000 hran, hloubka 128, 10 000 hlášených rozdílů, 64 MB na proud a celkem 512 MB bajtů proudů, 1 MB na hodnotu a 4 096 bajtů na cestu. Zvyšte je záměrně, když znáte svůj korpus, a snižte je při porovnávání souborů, které přišly zvenčí:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // rychlé selhání v CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // výchozí je 72
Options.ColorTolerance := 2; // ignorovat šum zaokrouhlení o 1-2 úrovně
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // zapsat obrázky s překryvem pro kontrolu
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Vykreslovací průchod odhaduje počet pixelů z rozměrů stránky a požadovaného DPI ještě předtím, než se alokuje jakákoli bitmapa, a poté kontroluje skutečnou bitmapu, takže poškozená geometrie stránky nemůže proklouznout kolem rozpočtu lhaním o své velikosti. Zvýšení DPI zvyšuje věrnost i náklady kvadraticky: 150 DPI má čtyřikrát víc pixelů než 72, a stropy pixelů na stránku i celkové existují přesně proto, že dávková úloha při 300 DPI by se jinak alokovala do potíží
Jak podobné je dost podobné?
Dvě stránky se počítají jako podobné pouze tehdy, když platí obě podmínky: poměr změněných pixelů je na úrovni nebo pod MaxChangedPixelRatio a podobnost je na úrovni nebo nad MinimumSimilarity. Dva prahy místo jednoho, protože hrstka katastrofálně chybných pixelů a široký nádech drobných barevných posunů jsou odlišná selhání, a kterékoli z nich může být v jednom pracovním postupu přijatelné a v jiném diskvalifikující. Testy prahů používají nezaokrouhlené hodnoty; šest desetinných míst v JSON existuje kvůli stabilitě a porovnatelnosti zpráv, ne kvůli definici porovnání
Změněné pixely se seskupují do oblastí pomocí dlaždic pevné velikosti jako uzlů se čtyřsměrnou sousedností, nikoli pomocí vyplňování po jednotlivých pixelech. To udržuje paměť omezenou a seznam oblastí stabilní napříč běhy. Oříznutí uchovávaných detailů oblastí ovlivňuje pouze výpis, nikoli hlášený počet oblastí, takže stránka s více změněnými oblastmi, než je MaxChangedRegions, přesto uvede, kolik jich skutečně bylo
Jedno chování stojí za to výslovně zmínit, protože obrací obvyklý instinkt. Selhání vykreslovače, selhání alokace a selhání překryvu se nikdy nepolykají potichu. Cokoli takového se zaznamená jako renderError nebo renderBudget a vynutí renderComparisonComplete=false, protože stránka, kterou se nepodařilo vykreslit, je stránka, kterou nikdo neporovnal, a označit ji za shodnou je horší než neuvést nic
Kam který režim v pipeline patří
Strukturální porovnání odpovídá na otázku, co se změnilo, a je správnou výchozí volbou pro regresní sady: pojmenuje cestu, index stránky a zúčastněná čísla objektů, takže selhání ukazuje přímo na kód, který jej způsobil. Vykreslené porovnání odpovídá na otázku, jestli si toho někdo všimne, což je otázka pro schvalování a pro ověření, že optimalizační průchod byl skutečně bezztrátový
Dobře se kombinují. Spouštějte cmStructural při každém sestavení a nechte jej hlasitě selhat na neočekávaných změnách na úrovni objektů; spouštějte cmFull s teplotními mapami před vydáním, kdy je k dispozici člověk, který se na překryvy podívá. Pro pipeline, které už z jiných důvodů generují textový popis stránky, nabízí textový výstup popsaný v článku export stránek PDF do SVG třetí, člověkem porovnatelný pohled, a automatizované kontroly v článku automatizace preflight zpráv pokrývají otázky shody, na které žádný z režimů porovnání odpovídat nemá
Porovnání, preflight a vykreslování sdílejí stejný objektový model načteného dokumentu, takže jeden průchod souborem může sytit všechny tři. Kompletní seznam funkcí pro Delphi a C++Builder je na stránce komponenty HotPDF PDF pro Delphi