Artykuł techniczny

Dekodowanie predyktora PDF i LZWDecode w Delphi z HotPDF

Strumień oznaczony /Predictor 12 nie oznacza, że każdy wiersz używa filtra PNG 2. HotPDF, natywny komponent VCL PDF dla Delphi i C++Builder, traktuje wartości predyktora od 10 do 15 jako jedną rodzinę: prawdziwy znacznik filtra, od 0 do 4, jest pierwszym bajtem każdego zakodowanego wiersza, a HPDFDecodePredictor odczytuje i waliduje ten znacznik wiersz po wierszu. To rozróżnienie to kształt niemal każdego błędu w tym zakątku PDF, ponieważ nic nie zgłasza wyjątku, gdy zrobisz to źle. Łańcuch filtrów działa, raster ma oczekiwany rozmiar, a obraz wychodzi jako ukośny szum lub gradient dryfujący coraz bardziej z każdą linią skanowania. Pięć liczb w /DecodeParms (ISO 32000-1 §7.4.4) w większości zmienia znaczenie bajtów, a nie ich długość, więc błędna wartość produkuje wiarygodny bałagan zamiast błędu

Dlaczego /Predictor 12 nie oznacza filtra PNG 2 na każdym wierszu?

Ponieważ numer predyktora mówi tylko „stosowana jest predykcja PNG”, nie który filtr. Enkodery PNG wybierają filtr na linię skanowania, a filtr PDF dziedziczy to, więc wartości predyktora 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) i 15 (Optimum) wszystkie dekodują się identycznie: wiodący bajt znacznika każdego wiersza jest tym, czemu dekoder musi być posłuszny. Konsekwencja układu ma równie duże znaczenie, co semantyka. Każdy zakodowany wiersz ma długość 1 + RowBytes bajtów, wejście przekracza więc wyjście dokładnie o liczbę wierszy, a strumień, którego długość nie jest całkowitą wielokrotnością RowBytes + 1, jest z definicji obcięty. HotPDF sprawdza tę granicę, zanim dotknie bajtu, odrzuca każdy znacznik powyżej 4 z komunikatem Invalid PNG predictor row tag i odczytuje poprzedni wiersz bezpośrednio z pojedynczego bufora wyjściowego zamiast materializować dwuwymiarową tablicę wierszy. Filtry 1 i 3 sięgają wstecz o BytesPerPixel w bieżącym wierszu, filtr 2 czyta prosto w górę, filtr 4 uruchamia wybór Paeth po lewej, górnej i górno-lewej — a wszystkie cztery operują na już zrekonstruowanym wyjściu, dlatego wiersz górny musi być wierszem zdekodowanym, a nigdy filtrowanym wejściem

uses
  HPDFPredictor;

var
  Filtered, Raster: AnsiString;
  ErrorText: string;
begin
  // /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
  if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
       Int64(1024) * 3 * 8192, Raster, ErrorText) then
    ConsumeRaster(Raster)
  else
    LogStreamDefect('predictor', ErrorText);   // no exception, no partial raster
end;

Argument MaxOutputBytes nie jest dekoracją. Etap predyktora jest w gruncie rzeczy etapem dekompresji, a wroga lub po prostu zepsuta wartość /Columns zamienia kilka kilobajtów wejścia w żądanie alokacji wielu gigabajtów. HotPDF liczy bity na wiersz, bajty na wiersz i całkowity rozmiar rastra najpierw w Int64, odmawia geometrii, która przepełnia, i honoruje sufit dostarczony przez wywołującego. Przekaż rzeczywistą granicę wyprowadzoną ze słownika obrazu, a tryb awarii staje się zalogowaną wiadomością zamiast okna dialogowego braku pamięci na maszynie klienta

Dlaczego TIFF Predictor 2 psuje obrazy 4-bitowe?

Ponieważ Predictor 2 to poziome różnicowanie na próbkę, nie na bajt, a przy 1, 2 lub 4 bitach na składową kilka próbek dzieli bajt. Powszechna implementacja dodaje bajt N-Colors do bajtu N, co jest poprawne przy 8 bitach na składową i po cichu błędne wszędzie indziej. Skan 8-bitowy RGB dekoduje się idealnie, a potem ten sam kod niszczy 4-bitowy obraz indeksowany, gdy tylko taki pojawi się w produkcji

