Tehnički članak

Usporedba dviju PDF datoteka u Delphiju: struktura i pikseli

HotPDF iz Delphija uspoređuje dva PDF dokumenta putem THPDFDocComparison, koji prolazi kroz graf objekata obiju datoteka počevši od kataloga prema van, a na zahtjev i renderira svaki par stranica te mjeri piksele koji se razlikuju. Rezultat je JSON izvještaj koji imenuje svaku pronađenu razliku, proračun koji je potrošen i je li usporedba dovršena do kraja. Oba prolaza su važna, jer strukturna i vizualna usporedba odgovaraju na različita pitanja

Pitanje koje stoji iza ove značajke obično je pitanje izdanja. Stroj za izvještaje dobije izmjenu, izlaz se ponovno generira i netko mora odlučiti je li se išta promijenilo. Otvaranje obje datoteke jedna pored druge funkcionira do otprilike tri stranice prije nego što pažnja popusti. Usporedba sirovih bajtova odmah zakaže, jer dva pokretanja istog generatora proizvode različite bajtove iz razloga koji nemaju nikakve veze s onim što čitatelj vidi

Zašto PDF-ovi mogu biti različiti po bajtovima, a vizualno identični?

Dva neovisno generirana PDF-a koja se ispisuju identično redovito se razlikuju po bajtovima, a razlozi su strukturni, a ne kozmetički. Brojevi objekata dodjeljuju se redoslijedom kojim su objekti zapisani. Podskupovi fontova dodjeljuju CID-ove redoslijedom kojim su glifovi prvi put naiđeni, pa podskup izgrađen tijekom malo drugačijeg obilaska proizvodi drugačije bajtove tijeka sadržaja za isti vidljivi tekst. Pomaci unakrsnih referenci mijenjaju se kad god se bilo gdje uzvodno promijeni duljina

Zbog toga se brojevi objekata ne mogu koristiti kao identitet među dokumentima. HotPDF umjesto toga gradi svaki snimak obilaskom od kataloga, proširujući rječnike prema bajtovnom redoslijedu njihovih ključeva i polja prema indeksu, tako da je svaki objekt imenovan putanjom koja do njega vodi. Objekti do kojih obilazak ne može doći od korijena padaju natrag na sintetičku putanju $Unreachable[...] koja nosi broj objekta i generaciju, čime napušteni sadržaj ostaje vidljiv u izvještaju umjesto da tiho nedostaje

Tokovi se ne uspoređuju kopiranjem. Svaki tok doprinosi inkrementalnim SHA-256 potpisom, izračunatim uz naknadno vraćanje izvorne pozicije toka, tako da usporedba dvije datoteke od po dvjesto megabajta ne znači materijalizaciju dvjesto megabajta dvaput

Poravnavanje stranica kada jedan dokument sadrži umetak

Uspoređivanje stranice 1 sa stranicom 1, stranice 2 sa stranicom 2 i tako dalje ispravno je samo kada ništa nije umetnuto. Umetnite naslovnu stranicu i naivna usporedba prijavit će svaku stranicu kao promijenjenu, što je tehnički točno, a operativno beskorisno

HotPDF poravnava stranice prije njihove usporedbe. Za svaku stranicu gradi potpis iz teksta koji se može izdvojiti, za stranice bez teksta pada natrag na strukturni potpis, a zatim izračunava najdulji rastući podniz nad podudarenim ciljnim indeksima. Stranice unutar tog podniza samo su se pomaknule; stranice izvan njega prave su izmjene. Ta razlika čini razliku 400-stranične upute čitljivom, jer izvještaj kaže da je umetnuta jedna stranica umjesto da je promijenjeno četiristo stranica

Pokretanje strukturne usporedbe

Najjednostavniji poziv prima dva učitana dokumenta i način rada. cmStructural izvodi prolazak kroz graf objekata, cmRenderedImage izvodi usporedbu piksela, cmFull radi oboje, a lakši načini rada cmPageCount, cmPageText i cmObjectCount postoje za jeftine osnovne provjere:

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;

Izvještaj razlikuje tri stanja koja boolean ne može izraziti. identical govori je li se išta razlikovalo, comparisonComplete govori je li obilazak završen, a comparisonBudget imenuje ograničenje koje ga je zaustavilo, ako je do toga došlo. Usporedba koja iscrpi proračun prijavljuje zajedno comparisonComplete=false i identical=false, jer skraćeni obilazak nema temelja za tvrdnju o jednakosti. Svaka automatizacija koja čita samo identical u konačnici će zaustavljanje zbog proračuna tretirati kao stvarnu razliku, pa čitajte sva tri polja

Koja ograničenja drže obilazak omeđenim?

