Vizuální srovnání v PDFlibPas vykreslí obě PDF ve stejném DPI a diffuje to, co čtečka skutečně vidí. TPDFlib.CompareRenderedPagesWith srovná jednu dvojici stránek a vrátí změněné pixely, oblasti, masku a overlay. TPDFlib.CompareDocumentVisualsWith a TPDFlib.CompareFileVisuals párují celé dokumenty strana po straně, detekují vložené a odebrané stránky a vše reportují jako JSON
Případ použití je vizuální regresní testování. Někdo přepíše šablonu faktury, povýší fontový balíček nebo vymění renderovací engine a otázkou není, zda se bajty změnily (mění vždycky), ale zda nějaký zákazník uvidí rozdíl. Strukturální diff odpovídá na jinou otázku. Dva soubory se mohou lišit v každém content streamu a přesto malovat identické stránky a jediný posunutý operand může pohnout součtem o tři body, aniž by se dotkl čehokoli, co by textový extraktor označil. Pixely jsou jediná vrstva, kde je "vypadá stejně" testovatelné
Co vizuální PDF diff doopravdy srovnává?
PDFlibPas srovnává viditelný výsledek na bílé stránce, ne syrový obsah bufferů. Každá stránka se vykreslí přes RenderPageToString jako nekomprimované BMP, dekóduje do bitmapy shora dolů a každý pixel nesoucí alfu se před porovnáním kanálů složí přes bílou. RGB bitmapa a RGBA bitmapa, které na obrazovce vypadají stejně, se proto porovnají jako identické — přesně to chcete, když obě strany přišly z různých renderovacích cest
BMP dekodér má jednu záměrnou zvláštnost, o které stojí za to vědět. 32bitová bitmapa BI_RGB nese čtvrtý bajt na pixel, ale formát tento bajt nedefinuje: někteří zapisovači ho plní nulou, někteří 255 a někteří zbytky. PLDecodeVisualBMP ho ignoruje a barvu zachová. Brát ten bajt jako krytí by znamenalo, že renderer zapisující nuly smaže každý pixel na stránce a diff by rád ohlásil dva prázdné identické dokumenty. Krmíte-li do API nižší úrovně vlastní bitmapy, alfa se respektuje jen tehdy, když formát sami deklarujete jako vpfRGBA nebo vpfBGRA
Pixel se počítá jako změněný, když se kterýkoli z jeho kanálů červené, zelené nebo modré liší o víc než ColorTolerance. Tolerance 0 znamená shodu na bit. V praxi jednociferná hodnota pohltí chvění anti-aliasingu mezi buildy rendereru, aniž by schovala změněný znak, protože skutečná změna textu pohne kanály o hodně víc. Číslo si vyberte z ověřeného základního běhu, ne hádáním
Srovnání jedné dvojice stránek přes CompareRenderedPagesWith
TPDFlib.CompareRenderedPagesWith je nástroj pro případ, kdy už víte, které stránky k sobě patří, a chcete detail: bounding box, souvislé změněné oblasti, volitelnou masku po pixelech a volitelný overlay zbarvený do červena. Záznam voleb a typy výsledků žijí v unitě PDFlibVisualCompare a PLDefaultVisualCompareOptions dává toleranci 0, dlaždice 32 pixelů, žádné ignorované oblasti a žádnou masku ani overlay
uses
PDFlibrary, PDFlibVisualCompare;
var
Baseline, Candidate: TPDFlib;
Options: TPDFVisualCompareOptions;
Comparison: TPDFVisualCompareResult;
ErrorText: WideString;
begin
Baseline := TPDFlib.Create;
Candidate := TPDFlib.Create;
try
if (Baseline.LoadFromFile('invoice-v1.pdf', '') = 1) and
(Candidate.LoadFromFile('invoice-v2.pdf', '') = 1) then
begin
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.IncludeOverlay := True;
// Zamaskujte tiskové razítko času; pixelové jednotky při DPI níže
SetLength(Options.IgnoreRects, 1);
Options.IgnoreRects[0].Left := 600;
Options.IgnoreRects[0].Top := 20;
Options.IgnoreRects[0].Right := 790;
Options.IgnoreRects[0].Bottom := 50;
if Baseline.CompareRenderedPagesWith(Candidate, 1, 1, 96, Options,
Comparison, ErrorText) then
begin
if Comparison.ChangedPixels > 0 then
Writeln(PLVisualCompareResultToJSON(Comparison));
end
else
Writeln('Compare failed: ', ErrorText);
end;
finally
Candidate.Free;
Baseline.Free;
end;
end;
Tři detaily tady kousají. Ignorované obdélníky jsou ve vykreslených pixelech s počátkem vlevo nahoře a polouzavřenými intervaly, takže Right a Bottom jsou exkluzivní a tentýž obdélník pokryje jinou část stránky, když změníte DPI. Překrývající se ignorované obdélníky se počítají jednou a ignorované pixely se objeví v IgnoredPixels, aniž by zmizely, takže ChangedRatio zůstává poctivý, protože dělí jen porovnanými pixely. Nakonec volání pro jednu stránku vyžaduje, aby obě vykreslení měla stejný rozměr; stránka, která změnila velikost, selže chybou místo aby se natáhla na míru
Oblasti vycházejí z průchodu dlaždicemi: změněné pixely se sypou do čtverců TileSize a sousední dlaždice se sloučí do jedné oblasti s vlastními mezemi a počtem pixelů. Větší dlaždice dají méně a hrubších oblastí, což je obvykle to, co test report chce. Overlay je RGB kopie druhé stránky se změněnými pixely zbarvenými do červena, vrácená jako syrové bajty v Comparison.Overlay; zapsat ji do PNG pro artefakty CI je věc vašeho kódu, ne knihovny
Jak párování stránek na úrovni dokumentu zachází s vloženými stránkami?
TPDFlib.CompareDocumentVisualsWith páruje stránky nejdelší společnou podsekvencí (LCS) nad přesnými otisky pixelů, takže jedna vložená stránka nepřemění každou následující na falešnou změnu. Naivní smyčka stránka 1 na stránku 1, stránka 2 na stránku 2 je k ničemu v okamžiku, kdy šablona dostane titulní list: každá stránka po vložení je posunutá o jednu a report svítí červeně odshora dolů
Každá stránka se vykreslí a zredukuje na klíč složený z pixelové šířky, výšky a SHA-256 digestu viditelných pixelů. Rozměr má větší váhu, než vypadá: celobílá stránka na výšku a celobílá stránka na šířku o stejné ploše mají bajtově identické pixelové buffery a bez rozměrů v klíči by se kotvily navzájem. Identické klíče na obou stranách se stanou kotvami, LCS vybere z nich nejdelší uspořádaný řetěz a stránky mezi dvěma sousedními kotvami se párují v pořadí. Když jedna mezera drží na jedné straně víc stránek, přebytek se reportuje jako added nebo removed
var
Options: TPDFVisualCompareOptions;
Report: WideString;
begin
// Baseline a Candidate načtené jako dřív, se shodným nastavením vykreslení
Options := PLDefaultVisualCompareOptions;
Options.ColorTolerance := 8;
Options.TileSize := 64;
Report := Baseline.CompareDocumentVisualsWith(Candidate, 96, Options);
if Baseline.LastErrorCode <> 0 then
Writeln('Visual diff failed: ', Report)
else
Writeln(Report);
end;
Kotvy jsou přesné na bit a je to záměr, ne opomenutí. ColorTolerance se vztahuje jen na srovnání párovaných stránek, nikdy na otisk. Stránka, na které není nic než šum anti-aliasingu, se nezakotví, ale pořád spadne do mezery, spáruje se v pořadí se svým protějškem a pak se pod tolerancí porovná jako unchanged. Rozmazané kotvy by byly shovívavější a mnohem méně předvídatelné, protože téměř duplicitní stránky (prázdné oddělovače, opakované stránky podmínek) by se začaly párovat navzájem
Mějte jasno v tom, co toto zarovnání je. JSON ho jmenuje exact-raster-lcs-gap-order záměrně: je to rastrové zarovnání, ne sémantické rozpoznávání stránek. Pokud mezera obsahuje přepsanou stránku a vloženou stránku, párování v pořadí může srovnat přepis s vložkou a ohlásit originál jako removed. Dokumenty plné identických stránek jsou podobně dvouznačné. Když na párování verdiktu záleží, koukejte na páry, ne jen na počítadla
Čtení JSON reportu
Dokumentový report má pevný tvar, na který můžete test runnerem assertovat. Při úspěchu nese "status":"complete", "complete":true, název zarovnání, firstPageCount, secondPageCount a počítadla changedPages, addedPages, removedPages a unchangedPages. Pole pairs pak vypisuje každý řádek s firstPage, secondPage (0 pro chybějící stranu) a kind o hodnotě unchanged, changed, size_changed, added nebo removed
Řádky druhu changed a unchanged vkládají objekt comparison s width, height, comparedPixels, changedPixels, ignoredPixels, changedRatio, boundingBox (null, když se nic nezměnilo) a seznam regions z bounds plus changedPixels. Řádek size_changed nemá žádné srovnání; reportuje firstWidth, firstHeight, secondWidth a secondHeight a počítá se do changedPages. Natáhnout stránku se změněným formátem, aby se dala srovnat, by vyprodukovalo zeď nesmyslných změněných pixelů, takže knihovna ohlásí změnu geometrie a tam skončí
Dokumentový report nikdy neobsahuje masky ani overlaye. CompareDocumentVisualsWith vypne IncludeMask a IncludeOverlay, i když je vaše volby žádají, aby paměť při rostoucím počtu stránek zůstala plochá místo držet náhled celé stránky pro každý pár. Report použijte k nalezení zajímavých stránek a pak zavolejte CompareRenderedPagesWith jen na nich, abyste overlay dostali. Selhání přijdou jako "status":"error", "complete":false s řetězcem error a LastErrorCode se nastaví na 401
Srovnání se souborem přes CompareFileVisuals
TPDFlib.CompareFileVisuals je forma na jedno volání pro CI: načte druhé PDF, zkopíruje na něj aktuální nastavení rendereru a vrátí tentýž dokumentový JSON. Signatura je CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance), kde tolerance musí být 0 až 255 a DPI musí být konečné a kladné
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('baseline\statement.pdf', '') = 1 then
begin
Report := Lib.CompareFileVisuals('build\statement.pdf', '', 96, 8);
Writeln(Report);
end;
finally
Lib.Free;
end;
end;
Dědičnost rendereru je důvod, proč toto volání preferovat nad vlastním načtením druhého souboru. Načtený dokument převezme renderovací měřítko, typ ořezu a grafická nastavení aktuální instance, plus volbu enginu Cairo nebo PDFium, jsou-li zakompilované. Stavíte-li dvě instance TPDFlib ručně kvůli CompareDocumentVisualsWith, jejich identická konfigurace je na vás a nesouhlas se projeví jako každá stránka změněná. Toto volání navíc nemůže brát ignorované oblasti; sáhněte po typovaném API, když je potřebujete
Limity, cena a kam patří strukturální diff
PDFlibPas ohraničuje práci explicitně, aby nepřátelský nebo obří vstup selhal chybou místo vyčerpat paměť. Vyrenderovaná stránka nesmí přesáhnout 64 Mi pixelů, dlaždicová mřížka má strop 4 Mi dlaždic a zarovnání stránek odmítá vstupy, kde součet obou počtů stránek přeleze 8192 nebo kde matice LCS přesáhne 4 Mi buněk. Srovnání dokumentů navíc renderuje každou stránku dvakrát, jednou pro otisk a znovu pro párované srovnání, takže s tím počítejte na 150 DPI a výš. Pro většinu regresních sad chytne 72 až 96 DPI změny layoutu a textu za zlomek času
Vizuální diff a strukturální diff odpovídají na různé otázky a seriózní pipeline chce obvykle obojí. Pixelové srovnání vám řekne, zda by čtečka něco zaznamenala. Neřekne vám, zda podepsaný dokument dostal anotaci, která se renderuje mimo stránku, nebo zda inkrementální aktualizace potichu vyměnila hodnotu formulářového pole za tentýž viditelný text. Pro tu vrstvu prochází třídění změn PDF po podpisu diff na úrovni revizí. A protože volba rendereru řídí pixely, víceenginové vykreslování PDF v Delphi stojí za přečtení, než nějaký engine ukotvíte jako základnu
FAQ
Natahuje vizuální srovnání PDFlibPas stránky, které změnily velikost?
Ne. CompareRenderedPagesWith vrací chybu u stránek s různou pixelovou velikostí a dokumentový report označí takové páry size_changed s oběma rozměry místo je srovnávat
Proč se téměř identické stránky nepoužívají jako kotvy?
Kotvy používají přesný SHA-256 otisk viditelných pixelů plus velikost stránky, takže zarovnání zůstává předvídatelné. Tolerance se aplikuje poté, když se párované stránky srovnávají, takže stránky se samým šumem skončí i tak jako unchanged
Dostanu obrázek overlay pro každou stránku dokumentu?
Ne z volání na dokument, které masky a overlaye záměrně vynechává, aby paměť rostla plochě. JSON použijte k nalezení změněných stránek a pak zavolejte CompareRenderedPagesWith s IncludeOverlay na nich
Vizuální srovnání, párování stránek a zbytek renderovacích a dokumentových API dodává PDFlibPas PDF library for Delphi