Poprawna arytmetyka działa wewnątrz pola bitowego. HotPDF przechodzi próbki od indeksu Colors do Colors * Columns - 1, wyodrębnia próbkę i jej lewego sąsiada tej samej składowej z maską (1 shl BitsPerComponent) - 1 na odpowiednim przesunięciu, dodaje je modulo tę maskę i zapisuje wynik z powrotem bez naruszania innych próbek zapakowanych w tym samym bajcie. Ogon też ma znaczenie: wiersz jest dopełniony do granicy bajtu, więc bity dopełnienia po ostatniej próbce muszą przetrwać nietknięte, zamiast być wciągnięte w arytmetykę. Przy 16 bitach na składową każda próbka jest big-endianową parą bajtów, a dodawanie zawija się przy $FFFF w poprzek pary, zamiast przenosić między bajtami niezależnie; przy 8 bitach prosta rekurencja bajtowa jest poprawna, krokując po Colors, więc czerwony akumuluje się względem czerwonego, a alfa względem alfy. W każdym wariancie pierwszy piksel wiersza jest literałem, nigdy różnicą, a rekurencja restartuje na każdej granicy wiersza — predykcja TIFF nigdy nie czyta wiersza powyżej, co jest całą różnicą między nią a rodziną PNG

Co naprawdę kontroluje EarlyChange w LZWDecode?

Kontroluje, kiedy czytnik poszerza rozmiar kodu o jeden bit, a bycie o jeden kod poza rytmem psuje wszystko, co następuje. HotPDF wyraża zasadę jako pojedynczy niezmiennik: po dodaniu wpisu do słownika, następny odczyt poszerza się, gdy NextCode osiąga (1 shl CodeSize) - Ord(EarlyChange). Przy /EarlyChange 1, domyślnej wartości ISO 32000-1 §7.4.4, przełączenie następuje o jeden kod wcześniej; przy /EarlyChange 0 następuje dokładnie na granicy. Oba pojawiają się w rzeczywistych plikach i nic w strumieniu bitów nie mówi, którego użył enkoder. Reszta maszyny stanów musi poruszać się w zgodzie: kod czyszczenia resetuje razem rozmiar kodu, maskę bitową, następny wolny kod i pamięć fraz, a kod end-of-information jest odczytywany przy dowolnej aktualnej szerokości w tym momencie, nie przy początkowych 9 bitach. HotPDF zaczyna od InitialCodeSize 9, ogranicza rozmiar kodu do 12, a słownik do 4096 wpisów, i domyślnie ustawia FillOrder na foTop, ponieważ PDF pakuje kody z najstarszym bitem najpierw — foBottom istnieje dla strumieni w stylu TIFF, które tego nie robią

uses
  HPDFLZW;

var
  Decoder: TPDFLZWDecompressor;
  Parms: TPDFLZWParms;
  Plain: AnsiString;
begin
  Decoder := TPDFLZWDecompressor.Create;
  try
    Decoder.EarlyChange := True;      // /EarlyChange 1 is the PDF default
    Decoder.FillOrder := foTop;       // high-order bit first
    Decoder.MaxOutputBytes := 256 * 1024 * 1024;
    Decoder.RequireInitialClear := False;
    Decoder.RequireEndOfInformation := False;

    Parms.Predictor := 12;
    Parms.Colors := 3;
    Parms.BitsPerComponent := 8;
    Parms.Columns := 1024;
    Parms.ExpandedTo8Bit := False;
    Parms.ColorSpace := 'DeviceRGB';

    if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
      LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
        Decoder.KwKwKExpansions, Decoder.OutputBytes)
    else
      LogStreamDefect('lzw', Decoder.LastError);
  finally
    Decoder.Free;
  end;
end;

Statystyki istnieją do triage, nie próżności. Gdy plik dekoduje się do właściwej długości, ale niewłaściwych pikseli, PeakCodeSize i DictionaryAdds mówią ci natychmiast, czy czytnik kiedykolwiek poszerzył tam, gdzie zrobił to writer. Przełącz EarlyChange, zdekoduj ponownie, porównaj oba: jeśli liczby się zmieniają, masz odpowiedź w jednym przebiegu zamiast przechodzenia krok po kroku przez czytnik bitów

Gałąź KwKwK i kiedy strumień powinien po prostu zawieść

Jedyny legalny przypadek, który wygląda na nielegalny, to Code = NextCode, a HotPDF obsługuje go, konstruując wpis przed jego wyemitowaniem. Enkoder może wyemitować kod dla frazy, którą definiuje w tym samym kroku, co dzieje się zawsze, gdy wejście zawiera wzorzec postaci K w K w K; dekoder nie może wyszukać tego kodu, ponieważ jeszcze nie istnieje, więc musi zbudować Previous + First(Previous), dodać to jako nowy wpis i wyemitować wpis, który właśnie utworzył. HotPDF liczy je w KwKwKExpansions i sprawdza krzyżowo, że dodany kod to kod, o który poproszono. Wszystko powyżej NextCode to korupcja, i tam dekoder powinien się zatrzymać, a nie improwizować: HotPDF zgłasza wyjątek na przyszłym kodzie, na prefiksie słownika wskazującym poza arenę fraz, na pełnym słowniku i na pierwszym kodzie, który nie jest literałem. Dwa przełączniki ścisłości są celowo domyślnie wyłączone, RequireInitialClear i RequireEndOfInformation, ponieważ mnóstwo produkcyjnych plików PDF pomija wiodący kod czyszczenia lub kończy dane bez terminatora. Włącz je przy walidacji własnego wyjścia, zostaw wyłączone przy konsumowaniu plików z dzikiej przyrody

