A HotPDF Delphiből a THPDFDocComparison segítségével hasonlít össze két PDF-dokumentumot, amely mindkét fájl objektumgráfját a katalógustól kifelé haladva járja be, és kérésre minden oldalpárt le is renderel, majd megméri az eltérő pixeleket. Az eredmény egy JSON-jelentés, amely megnevezi az összes talált eltérést, a felhasznált költségkeretet, és azt, hogy az összehasonlítás lefutott-e a végéig. Mindkét folyamat számít, mert a strukturális és a vizuális összehasonlítás más-más kérdésre válaszol
A funkció mögötti kérdés általában egy kiadási kérdés. Egy jelentéskészítő motor módosítást kap, a kimenet újragenerálódik, és valakinek el kell döntenie, hogy változott-e bármi. A két fájl egymás mellett történő megnyitása kb. három oldalig skálázódik, utána elfogy a figyelem. A nyers bájtok összehasonlítása azonnal kudarcot vall, mivel ugyanannak a generátornak két futása olyan okokból ad eltérő bájtokat, amelyeknek semmi közük ahhoz, amit egy olvasó lát
Miért lehetnek a PDF-ek bájtszinten eltérők, de vizuálisan azonosak?
Két egymástól függetlenül generált, azonosan nyomtatódó PDF rendszeresen eltér a bájtjaiban, és az okok strukturálisak, nem kozmetikaiak. Az objektumszámok abban a sorrendben kerülnek kiosztásra, ahogy az objektumok éppen íródnak. A betűtípus-részhalmazok abban a sorrendben osztanak ki CID-eket, ahogy a glifákkal először találkoznak, így egy, kissé eltérő bejárás során felépített részhalmaz eltérő tartalomfolyam-bájtokat ad ugyanahhoz a látható szöveghez. A kereszthivatkozási eltolások minden alkalommal elmozdulnak, amikor egy megelőző elem hossza megváltozik
Ezért nem használhatók az objektumszámok dokumentumok közötti azonosítóként. A HotPDF ehelyett úgy építi fel az egyes pillanatképeket, hogy a katalógustól kiindulva bejárja a struktúrát, a szótárakat a kulcsaik bájtsorrendjében, a tömböket index szerint bontva ki, így minden objektumot az azonosít, hogy milyen útvonalon érhető el. Azok az objektumok, amelyeket a bejárás nem tud elérni a gyökértől, egy szintetikus $Unreachable[...] útvonalra esnek vissza, amely tartalmazza az objektumszámot és a generációt, ez pedig láthatóvá teszi az árva tartalmat a jelentésben, ahelyett hogy csendben hiányozna
A streameket nem másolással hasonlítja össze. Minden stream egy inkrementális SHA-256 aláírással járul hozzá, amelyet úgy számol ki, hogy utána visszaállítja az eredeti stream-pozíciót, így két, egyenként kétszáz megabájtos fájl összehasonlítása nem jelenti kétszer kétszáz megabájt materializálását
Oldalak igazítása, ha az egyik dokumentumban beszúrás történt
Az 1. oldal összehasonlítása az 1. oldallal, a 2. oldalé a 2. oldallal és így tovább csak akkor helyes, ha semmi nem lett beszúrva. Szúrj be egy borítóoldalt, és egy naiv összehasonlítás minden oldalt megváltozottként jelent, ami technikailag igaz, operatívan viszont haszontalan
A HotPDF az oldalak összehasonlítása előtt igazítja azokat. Oldalanként egy aláírást épít a kinyerhető szövegből, szöveg nélküli oldalaknál strukturális aláírásra vált, majd kiszámítja a leghosszabb növekvő részsorozatot az egyeztetett célindexek felett. Az e részsorozaton belüli oldalak azok, amelyek csupán eltolódtak; az azon kívüliek valódi mozgatások. Ez a megkülönböztetés teszi olvashatóvá egy 400 oldalas kézikönyv összehasonlítását, mert a jelentés azt mondja, egy oldal lett beszúrva, nem azt, hogy négyszáz oldal változott
Strukturális összehasonlítás futtatása
A legegyszerűbb hívás két betöltött dokumentumot és egy módot vár. A cmStructural az objektumgráf bejárását végzi, a cmRenderedImage a pixelösszehasonlítást, a cmFull mindkettőt, a könnyebb cmPageCount, cmPageText és cmObjectCount módok pedig olcsó gyorsellenőrzésekhez léteznek:
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;
A jelentés három állapotot különböztet meg, amit egy logikai érték nem tud. Az identical azt mondja meg, hogy volt-e bármi eltérés, a comparisonComplete azt, hogy a bejárás befejeződött-e, a comparisonBudget pedig megnevezi azt a korlátot, amely leállította a folyamatot, ha ez történt. Egy olyan összehasonlítás, amely kimeríti a költségkeretet, egyszerre jelent comparisonComplete=false és identical=false értéket, mert egy csonkolt bejárásnak nincs alapja egyenlőséget állítani. Bármely automatizálás, amely csak az identical-t olvassa, előbb-utóbb valódi eltérésként fogja kezelni a költségkeret-leállást, ezért mindhárom értéket olvasd el
Milyen korlátok tartják kordában a bejárást?
A THPDFStructuralCompareLimits.Default alapértelmezett értékei valódi dokumentumokhoz vannak méretezve, nem rosszindulatúakhoz, és minden szemantikailag releváns költségkeretnek megvan a saját felső határa: 250 000 objektum, 2 000 000 él, 128-as mélység, 10 000 jelentett eltérés, streamenként 64 MB, összesen 512 MB stream-bájt, értékenként 1 MB, útvonalanként pedig 4096 bájt. Emeld meg őket tudatosan, ha ismered a korpuszodat, és csökkentsd, amikor kívülről érkezett fájlokat hasonlítasz össze:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // gyors hibajelzés CI-ben
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // az alapértelmezett érték 72
Options.ColorTolerance := 2; // az 1-2 szintű kerekítési zajt figyelmen kívül hagyja
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // átfedő képek írása áttekintéshez
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
A renderelt folyamat a pixelszámot az oldalméretekből és a kért DPI-ből becsli meg, mielőtt bármilyen bitkép lefoglalásra kerülne, majd utólag újraellenőrzi a tényleges bitképet, így egy hibás oldalgeometria nem tud átcsúszni a költségkereten azzal, hogy hazudik a méretéről. A DPI emelése négyzetesen növeli a hűséget és a költséget: a 150 DPI négyszer annyi pixelt jelent, mint a 72, az oldalankénti és a teljes pixelhatár pedig pontosan azért létezik, mert egy 300 DPI-s kötegelt feladat egyébként bajba foglalhatja magát
Mennyire hasonló a kellően hasonló?
Két oldal csak akkor számít hasonlónak, ha mindkét feltétel teljesül: a megváltozott pixelek aránya eléri vagy alatta marad a MaxChangedPixelRatio értéknek, és a hasonlóság eléri vagy meghaladja a MinimumSimilarity értéket. Két küszöbérték egy helyett, mert egy maroknyi katasztrofálisan rossz pixel és a színek apró elmozdulásainak széles elterjedése két különböző hiba, és bármelyik önmagában elfogadható lehet az egyik munkafolyamatban, kizáró ok a másikban. A küszöbtesztek kerekítés nélküli értékeket használnak; a JSON-ban szereplő hat tizedesjegy a jelentések stabilitását és összehasonlíthatóságát szolgálja, nem az összehasonlítás definiálását
A megváltozott pixeleket fix méretű csempéket négyirányú szomszédossággal használó csomópontokkal csoportosítja régiókká, nem pedig pixelenkénti kitöltéssel. Ez korlátozza a memóriahasználatot, és futtatások között stabilan tartja a régiólistát. A megőrzött régiórészletek csonkolása csak a listázást érinti, nem a jelentett régiószámot, így egy olyan oldal, amelyen több megváltozott régió van, mint a MaxChangedRegions, továbbra is jelenti, hány volt belőlük
Egy viselkedést érdemes egyértelműen kimondani, mert ez megfordítja a szokásos ösztönt. A renderelési hibákat, a lefoglalási hibákat és az átfedési hibákat soha nem nyeli el a rendszer. Minden ilyen jellegű esemény renderError vagy renderBudget formájában kerül rögzítésre, és renderComparisonComplete=false értéket kényszerít ki, mert az az oldal, amelyet nem sikerült renderelni, olyan oldal, amelyet senki sem hasonlított össze, és azonosként jelenteni rosszabb, mint egyáltalán nem jelenteni
Hol a helye az egyes módoknak egy folyamatban?
A strukturális összehasonlítás arra válaszol, hogy mi változott, és a helyes alapértelmezés regressziós tesztkészletekhez: megnevezi az útvonalat, az oldalindexet és az érintett objektumszámokat, így egy hiba egyenesen az azt előidéző kódra mutat. A renderelt összehasonlítás arra válaszol, hogy bárki észreveszi-e, ami a jóváhagyások kérdése, valamint annak ellenőrzése, hogy egy optimalizálási lépés valóban veszteségmentes volt-e
Jól kiegészítik egymást. Futtasd a cmStructural-t minden buildnél, és hagyd, hogy hangosan elbukjon a váratlan objektumszintű változásoknál; futtasd a cmFull-t hőtérképekkel egy kiadás előtt, amikor van ember, aki megnézi az átfedéseket. Azoknál a folyamatoknál, amelyek más okból már oldaljelölést állítanak elő, a PDF-oldalak SVG-be exportálásában leírt szöveges kimenet egy harmadik, ember számára is összehasonlítható nézetet ad, a preflight jelentés automatizálásban szereplő automatizált ellenőrzések pedig olyan megfelelőségi kérdéseket fednek le, amelyekre egyik összehasonlítási mód sem hivatott válaszolni
Az összehasonlítás, a preflight és a renderelés ugyanazt a betöltöttdokumentum-objektummodellt osztja meg, így egyetlen áthaladás egy fájlon mindhármat táplálhatja. A Delphihez és C++Builderhez elérhető teljes funkciólista a HotPDF Delphi PDF-komponens oldalán található