HotPDF palygina du PDF dokumentus iš Delphi per THPDFDocComparison, kuri apeina abiejų failų objektų grafą nuo katalogo į išorę ir, jei paprašoma, taip pat atvaizduoja kiekvieną puslapių porą bei išmatuoja pikselius, kurie skiriasi. Rezultatas – JSON ataskaita, įvardijanti kiekvieną rastą skirtumą, sunaudotą biudžetą ir tai, ar palyginimas baigėsi iki galo. Svarbūs abu perėjimai, nes struktūrinis palyginimas ir vizualinis palyginimas atsako į skirtingus klausimus
Už šios funkcijos slypintis klausimas paprastai yra susijęs su leidimu. Ataskaitų variklis gauna pakeitimą, rezultatas perkuriamas, ir kažkas turi nuspręsti, ar kas nors pasikeitė. Atidaryti abu failus greta veikia iki maždaug trijų puslapių, po to dėmesys nebeatlaiko. Palyginti žalius baitus nepavyksta iš karto, nes du to paties generatoriaus paleidimai duoda skirtingus baitus dėl priežasčių, neturinčių nieko bendra su tuo, ką mato skaitytojas
Kodėl PDF failai gali skirtis baitais, bet atrodyti vizualiai identiški?
Du nepriklausomai sugeneruoti PDF failai, kurie atspausdinami identiškai, dažnai skiriasi baitais, ir priežastys yra struktūrinės, o ne kosmetinės. Objektų numeriai priskiriami ta tvarka, kuria objektai atsitiktinai buvo įrašyti. Šriftų poaibiai priskiria CID identifikatorius ta tvarka, kuria glifai pirmą kartą sutinkami, todėl poaibis, sukurtas per šiek tiek kitokį apėjimą, tam pačiam matomam tekstui duoda skirtingus turinio srauto baitus. Kryžminės nuorodos poslinkiai pasislenka kiekvieną kartą, kai bet kas prieš tai pakeičia ilgį
Būtent todėl objektų numeriai negali būti naudojami kaip tarpdokumentinė tapatybė. Vietoj to HotPDF kuria kiekvieną nuotrauką (snapshot) apeidama nuo katalogo, plėsdama žodynus pagal jų raktų baitų tvarką, o masyvus – pagal indeksą, todėl kiekvienas objektas įvardijamas keliu, kuriuo jis pasiekiamas. Objektai, kurių apėjimas negali pasiekti nuo šaknies, gauna dirbtinį $Unreachable[...] kelią, nešantį objekto numerį ir kartą, taip pametami objektai lieka matomi ataskaitoje, o ne tyliai dingsta
Srautai nelyginami kopijuojant. Kiekvienas srautas suformuoja laipsnišką SHA-256 parašą, apskaičiuojamą atkuriant pradinę srauto poziciją po to, todėl lyginant du šimtų megabaitų failus nereikia du kartus materializuoti dviejų šimtų megabaitų
Puslapių lygiavimas, kai viename dokumente yra įterpimas
Lyginti 1 puslapį su 1 puslapiu, 2 puslapį su 2 puslapiu ir taip toliau teisinga tik tada, kai niekas nebuvo įterpta. Įterpus viršelio puslapį naivus palyginimas praneša, kad pasikeitė kiekvienas puslapis, o tai techniškai teisinga, bet praktiškai nenaudinga
HotPDF prieš lygindama puslapius juos sulygiuoja. Ji suformuoja kiekvieno puslapio parašą iš išgaunamo teksto, o puslapiams be teksto naudoja struktūrinį parašą atsarginiu būdu, tada apskaičiuoja ilgiausią didėjančią posekę per sutampančius tikslinius indeksus. Puslapiai toje posekyje yra tie, kurie tiesiog pasislinko; puslapiai už jos ribų yra tikri pakeitimai. Būtent šis skirtumas leidžia perskaityti 400 puslapių instrukcijos palyginimą, nes ataskaita sako, kad buvo įterptas vienas puslapis, o ne kad pasikeitė keturi šimtai puslapių
Struktūrinio palyginimo vykdymas
Paprasčiausias iškvietimas priima du įkeltus dokumentus ir režimą. cmStructural atlieka objektų grafo apėjimą, cmRenderedImage atlieka pikselių palyginimą, cmFull atlieka abu, o lengvesni režimai cmPageCount, cmPageText ir cmObjectCount egzistuoja pigioms patikimumo (smoke) patikroms:
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;
Ataskaita atskiria tris būsenas, kurių loginė reikšmė negali atskirti. identical nurodo, ar kas nors skyrėsi, comparisonComplete nurodo, ar apėjimas baigėsi, o comparisonBudget įvardija ribą, kuri jį sustabdė, jei taip atsitiko. Palyginimas, išeikvojęs biudžetą, kartu praneša comparisonComplete=false ir identical=false, nes nutrauktas apėjimas neturi pagrindo tvirtinti lygybę. Bet kuri automatika, skaitanti tik identical, anksčiau ar vėliau biudžeto sustabdymą palaikys tikru skirtumu, todėl skaitykite visus tris laukus
Kokios ribos palaiko apėjimą apribotą?
Numatytosios reikšmės THPDFStructuralCompareLimits.Default parinktos realiems dokumentams, o ne priešiškiems, ir kiekvienas semantiškai svarbus biudžetas turi savo lubą: 250 000 objektų, 2 000 000 briaunų, gylis 128, 10 000 pranešamų skirtumų, 64 MB vienam srautui ir 512 MB srauto baitų iš viso, 1 MB vienai reikšmei ir 4096 baitai vienam keliui. Keiskite jas sąmoningai, kai žinote savo duomenų rinkinį, ir mažinkite, kai lyginate iš išorės atkeliavusius failus:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // greitai sustabdyti CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // numatytoji reikšmė yra 72
Options.ColorTolerance := 2; // ignoruoti 1-2 lygio apvalinimo triukšmą
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // rašyti perdangos vaizdus peržiūrai
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Atvaizdavimo perėjimas įvertina pikselių skaičių pagal puslapio matmenis ir prašomą DPI dar prieš paskirstant bet kokį bitmap, o po to patikrina faktinį bitmap dar kartą, todėl sugadinta puslapio geometrija negali apeiti biudžeto meluodama apie savo dydį. Padidinus DPI, tikslumas ir kaina auga kvadratiškai: 150 DPI turi keturis kartus daugiau pikselių nei 72, ir vieno puslapio bei bendra pikselių lubos egzistuoja būtent todėl, kad partinis darbas 300 DPI raiška kitaip paskirstytų save į bėdą
Kiek panašus yra pakankamai panašus?
Du puslapiai laikomi panašiais tik kai galioja abi sąlygos: pasikeitusių pikselių santykis yra lygus arba mažesnis už MaxChangedPixelRatio, o panašumas yra lygus arba didesnis už MinimumSimilarity. Dvi ribos vietoj vienos, nes saujelė katastrofiškai neteisingų pikselių ir platus, bet menkas spalvų poslinkis yra skirtingi gedimai, ir bet kuris iš jų vienoje darbo eigoje gali būti priimtinas, o kitoje – diskvalifikuojantis. Ribų tikrinimas naudoja neapvalintas reikšmes; šeši dešimtainiai skaitmenys JSON faile egzistuoja tam, kad ataskaitos būtų stabilios ir palyginamos, o ne tam, kad apibrėžtų patį palyginimą
Pasikeitę pikseliai grupuojami į regionus naudojant fiksuoto dydžio plyteles kaip mazgus su keturkryptiu gretumu, o ne pikselis po pikselio užpildymą (flood fill). Tai palaiko ribotą atminties naudojimą ir stabilų regionų sąrašą tarp paleidimų. Išsaugoto regiono detalumo apkarpymas paveikia tik sąrašą, bet ne pranešamą regionų skaičių, todėl puslapis, kuriame pasikeitusių regionų daugiau nei MaxChangedRegions, vis tiek praneša, kiek jų iš tikrųjų buvo
Vieną elgseną verta paminėti aiškiai, nes ji apverčia įprastą intuiciją. Atvaizdavimo gedimai, paskirstymo gedimai ir perdangos gedimai niekada nenuslepiami. Bet kas tokio pobūdžio užregistruojama kaip renderError arba renderBudget ir priverstinai nustato renderComparisonComplete=false, nes puslapis, kurio nepavyko atvaizduoti, yra puslapis, kurio niekas nepalygino, o pranešti jį kaip identišką yra blogiau nei nepranešti nieko
Kur kiekvienas režimas priklauso konvejeryje?
Struktūrinis palyginimas atsako, kas pasikeitė, ir yra teisingas numatytasis pasirinkimas regresijos rinkiniams: jis įvardija kelią, puslapio indeksą ir susijusius objektų numerius, todėl nesėkmė nurodo tiesiai į ją sukėlusį kodą. Atvaizduotas palyginimas atsako, ar kas nors tai pastebės – tai klausimas patvirtinimams ir tikrinimui, ar optimizacijos perėjimas iš tikrųjų buvo be nuostolių
Jie gerai dera kartu. Vykdykite cmStructural per kiekvieną kūrimą ir leiskite jai garsiai sužlugti dėl netikėtų objektų lygmens pakeitimų; vykdykite cmFull su šilumos žemėlapiais prieš leidimą, kai yra žmogus, galintis peržiūrėti perdangas. Konvejeriams, kurie jau kitais tikslais generuoja puslapio žymėjimą, tekstinė išvestis, aprašyta straipsnyje PDF puslapių eksportavimas į SVG, suteikia trečią, žmogui palyginamą vaizdą, o automatinės patikros straipsnyje ikispaudinės ataskaitos automatizavimas apima atitikties klausimus, į kuriuos nė vienas iš palyginimo režimų nėra skirtas atsakyti
Palyginimas, ikispaudinė patikra ir atvaizdavimas naudoja tą patį įkelto dokumento objektų modelį, todėl vienas perėjimas per failą gali aptarnauti visus tris. Pilnas funkcijų sąrašas Delphi ir C++Builder aplinkoms pateikiamas HotPDF Delphi PDF komponento puslapyje