HotPDF porównuje dwa dokumenty PDF z poziomu Delphi za pomocą THPDFDocComparison, które przechodzi graf obiektów obu plików od katalogu na zewnątrz, a na żądanie renderuje też każdą parę stron i mierzy różniące się piksele. Wynikiem jest raport JSON wymieniający każdą znalezioną różnicę, zużyty budżet oraz to, czy porównanie zakończyło się w całości. Oba przebiegi mają znaczenie, ponieważ diff strukturalny i diff wizualny odpowiadają na różne pytania
Pytanie stojące za tą funkcją jest zwykle pytaniem wydaniowym. Silnik raportów otrzymuje zmianę, wynik jest regenerowany i ktoś musi zdecydować, czy cokolwiek się przesunęło. Otwarcie obu plików obok siebie skaluje się do jakichś trzech stron, zanim zawiedzie uwaga. Porównanie surowych bajtów zawodzi natychmiast, ponieważ dwa uruchomienia tego samego generatora dają różne bajty z powodów niemających nic wspólnego z tym, co widzi czytelnik
Dlaczego pliki PDF mogą różnić się bajtami, a wyglądać identycznie?
Dwa niezależnie wygenerowane pliki PDF, które drukują się identycznie, rutynowo różnią się bajtami, a powody są strukturalne, nie kosmetyczne. Numery obiektów są przydzielane w kolejności, w jakiej obiekty akurat zostały zapisane. Podzbiory fontów przydzielają CID-y w kolejności pierwszego napotkania glifów, więc podzbiór zbudowany podczas nieco innego przejścia daje inne bajty strumienia treści dla tego samego widocznego tekstu. Przesunięcia w tablicy odwołań krzyżowych zmieniają się za każdym razem, gdy cokolwiek wcześniej zmienia długość
Dlatego numerów obiektów nie można wykorzystać jako tożsamości między dokumentami. HotPDF zamiast tego buduje każdy migawkowy obraz, przechodząc od katalogu, rozwijając słowniki w kolejności bajtowej ich kluczy, a tablice po indeksie, dzięki czemu każdy obiekt jest nazywany ścieżką, która do niego prowadzi. Obiekty, do których przejście nie dociera od korzenia, otrzymują zastępczą, syntetyczną ścieżkę $Unreachable[...] niosącą numer obiektu i generację, co utrzymuje osierocone treści widoczne w raporcie zamiast po cichu nieobecne
Strumienie nie są porównywane przez kopiowanie. Każdy strumień wnosi przyrostowy podpis SHA-256, obliczany z przywróceniem oryginalnej pozycji strumienia po zakończeniu, więc porównanie dwóch plików po sto megabajtów nie oznacza dwukrotnej materializacji stu megabajtów
Wyrównywanie stron, gdy w jednym dokumencie doszło do wstawienia
Porównywanie strony 1 ze stroną 1, strony 2 ze stroną 2 i tak dalej jest poprawne tylko wtedy, gdy nic nie zostało wstawione. Wstaw stronę tytułową, a naiwne porównanie zgłosi zmianę każdej strony, co jest technicznie prawdziwe i operacyjnie bezużyteczne
HotPDF wyrównuje strony przed ich porównaniem. Buduje sygnaturę dla każdej strony na podstawie tekstu możliwego do wyodrębnienia, dla stron bez tekstu przechodzi na sygnaturę strukturalną, a następnie oblicza najdłuższy rosnący podciąg dopasowanych indeksów docelowych. Strony wewnątrz tego podciągu to takie, które jedynie się przesunęły; strony poza nim to prawdziwe przesunięcia treści. Właśnie to rozróżnienie sprawia, że diff 400-stronicowego podręcznika jest czytelny, ponieważ raport mówi, że wstawiono jedną stronę, a nie że zmieniło się czterysta stron
Wykonywanie porównania strukturalnego
Najprostsze wywołanie przyjmuje dwa wczytane dokumenty i tryb. cmStructural wykonuje przejście grafu obiektów, cmRenderedImage wykonuje porównanie pikseli, cmFull robi jedno i drugie, a lżejsze tryby cmPageCount, cmPageText i cmObjectCount istnieją do tanich testów dymnych:
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;
Raport rozróżnia trzy stany, których nie da się wyrazić wartością logiczną. identical mówi, czy cokolwiek się różniło, comparisonComplete mówi, czy przejście się zakończyło, a comparisonBudget nazywa limit, który je zatrzymał, jeśli tak się stało. Porównanie, które wyczerpuje budżet, zgłasza razem comparisonComplete=false i identical=false, ponieważ obcięte przejście nie ma podstaw, by twierdzić o równości. Każda automatyzacja odczytująca tylko identical prędzej czy później potraktuje zatrzymanie z powodu budżetu jako prawdziwą różnicę, więc odczytuj wszystkie trzy pola
Jakie limity ograniczają przejście?
Wartości domyślne w THPDFStructuralCompareLimits.Default są dobrane pod rzeczywiste dokumenty, a nie pod dokumenty wrogie, i każdy istotny semantycznie budżet ma własny pułap: 250 000 obiektów, 2 000 000 krawędzi, głębokość 128, 10 000 zgłaszanych różnic, 64 MB na strumień i 512 MB bajtów strumieni łącznie, 1 MB na wartość i 4096 bajtów na ścieżkę. Podnoś je świadomie, gdy znasz swój korpus, a obniżaj przy porównywaniu plików pochodzących z zewnątrz:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // szybka porażka w CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // domyślnie 72
Options.ColorTolerance := 2; // ignoruj szum zaokrągleń rzędu 1-2 poziomów
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // zapisz obrazy nakładek do przeglądu
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Przebieg renderowania szacuje liczbę pikseli na podstawie wymiarów strony i żądanego DPI, zanim jakakolwiek mapa bitowa zostanie zaalokowana, a potem ponownie sprawdza rzeczywistą mapę bitową, dzięki czemu zniekształcona geometria strony nie może ominąć budżetu, kłamiąc o swoim rozmiarze. Podniesienie DPI podnosi wierność i koszt kwadratowo: 150 DPI to czterokrotność pikseli 72 DPI, a pułapy pikseli na stronę i łącznie istnieją właśnie dlatego, że zadanie wsadowe przy 300 DPI w przeciwnym razie zaalokuje się w kłopoty
Jak podobne jest wystarczająco podobne?
Dwie strony liczą się jako podobne tylko wtedy, gdy spełnione są oba warunki: odsetek zmienionych pikseli jest na poziomie MaxChangedPixelRatio lub poniżej, a podobieństwo jest na poziomie MinimumSimilarity lub powyżej. Dwa progi zamiast jednego, ponieważ garść katastrofalnie błędnych pikseli i szeroka fala drobnych przesunięć koloru to różne rodzaje awarii, a każdy z osobna może być akceptowalny w jednym przepływie pracy, a dyskwalifikujący w innym. Testy progowe używają wartości niezaokrąglonych; sześć miejsc po przecinku w JSON istnieje po to, by raporty były stabilne i porównywalne, a nie po to, by definiować porównanie
Zmienione piksele są grupowane w regiony za pomocą kafelków o stałym rozmiarze jako węzłów z sąsiedztwem czterokierunkowym, a nie przez wypełnianie powodziowe piksel po pikselu. Dzięki temu pamięć pozostaje ograniczona, a lista regionów stabilna między uruchomieniami. Obcięcie zachowywanych szczegółów regionu wpływa tylko na listę, nie na zgłaszaną liczbę regionów, więc strona z większą liczbą zmienionych regionów niż MaxChangedRegions nadal zgłasza, ile ich faktycznie było
Jedno zachowanie warto wyraźnie nazwać, ponieważ odwraca zwykłą intuicję. Awarie renderera, awarie alokacji i awarie nakładek nigdy nie są połykane. Wszystko tego rodzaju jest zapisywane jako renderError albo renderBudget i wymusza renderComparisonComplete=false, ponieważ strona, której nie udało się wyrenderować, to strona, której nikt nie porównał, a zgłoszenie jej jako identycznej jest gorsze niż niezgłoszenie niczego
Gdzie w potoku pasuje każdy tryb
Porównanie strukturalne odpowiada na pytanie, co się zmieniło, i jest właściwym domyślnym wyborem dla zestawów testów regresyjnych: podaje ścieżkę, indeks strony i numery obiektów, których to dotyczy, więc awaria wskazuje na kod, który ją spowodował. Porównanie renderowane odpowiada na pytanie, czy ktokolwiek to zauważy, co jest pytaniem właściwym dla zatwierdzeń i dla sprawdzenia, czy przebieg optymalizacji naprawdę był bezstratny
Dobrze się uzupełniają. Uruchamiaj cmStructural przy każdej kompilacji i pozwól mu głośno zawieść przy nieoczekiwanych zmianach na poziomie obiektów; uruchamiaj cmFull z mapami cieplnymi przed wydaniem, gdy dostępny jest człowiek do przejrzenia nakładek. Dla potoków, które z innych powodów już generują znaczniki stron, wyjście tekstowe opisane w eksporcie stron PDF do SVG daje trzeci, czytelny dla człowieka widok porównawczy, a zautomatyzowane sprawdzenia w automatyzacji raportów preflight pokrywają pytania o zgodność, na które żaden z trybów diff nie jest przeznaczony odpowiadać
Porównanie, preflight i renderowanie współdzielą ten sam model obiektowy wczytanego dokumentu, więc jeden przebieg po pliku może zasilić wszystkie trzy. Pełna lista funkcji dla Delphi i C++Builder znajduje się na stronie komponentu PDF HotPDF dla Delphi