Odborný článok

Porovnanie dvoch súborov PDF v Delphi: štruktúra a pixely

HotPDF porovnáva dva dokumenty PDF z Delphi prostredníctvom THPDFDocComparison, ktorý prechádza graf objektov oboch súborov od katalógu smerom von a na požiadanie tiež vykreslí každú dvojicu stránok a zmeria pixely, ktoré sa líšia. Výsledkom je JSON report, ktorý pomenúva každý nájdený rozdiel, spotrebovaný rozpočet a to, či sa porovnanie dokončilo. Na oboch priechodoch záleží, pretože štrukturálny diff a vizuálny diff odpovedajú na odlišné otázky

Otázka, ktorá stojí za touto funkciou, je zvyčajne otázkou vydania. Generátor zostáv dostane zmenu, výstup sa znovu vygeneruje a niekto musí rozhodnúť, či sa niečo pohlo. Otvorenie oboch súborov vedľa seba funguje asi do troch strán, kým zlyhá pozornosť. Porovnávanie surových bajtov zlyháva okamžite, pretože dva behy toho istého generátora produkujú odlišné bajty z dôvodov, ktoré nemajú nič spoločné s tým, čo vidí čitateľ

Prečo môžu byť súbory PDF bajtovo odlišné a pritom vizuálne identické?

Dva nezávisle vygenerované PDF súbory, ktoré sa tlačia identicky, sa bežne líšia vo svojich bajtoch a dôvody sú štrukturálne, nie kozmetické. Čísla objektov sa prideľujú v poradí, v akom sa objekty náhodou zapíšu. Podmnožiny písiem prideľujú CID v poradí, v akom sa glyfy prvýkrát objavia, takže podmnožina vytvorená pri mierne odlišnom prechode produkuje odlišné bajty obsahového streamu pre ten istý viditeľný text. Posuny krížových odkazov sa menia vždy, keď sa vyššie v reťazci zmení dĺžka čohokoľvek

Preto sa čísla objektov nedajú použiť ako identita naprieč dokumentmi. HotPDF namiesto toho zostavuje každý snímok prechádzaním od katalógu, pričom slovníky rozbaľuje v bajtovom poradí ich kľúčov a polia podľa indexu, takže každý objekt je pomenovaný cestou, ktorá k nemu vedie. Objekty, ku ktorým sa prechod od koreňa nedostane, dostanú náhradnú syntetickú cestu $Unreachable[...], ktorá nesie číslo objektu a generáciu, čím zostáva osirelý obsah v reporte viditeľný namiesto toho, aby ticho chýbal

Streamy sa neporovnávajú kopírovaním. Každý stream prispieva inkrementálnym podpisom SHA-256, ktorý sa počíta tak, že sa pôvodná pozícia streamu následne obnoví, takže porovnanie dvoch dvestomegabajtových súborov neznamená dvakrát materializovať dvesto megabajtov

Zarovnávanie stránok, keď jeden dokument obsahuje vloženú stránku

Porovnávanie strany 1 so stranou 1, strany 2 so stranou 2 a tak ďalej je správne iba vtedy, keď nič nebolo vložené. Vložte titulnú stranu a naivné porovnanie nahlási zmenu na každej strane, čo je technicky pravda, no prevádzkovo nepoužiteľné

HotPDF stránky pred porovnaním zarovná. Pre každú stránku zostaví podpis z extrahovateľného textu, pri stránkach bez textu použije náhradný štrukturálny podpis a následne vypočíta najdlhšiu rastúcu podsekvenciu nad zhodnými indexmi cieľa. Stránky vnútri tejto podsekvencie sú tie, ktoré sa len posunuli; stránky mimo nej predstavujú skutočné presuny. Práve tento rozdiel robí diff 400-stranovej príručky čitateľným, pretože report uvedie, že bola vložená jedna stránka, namiesto toho, aby oznámil zmenu na štyristo stránkach

Spustenie štrukturálneho porovnania

Najjednoduchšie volanie prijíma dva načítané dokumenty a režim. cmStructural vykoná prechod grafu objektov, cmRenderedImage vykoná porovnanie pixelov, cmFull urobí oboje, a ľahšie režimy cmPageCount, cmPageText a cmObjectCount existujú na lacné rýchle kontroly:

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;

Report rozlišuje tri stavy, ktoré boolovská hodnota rozlíšiť nedokáže. identical hovorí, či sa niečo líšilo, comparisonComplete hovorí, či sa prechod dokončil, a comparisonBudget pomenúva limit, ktorý ho prípadne zastavil. Porovnanie, ktoré vyčerpá rozpočet, nahlási spolu comparisonComplete=false a identical=false, pretože skrátený prechod nemá základ na tvrdenie o rovnosti. Akákoľvek automatizácia, ktorá číta len identical, bude časom považovať zastavenie kvôli rozpočtu za skutočný rozdiel, preto čítajte všetky tri hodnoty

Aké limity udržujú prechod obmedzený?