Gdzie /DecodeParms jest faktycznie odczytywane po stronie wczytanego dokumentu

HotPDF rozwiązuje /DecodeParms lub jego skrót /DP na słowniku strumienia obrazu, akceptuje albo słownik, albo tablicę i bierze ostatni element, gdy jest tablicą, a następnie przenosi Predictor, Colors, BitsPerComponent, Columns i EarlyChange do ścieżki rastra. Przypadek tablicy to ten, o którym ludzie zapominają: strumień filtrowany przez [/ASCII85Decode /FlateDecode] niesie równoległą tablicę parametrów, a ustawienia predyktora należą do ostatniego filtra, nie do pierwszego

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
      for I := 0 to Pdf.GetLoadedImageCount - 1 do
        if Pdf.GetLoadedImageInfo(I, Info) then
        begin
          Bmp := Pdf.ExtractLoadedImage(I);   // nil when the raster is unusable
          if Bmp <> nil then
          try
            Bmp.SaveToFile(Format('image-%d.bmp', [I]));
          finally
            Bmp.Free;
          end;
        end;
  finally
    Pdf.Free;
  end;
end;

Jeden historyczny defekt na tej ścieżce jest wart nazwania, ponieważ klasa błędu się powtarza. Stara procedura Flate z parametrami tworzyła strumień dekompresji, a potem kopiowała z oryginalnego skompresowanego wejścia, więc etap predyktora otrzymywał skompresowane bajty i sumiennie je „odpredykowywał”: zawsze błędnie, nigdy z wyjątkiem. Aktualny kod czyta tylko z dekodera przed przekazaniem wyniku do wspólnego predyktora i odrzuca raster krótszy niż obliczony rozmiar, zamiast wracać do wciąż skompresowanych bajtów — fallback, który kiedyś zamieniał niepowodzenie dekodowania w uszkodzoną bitmapę. Ta sama implementacja predyktora obsługuje teraz też strumienie cross-reference, co jest użyteczną spójnością, jeśli pracujesz też ze strumieniami obiektów i aktualizacjami przyrostowymi, a otaczająca maszyneria ekstrakcji jest omówiona w towarzyszącym artykule o wyodrębnianiu wczytanych obrazów i ich filtrów dekodowania. Obrazy przybywające jako DCTDecode lub JPXDecode nigdy nie docierają do predyktora; niosą własny skompresowany model pikseli

Przepustowość: ciągła arena fraz kontra ciągi per wpis

Zastąpienie słownika ciągów per wpis ciągłą areną fraz zmierzono jako około 1,61 razy szybsze na patologicznym wejściu: 1558 MiB/s wobec 969 MiB/s na benchmarku, którego pojedyncza najdłuższa fraza sięga 7 370 880 bajtów. Kształt tego wejścia wyjaśnia różnicę, ponieważ klasyczne implementacje wybierają jeden z dwóch złych kompromisów. Słownik wartości AnsiString alokuje i kopiuje świeży ciąg dla każdego z do 4096 wpisów, przy czym każdy nowy wpis kopiuje w całości swojego rodzica; stos prefiks/sufiks unika tej pamięci całkowicie, ale rekonstruuje każdą frazę, przechodząc łańcuch wstecz bajt po bajcie i odwracając go, co jest w porządku dla zwykłego tekstu i bolesne, gdy jedna fraza sięga megabajtów. HotPDF dopisuje każdą frazę ciągle do geometrycznie rosnącej areny, indeksuje wpisy według przesunięcia i długości i emituje frazę pojedynczym Move do bufora wyjściowego. Uczciwym kosztem jest pamięć: arena trzymająca każdą frazę w całości jest ograniczona sumą wszystkich długości fraz, a nie liczbą wpisów, co jest dokładnie powodem, dla którego MaxOutputBytes istnieje zarówno na dekompresorze, jak i na predyktorze. Wyprowadź ten limit z tego, co słownik obrazu twierdzi, że powinien być raster, a kłamliwy strumień zawodzi szybko

Dekompresor LZW, wspólny predyktor i ścieżka ekstrakcji wczytanych obrazów pokazane tutaj są dostarczane jako część standardowego HotPDF Component dla Delphi i C++Builder, z pełnym odniesieniem do filtrów i DecodeParms na stronie produktu