Tehnički članak

Poređenje dva PDF fajla u Delphiju: struktura i pikseli

HotPDF upoređuje dva PDF dokumenta iz Delphija kroz THPDFDocComparison, koji obilazi graf objekata oba fajla od kataloga naovamo i, kada se to traži, takođe renderuje svaki par stranica i meri piksele koji se razlikuju. Rezultat je JSON izveštaj koji imenuje svaku pronađenu razliku, budžet koji je potrošen i da li je poređenje izvršeno do kraja. Oba prolaza su bitna, jer strukturna razlika i vizuelna razlika odgovaraju na različita pitanja

Pitanje iza ove funkcije je obično pitanje izdanja. Mašina za izveštaje dobije izmenu, izlaz se ponovo generiše, i neko mora da odluči da li se išta pomerilo. Otvaranje oba fajla jedan pored drugog dobro funkcioniše do oko tri stranice pre nego što pažnja popusti. Poređenje sirovih bajtova ne uspeva odmah, jer dva pokretanja istog generatora proizvode različite bajtove iz razloga koji nemaju veze sa onim što čitalac vidi

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

Dva nezavisno generisana PDF-a koji se identično štampaju rutinski se razlikuju po bajtovima, a razlozi su strukturni, a ne kozmetički. Brojevi objekata se dodeljuju redosledom kojim su objekti napisani. Podskupovi fontova dodeljuju CID-ove redosledom kojim se glifovi prvi put sretnu, pa podskup izgrađen tokom malo drugačijeg obilaska proizvodi drugačije bajtove toka sadržaja za isti vidljivi tekst. Ofseti unakrsnih referenci se pomeraju kad god se dužina bilo čega uzvodno promeni

Zato se brojevi objekata ne mogu koristiti kao identitet između dokumenata. HotPDF umesto toga gradi svaki snimak obilaskom od kataloga, širenjem rečnika po redosledu bajtova njihovih ključeva i nizova po indeksu, tako da je svaki objekat imenovan putanjom koja do njega dovodi. Objekti do kojih obilazak ne može doći od korena vraćaju se na sintetičku putanju $Unreachable[...] koja nosi broj i generaciju objekta, što drži usiročeni sadržaj vidljivim u izveštaju umesto tihog izostavljanja

Tokovi se ne upoređuju kopiranjem. Svaki tok doprinosi inkrementalnim SHA-256 potpisom, izračunatim uz naknadno vraćanje originalne pozicije toka, tako da upoređivanje dva fajla od po sto megabajta ne znači materijalizaciju dve stotine megabajta dvaput

Usklađivanje stranica kada jedan dokument ima umetak

Poređenje 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 naivno poređenje prijavljuje svaku stranicu kao izmenjenu, što je tehnički tačno, a operativno beskorisno

HotPDF usklađuje stranice pre poređenja. Gradi potpis po stranici iz teksta koji se može izvući, vraća se na strukturni potpis za stranice bez teksta, a zatim izračunava najduži rastući podniz preko usklađenih indeksa cilja. Stranice unutar tog podniza su one koje su samo pomerene; stranice van njega su prave promene. Ta razlika je ono što čini razliku 400-stranog priručnika čitljivom, jer izveštaj kaže da je jedna stranica umetnuta umesto da je četiri stotine stranica izmenjeno

Pokretanje strukturnog poređenja

Najjednostavniji poziv uzima dva učitana dokumenta i režim. cmStructural izvodi obilazak grafa objekata, cmRenderedImage izvodi poređenje piksela, cmFull radi oba, a lakši režimi cmPageCount, cmPageText i cmObjectCount postoje za jeftine proverne testove:

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;

Izveštaj razlikuje tri stanja koja bulova vrednost ne može. identical kaže da li se bilo šta razlikovalo, comparisonComplete kaže da li je obilazak završen, a comparisonBudget imenuje ograničenje koje ga je zaustavilo ako je do toga došlo. Poređenje koje potroši budžet prijavljuje comparisonComplete=false i identical=false zajedno, jer skraćen obilazak nema osnov da tvrdi jednakost. Svaka automatizacija koja čita samo identical će vremenom tretirati zaustavljanje zbog budžeta kao pravu razliku, pa čitajte sve tri vrednosti

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