Predvolené hodnoty v THPDFStructuralCompareLimits.Default sú dimenzované pre reálne dokumenty, nie pre nepriateľské, a každý sémanticky relevantný rozpočet má vlastný strop: 250 000 objektov, 2 000 000 hrán, hĺbka 128, 10 000 hlásených rozdielov, 64 MB na stream a 512 MB bajtov streamov celkovo, 1 MB na hodnotu a 4 096 bajtov na cestu. Zvyšujte ich zámerne, keď poznáte svoj korpus, a znižujte ich pri porovnávaní súborov, ktoré prišli zvonka:

var
  Limits: THPDFStructuralCompareLimits;
  Options: THPDFRenderedCompareOptions;
begin
  Limits := THPDFStructuralCompareLimits.Default;
  Limits.MaxDifferences := 200;        // rýchle zlyhanie v CI
  Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // predvolená hodnota je 72
  Options.ColorTolerance := 2;         // ignorovať zaokrúhľovací šum na úrovni 1-2
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // zapísať prekryvné obrázky na kontrolu

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

Vykresľovací priechod odhadne počet pixelov z rozmerov stránky a požadovaného DPI ešte pred alokáciou akejkoľvek bitmapy a následne overí skutočnú bitmapu, takže poškodená geometria stránky nemôže obísť rozpočet klamaním o svojej veľkosti. Zvýšenie DPI zvyšuje vernosť aj náklady kvadraticky: 150 DPI má štyrikrát viac pixelov než 72, a stropy na stránku aj celkové stropy pixelov existujú práve preto, že dávková úloha pri 300 DPI by sa inak alokáciou dostala do problémov

Ako podobné je dostatočne podobné?

Dve stránky sa počítajú ako podobné iba vtedy, keď platia obe podmienky: pomer zmenených pixelov je na úrovni MaxChangedPixelRatio alebo pod ňou a podobnosť je na úrovni MinimumSimilarity alebo nad ňou. Dva prahy namiesto jedného, pretože hŕstka katastrofálne nesprávnych pixelov a širokoplošný nádych drobných farebných posunov sú odlišné zlyhania, a každé z nich môže byť samostatne prijateľné v jednom pracovnom postupe a diskvalifikujúce v inom. Testy prahov používajú nezaokrúhlené hodnoty; šesť desatinných miest v JSON existuje na to, aby reporty zostali stabilné a porovnateľné, nie na definovanie samotného porovnania

Zmenené pixely sa zoskupujú do oblastí pomocou dlaždíc pevnej veľkosti ako uzlov so susednosťou v štyroch smeroch, namiesto vyplňovania po jednotlivých pixeloch. To udržuje pamäť obmedzenú a zoznam oblastí stabilný naprieč behmi. Skrátenie zachovaného detailu oblastí ovplyvňuje iba výpis, nie hlásený počet oblastí, takže stránka s viacerými zmenenými oblasťami, než je MaxChangedRegions, aj tak nahlási, koľko ich v skutočnosti bolo

Jedno správanie stojí za to jasne vysloviť, pretože obracia zvyčajný inštinkt. Zlyhania vykresľovača, zlyhania alokácie a zlyhania prekrytia sa nikdy nepohltia potichu. Čokoľvek podobné sa zaznamená ako renderError alebo renderBudget a vynúti renderComparisonComplete=false, pretože stránka, ktorú sa nepodarilo vykresliť, je stránka, ktorú nikto neporovnal, a nahlásiť ju ako identickú je horšie než nenahlásiť nič

Kam jednotlivé režimy patria v pipeline

Štrukturálne porovnanie odpovedá na to, čo sa zmenilo, a je správnou predvoľbou pre regresné sady: pomenúva cestu, index stránky a čísla dotknutých objektov, takže zlyhanie ukazuje priamo na kód, ktorý ho spôsobil. Vykreslené porovnanie odpovedá na to, či si to niekto všimne, čo je otázka pre schvaľovanie a pre overenie, že optimalizačný priechod bol naozaj bezstratový

Dobre sa dopĺňajú. Spúšťajte cmStructural pri každom zostavení a nechajte ho hlasno zlyhať pri neočakávaných zmenách na úrovni objektov; pred vydaním spustite cmFull s teplotnými mapami, keď je k dispozícii človek, ktorý sa môže pozrieť na prekryvy. Pre pipeline, ktoré už z iných dôvodov generujú markup stránok, dáva textový výstup opísaný v časti export stránok PDF do SVG tretí, pre človeka porovnateľný pohľad, a automatizované kontroly v časti automatizácia preflight reportov pokrývajú otázky zhody, na ktoré ani jeden z režimov diffu nemá odpovedať

Porovnávanie, preflight a vykresľovanie zdieľajú rovnaký objektový model načítaného dokumentu, takže jediný priechod súborom môže zásobiť všetky tri. Kompletný zoznam funkcií pre Delphi a C++Builder nájdete na stránke HotPDF Delphi PDF component