Zadane vrijednosti u THPDFStructuralCompareLimits.Default dimenzionirane su za stvarne dokumente, a ne za neprijateljski osmišljene, i svaki semantički relevantan proračun ima vlastiti gornji limit: 250.000 objekata, 2.000.000 rubova, dubina 128, 10.000 prijavljenih razlika, 64 MB po toku i 512 MB ukupnih bajtova tokova, 1 MB po vrijednosti i 4.096 bajtova po putanji. Podignite ih namjerno kada poznajete svoj korpus, a spustite ih pri usporedbi datoteka koje su stigle izvana:

var
  Limits: THPDFStructuralCompareLimits;
  Options: THPDFRenderedCompareOptions;
begin
  Limits := THPDFStructuralCompareLimits.Default;
  Limits.MaxDifferences := 200;        // brz neuspjeh u CI
  Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // zadana vrijednost je 72
  Options.ColorTolerance := 2;         // ignoriraj šum zaokruživanja razine 1-2
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // zapiši preklopne slike za pregled

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

Renderirani prolaz procjenjuje broj piksela iz dimenzija stranice i tražene rezolucije (DPI) prije nego što se alocira ijedna bitmapa, a naknadno ponovno provjerava stvarnu bitmapu, tako da deformirana geometrija stranice ne može zaobići proračun lažući o svojoj veličini. Podizanje DPI-ja podiže vjernost i trošak kvadratno: 150 DPI ima četiri puta više piksela od 72, a gornje granice piksela po stranici i ukupno postoje upravo zato što bi paketni posao pri 300 DPI inače alokacijom upao u nevolje

Koliko slično je dovoljno slično?

Dvije se stranice smatraju sličnima samo kada su ispunjena oba uvjeta: omjer promijenjenih piksela jednak je ili manji od MaxChangedPixelRatio, a sličnost je jednaka ili veća od MinimumSimilarity. Dva praga umjesto jednog, jer je šačica katastrofalno pogrešnih piksela i široki val sitnih pomaka boje različit tip greške, i svaki od njih zasebno može biti prihvatljiv u jednom tijeku rada, a diskvalificirajući u drugom. Testovi praga koriste nezaokružene vrijednosti; šest decimalnih mjesta u JSON-u postoji kako bi izvještaji ostali stabilni i usporedivi, a ne kako bi definirali usporedbu

Promijenjeni pikseli grupiraju se u regije koristeći pločice fiksne veličine kao čvorove s četverosmjernom susjednošću, umjesto popunjavanja po pikselu (flood fill). To drži memoriju omeđenom, a popis regija stabilnim između pokretanja. Skraćivanje zadržanih detalja regije utječe samo na popis, ne i na prijavljeni broj regija, pa stranica s više promijenjenih regija od MaxChangedRegions i dalje prijavljuje koliko ih je stvarno bilo

Jedno ponašanje vrijedi jasno istaknuti jer preokreće uobičajeni instinkt. Greške renderera, greške alokacije i greške preklapanja nikada se ne gutaju. Sve takvo bilježi se kao renderError ili renderBudget i prisiljava renderComparisonComplete=false, jer je stranica koja nije uspjela renderirati stranica koju nitko nije usporedio, a njezino prijavljivanje kao identične gore je od neprijavljivanja ničega

Gdje svaki način rada pripada u pipelineu

Strukturna usporedba odgovara na pitanje što se promijenilo i pravi je zadani izbor za regresijske pakete: imenuje putanju, indeks stranice i uključene brojeve objekata, pa greška upućuje na kod koji ju je proizveo. Renderirana usporedba odgovara na pitanje hoće li itko primijetiti, a to je pitanje za odobrenja i za provjeru da je prolaz optimizacije doista bio bezgubitan

Dobro se nadopunjuju. Pokrenite cmStructural na svakom buildu i pustite ga da glasno zakaže kod neočekivanih promjena na razini objekata; pokrenite cmFull s toplinskim kartama prije izdanja, kada je čovjek dostupan da pogleda preklope. Za pipelineove koji već iz drugih razloga generiraju oznake stranica, tekstualni izlaz opisan u izvozu PDF stranica u SVG daje treći, ljudima čitljiv prikaz, a automatizirane provjere u automatizaciji preflight izvještaja pokrivaju pitanja usklađenosti na koja nijedan od načina usporedbe nije namijenjen odgovoriti

Usporedba, preflight i renderiranje dijele isti objektni model učitanog dokumenta, pa jedan prolazak kroz datoteku može opskrbiti sva tri. Cjelokupni popis značajki za Delphi i C++Builder nalazi se na stranici HotPDF Delphi PDF komponente