HotPDF 2.747.0 dekoduje obrazy WebP za pomocą napisanego od zera w Object Pascalu dekodera VP8L (WebP lossless), dzięki czemu THotPDF.AddImageFromFile przyjmuje bezpośrednio ścieżkę .webp, bez dostarczania DLL libwebp i bez uruchamiania procesu pomocniczego. Dekoder w całości implementuje sekcję 3 RFC 9649: przejście po kontenerze RIFF, kanoniczne kody prefiksowe, odwołania wsteczne LZ77, cache kolorów i wszystkie cztery transformacje odwrotne. Ramki stratne VP8 są głośno odrzucane zamiast częściowego dekodowania
Impuls był zwyczajny. Narzędzie projektowe eksportuje każdy zasób jako WebP, bo to nowoczesny domyślny format, zasoby trafiają do generatora faktur albo katalogów, który przez dekadę bez problemu przyjmował PNG i JPEG, i nagle połowa danych wejściowych jest odrzucana. Oczywistą poprawką jest podpięcie libwebp i pójście dalej. Oczywista poprawka zamienia jednak samowystarczalny komponent VCL w coś z własną historią wdrożenia
Dlaczego implementować VP8L zamiast podłączać libwebp?
HotPDF implementuje kodek w Pascalu, ponieważ komponent Delphi kompilowany przez klientów do ich własnego pliku wykonywalnego nie może po cichu nabywać DLL runtime. Natywna zależność oznacza osobny plik 32-bitowy i 64-bitowy do śledzenia, wersję do przypięcia, łańcuch podpisywania kodu do wyjaśnienia osobie wykonującej wdrożenie oraz kolejny plik, którego program antywirusowy na zablokowanym terminalu może nie zaakceptować. Dla komponentu, którego główną zaletą jest wstawienie go do projektu i działanie, jest to realny koszt, a nie teoretyczny problem. Druga połowa argumentu mówi, że VP8L jest mały: format z kodami prefiksowymi i LZ77, czterema transformacjami odwrotnymi oraz mapą odległości sąsiedztwa z 120 wpisami, a cały dekoder w HPDFWebP.pas ma mniej niż 900 linii Pascala. Wewnątrz THotPDF.AddImage gałąź WebP znajduje się w tym samym dispatchu rozszerzeń, który już kieruje .jp2, .j2k, .jpt i .jpc do ścieżki JPEG 2000, więc plumbing był już na miejscu, w tym samym miejscu opisanym w instrukcji dodawania obrazów JPEG 2000 do PDF-ów w Delphi. Wywołujący potrzebujący surowych pikseli zamiast obrazu PDF mogą przejść bezpośrednio do HPDFDecodeWebPLossless, który wypełnia TWebPCardinalArray wartościami $AARRGGBB w kolejności wierszy skanowania
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp jest kierowane do wbudowanego dekodera VP8L, bez DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dlaczego bitstream VP8L jest czytany naraz w dwóch kierunkach?
Bo kolejność bitów kontenera i kolejność bitów kodu prefiksowego są określone niezależnie, a VP8L wybiera dla nich przeciwne konwencje. Sekcja 3.2 RFC 9649 mówi wprost, że bitstream jest czytany od najmniej znaczącego bitu: czytnik zaczyna od bitu 0 bajtu i idzie w górę. Kanoniczne kody prefiksowe niesione przez ten strumień przychodzą od najbardziej znaczącego bitu, od korzenia drzewa, więc przebieg dekodowania przesuwa akumulator w lewo i dokłada każdy nowy bit na dole. Czytnik i przebieg kodu idą zatem w przeciwnych kierunkach w tej samej pętli, co za każdym ponownym odczytaniem wygląda jak błąd
function TWebPBitReader.ReadBit: Integer;
begin
if BytePos >= Length(Data) then
raise EWebPDecode.Create('WebP bitstream exhausted');
Result := (Data[BytePos] shr BitPos) and 1; // od najmniej znaczącego bitu, RFC 9649 3.2
Inc(BitPos);
if BitPos = 8 then
begin
BitPos := 0;
Inc(BytePos);
end;
end;
// przebieg kanoniczny działa w drugą stronę: pierwszy bit ze strumienia
// jest najbardziej znaczącym bitem kodu
for Len := 1 to 15 do
begin
Code := (Code shl 1) or BR.ReadBit;
if Counts[Len] > 0 then
begin
if Code - First < Counts[Len] then
Exit(Symbols[Index + Code - First]);
First := (First + Counts[Len]) shl 1;
Index := Index + Counts[Len];
end
else
First := First shl 1;
end;
Trzy szczegóły RFC, które po cichu desynchronizują strumień
Trzy semantyki w RFC 9649 są opisane dokładnie raz, łatwo je przeoczyć, a każda kosztuje albo oszczędza jeden bit, co wystarczy, aby każda późniejsza tablica stała się szumem. Wszystkie trzy znaleziono w dekoderze VP8L HotPDF i wszystkie dają ten sam objaw: wiarygodnie wyglądający obraz, który wszędzie jest błędny
- Obraz kodowany entropijnie w roli innej niż podstawowa nie zapisuje w ogóle bitu meta-prefiksu. ABNF dla
entropy-coded-imagepo prostu nie zawiera tego elementu, więc odczyt jednego bitu desynchronizuje strumień o bit. HotPDF przekazujeAllowMeta = Falsedla samego obrazu entropijnego, dla danych predykcji i transformacji kolorów oraz dla palety indeksowania kolorów - Jednoliściowy kod prefiksowy zużywa zero bitów. Sekcja 3.7.2.1 RFC 9649 mówi to bezpośrednio, a przebieg kanoniczny chętnie odczytałby bit i nie potrafił go umieścić, więc
BuildHuffwykrywa łączną liczbę symboli równą 1 i oznacza drzewo jakoSingle, dekodując ten jeden symbol bez dotykania czytnika - Wartość cache_bits równa 0 oznacza rozmiar cache kolorów równy 0, a nie
1 shl 0. Wygodne przesunięcie daje 1, przez co alfabet zielonego kanału256 + 24 + CacheSizema wartość 281 zamiast 280, a każdy odczyt tabeli kodów prefiksowych po nim jest przesunięty
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 naprawdę oznacza brak cache
if BR.ReadBit = 1 then
begin
CacheBits := Integer(BR.ReadBits(4));
if (CacheBits < 1) or (CacheBits > 11) then
raise EWebPDecode.Create('WebP color cache bits out of range');
CacheSize := 1 shl CacheBits;
end;
// RFC 9649 3.8.3: tylko obraz kodowany przestrzennie (ARGB) ma
// bit meta-prefiksu; role kodowane entropijnie nigdy go nie zapisują
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, nie 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Na wzorcu użytym podczas uruchamiania te trzy problemy ujawniły się odpowiednio na bicie 47, 81 i 89. Te liczby są sednem tej sekcji. Żaden z trzech nie zgłosił się jako błąd off-by-one; każdy dał obraz, który zakończył dekodowanie i wyglądał jak szum, a jedyną rzeczą rozdzielającą przypadki była dokładna pozycja bitu, w której strumień przestał zgadzać się ze wzorcem referencyjnym
Co daje różnicowanie pozycji bitów?
Różnicowanie pozycji bitów zamienia bezużyteczne pytanie w pytanie jednolinijkowe: nie dlaczego ten obraz jest błędny, lecz dlaczego strumień rozjechał się na bicie 81. Przygotowanie jest tanie. Pillow zapisuje każdy wzorzec .webp oraz zrzut .rgba własnego dekodowania tego samego obrazu; sonda Pascalowa i mały model referencyjny w Pythonie zapisują przy każdym odczycie bieżący licznik bitów; pierwsza pozycja, na której logi się rozchodzą, wskazuje miejsce błędu. Zacznij od wzorca wykonującego jak najmniej pracy: płaskiego obrazu 32x32 korzystającego wyłącznie z prostej ścieżki kodu. Doprowadź go do zielonego wyniku, a potem dodawaj gradienty, nietypowe wymiary i alfę po jednym wzorcu. Zgadywanie kolejności bitów to sposób na zmarnowanie całego dnia
Uczciwe zastrzeżenie jest takie, że referencja również była błędna. Model Python nie odczytywał cache_bits, a jego pętla transformacji nie dochodziła do końca, więc niektóre punkty rozbieżności wynikały z utraty synchronizacji przez dekoder referencyjny, a nie przez Pascal. Błędna implementacja referencyjna nie sprawia, że implementacja testowana jest poprawna, i żadna ze stron nie dostaje kredytu zaufania: każdą rozbieżność trzeba rozstrzygnąć względem tekstu RFC. Ten tekst także pobieraj ze źródła. Podsumowania wyszukiwania regularnie zniekształcają tablice liczbowe, a mapa odległości z 120 wpisami, 14 trybów predykcji i mnożnik cache kolorów $1e35a7bd muszą zostać przepisane dokładnie
Gdzie dzielenie całkowitoliczbowe Pascala rozchodzi się z C
Transformacja kolorów VP8L używa stałego punktu 3.5 ze znakowanymi deltami i właśnie tutaj Pascal oraz C przestają się zgadzać. C przesuwa liczby ujemne arytmetycznie, co daje zaokrąglenie w dół; Pascal div obcina w kierunku zera. Dla każdego ujemnego iloczynu wyniki różnią się o jeden, więc odwrotna transformacja kolorów przesuwa kanał o jeden krok na piksel w całym obrazie. Dlatego HotPDF jawnie wykonuje zaokrąglenie w dół w FloorDiv32, zamiast polegać na div
// C przesuwa arytmetycznie i zaokrągla ujemne wartości w dół; Pascal div obcina
// w kierunku zera, więc przypadek ujemny wymaga jawnej korekty
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// Delta stałoprzecinkowa 3.5 między bajtem elementu transformacji a bajtem
// kanału koloru, oba najpierw rozszerzane ze znakiem
function ColorDelta(T, C: Integer): Integer;
var
T8, C8: Integer;
begin
T8 := T;
if T8 >= 128 then
Dec(T8, 256);
C8 := C;
if C8 >= 128 then
Dec(C8, 256);
Result := FloorDiv32(T8 * C8);
end;
Warto nazwać tę klasę defektu, ponieważ jest niewidoczna dla każdego testu, którego wzorce przypadkiem dają nieujemne iloczyny, gdzie div i floor są zgodne. To także powód, dla którego testy WebP HotPDF sprawdzają zgodność piksel po pikselu z dekodowaniem Pillow tych samych plików, a nie tolerancję: gradienty, nietypowy rozmiar 100x37, obraz 40x40 z prawdziwym kanałem alfa i płaski obraz 32x32, każdy piksel porównany bit po bicie. Jednokrokowe przesunięcie przechodzi kontrolę percepcyjną, a odpada przy kontroli bitowej
Czego obsługa WebP celowo odmawia
HotPDF dekoduje pierwszy chunk VP8L pliku WebP i nic więcej. Stratne ramki VP8, animacje oraz każdy kontener, którego pasujący chunk nie jest VP8L, zwracają False z HPDFDecodeWebPLossless, a AddImage zamienia to w wyjątek z nazwą pliku: Failed to decode WebP image (lossless VP8L only). To zamierzona granica, a nie przeoczenie: plik w złym formacie powinien zawieść w miejscu, w którym wywołujący może go wcześniej przekonwertować, zamiast produkować szary prostokąt. Pole wersji musi wynosić 0, stos transformacji jest ograniczony do czterech wpisów, a każde wyjście poza granice zgłasza EWebPDecode, który publiczny punkt wejścia zamienia na zwykłe False. Dekodowanie przy imporcie jest również kierunkiem przeciwnym do wyciągania obrazów z otwartego dokumentu, które przechodzi przez ścieżkę załadowanych obrazów opisaną w artykule o wyodrębnianiu obrazów z załadowanego PDF-a i ich filtrach dekodujących. Każdy dekoder obrazu jest parserem karmionym plikami, których nie utworzyłeś: jeśli zasoby WebP pochodzą od klientów albo z publicznego internetu, opisane sprawdzenia granic są minimum, a mocniejszą odpowiedzią jest uruchamianie kodeków obrazów w izolowanym procesie roboczym, aby wadliwa ramka nie powaliła razem z sobą hosta
Praktyczny rezultat jest taki, że aplikacja Delphi albo C++Builder może teraz wkładać zasoby WebP do PDF-a tak samo jak PNG: jedno wywołanie AddImageFromFile, jedno wywołanie ShowImage, nic dodatkowego w instalatorze. Jeśli potrzebujesz reszty otaczającego pipeline'u obrazów i dokumentów, komponent PDF HotPDF dla Delphi obejmuje zapis, ładowanie i renderowanie z tego samego zestawu unitów