Podrazumevane vrednosti u THPDFStructuralCompareLimits.Default su podešene za prave dokumente, ne za neprijateljske, a svaki semantički relevantan budžet ima sopstveni plafon: 250.000 objekata, 2.000.000 ivica, dubina 128, 10.000 prijavljenih razlika, 64 MB po toku i 512 MB ukupno bajtova toka, 1 MB po vrednosti i 4.096 bajtova po putanji. Podignite ih namerno kada poznajete svoj korpus, a spustite kada upoređujete fajlove pristigle spolja:

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

  Options := THPDFRenderedCompareOptions.Default;
  Options.DPI := 150;                  // podrazumevano je 72
  Options.ColorTolerance := 2;         // zanemari 1-2 nivoa buke zaokruživanja
  Options.MinimumSimilarity := 0.9995;
  Options.MaxChangedPixelRatio := 0.0005;
  Options.GenerateHeatmaps := True;    // upiši preklopne slike za pregled

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

Prolaz renderovanja procenjuje broj piksela iz dimenzija stranice i tražene DPI pre nego što se ijedna bitmapa dodeli, a naknadno proverava stvarnu bitmapu, tako da izobličena geometrija stranice ne može proći pored budžeta lažući o svojoj veličini. Podizanje DPI podiže vernost i cenu kvadratno: 150 DPI je četiri puta više piksela od 72, a plafoni po stranici i ukupni plafon piksela postoje upravo zato što će serijski posao pri 300 DPI inače dodeljivati sebi put u nevolju

Koliko slično je dovoljno slično?

Dve stranice se računaju kao slične samo kada oba uslova važe: odnos izmenjenih piksela je na ili ispod MaxChangedPixelRatio, a sličnost je na ili iznad MinimumSimilarity. Dva praga umesto jednog, jer je nekoliko katastrofalno pogrešnih piksela i široko rasprostranjena blaga promena boje različit tip otkaza, i bilo koji sam po sebi može biti prihvatljiv u jednom toku rada, a diskvalifikujući u drugom. Testovi praga koriste nezaokružene vrednosti; šest decimalnih mesta u JSON-u postoje da bi izveštaji ostali stabilni i uporedivi kao razlika, ne da bi definisali poređenje

Izmenjeni pikseli se grupišu u regione koristeći pločice fiksne veličine kao čvorove sa četvorosmernom susedstvom, umesto poplavnog popunjavanja po pikselu. To drži memoriju omeđenom, a listu regiona stabilnom preko pokretanja. Skraćivanje zadržanog detalja regiona utiče samo na listing, ne na prijavljen broj regiona, pa stranica sa više izmenjenih regiona od MaxChangedRegions i dalje prijavljuje koliko ih je bilo

Jedno ponašanje vredi jasno reći jer preokreće uobičajen instinkt. Otkazi renderera, otkazi dodele memorije i otkazi preklapanja se nikada ne gutaju. Sve to se beleži kao renderError ili renderBudget i prisiljava renderComparisonComplete=false, jer je stranica koja nije uspela da se renderuje stranica koju niko nije uporedio, a prijaviti je kao identičnu gore je nego ne prijaviti ništa

Gde svaki režim pripada u pipeline-u

Strukturno poređenje odgovara na pitanje šta se promenilo i pravi je podrazumevani izbor za regresione komplete: imenuje putanju, indeks stranice i uključene brojeve objekata, pa greška ukazuje na kod koji ju je proizveo. Renderovano poređenje odgovara na pitanje da li će iko primetiti, što je pitanje za odobrenja i za proveru da li je optimizacioni prolaz zaista bio bez gubitaka

Dobro se kombinuju. Pokrenite cmStructural na svakoj izgradnji i pustite ga da glasno padne na neočekivanim izmenama na nivou objekata; pokrenite cmFull sa toplotnim mapama pre izdanja, kada je čovek dostupan da pogleda preklapanja. Za pipeline-ove koji već emituju markup stranica iz drugih razloga, tekstualni izlaz opisan u izvozu PDF stranica u SVG daje treći, ljudski-uporediv prikaz, a automatizovane provere u automatizaciji preflight izveštaja pokrivaju pitanja usklađenosti koja nijedan od dva režima poređenja nije namenjen da odgovori

Poređenje, preflight i renderovanje dele isti model objekta učitanog dokumenta, pa jedan prolaz kroz fajl može da nahrani sve tri funkcije. Kompletna lista funkcija za Delphi i C++Builder je na stranici HotPDF Delphi PDF komponente