Odborný článok

PDF Predictor a LZWDecode dekódovanie v Delphi s HotPDF

Stream označený /Predictor 12 neznamená, že každý riadok používa PNG filter 2. HotPDF, natívna VCL PDF komponenta pre Delphi a C++Builder, traktuje hodnoty predictora 10 až 15 ako jednu rodinu: skutočný filter tag, 0 až 4, je prvý bajt každého zakódovaného riadku, a HPDFDecodePredictor tento tag číta a validuje riadok po riadku. Toto rozlíšenie je tvar takmer každej chyby v tomto kúte PDF, pretože nič nevyvolá chybu, keď to pokazíte. Filter chain beží, raster má očakávanú veľkosť, a obrázok vyjde ako diagonálny šum alebo gradient, ktorý sa s každým riadkom viac unáša. Päť čísel v /DecodeParms (ISO 32000-1 §7.4.4) väčšinou mení význam bajtov, nie ich dĺžku, takže nesprávne číslo vyprodukuje vierohodný odpad namiesto chyby

Prečo /Predictor 12 neznamená PNG filter 2 na každom riadku?

Pretože číslo predictora hovorí len „PNG predikcia sa používa", nie ktorý filter. PNG enkodéry volia filter pre každú scanline a filter PDF túto vlastnosť zdedí, takže hodnoty predictora 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) a 15 (Optimum) sa dekódujú identicky: dekodér musí poslúchnuť vedúci tag bajt každého riadku. Dôsledok na rozloženie záleží rovnako ako sémantika. Každý zakódovaný riadok má dĺžku 1 + RowBytes bajtov, vstup teda prevyšuje výstup presne o počet riadkov, a stream, ktorého dĺžka nie je celočíselným násobkom RowBytes + 1, je z definície skrátený. HotPDF túto hranicu kontroluje skôr, než sa dotkne bajtu, odmietne akýkoľvek tag nad 4 s Invalid PNG predictor row tag a číta predchádzajúci riadok priamo z jediného výstupného bufferu namiesto materializácie dvojrozmerného poľa riadkov. Filtre 1 a 3 siahajú späť o BytesPerPixel v rámci aktuálneho riadku, filter 2 číta priamo nahor, filter 4 beží Paeth voľbu cez ľavý, horný a horný-ľavý — a všetky štyri operujú na už rekonštruovanom výstupe, čo je dôvod, prečo horný riadok musí byť dekódovaný riadok a nikdy nie filtrovaný vstup

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 je dekorácia. Etapa predictora je zamaskovaná dekompresná etapa, a nepriateľská alebo len pokazená hodnota /Columns zmení pár kilobajtov vstupu na požiadavku na alokáciu mnohých gigabajtov. HotPDF vypočíta bity na riadok, bajty na riadok a celkovú veľkosť rastra najprv v Int64, odmietne geometriu, ktorá pretečie, a rešpektuje strop dodaný volajúcim. Odovzdajte skutočnú hranicu odvodenú zo slovníka obrázka, a chybový režim sa stane logovanou správou namiesto dialógu o nedostatku pamäte na počítači zákazníka

Prečo TIFF Predictor 2 poškodzuje 4-bitové obrázky?

Pretože Predictor 2 je horizontálne rozdiely per vzorka, nie per bajt, a pri 1, 2 alebo 4 bitoch na komponent zdieľa niekoľko vzoriek jeden bajt. Bežná implementácia pridá bajt N-Colors k bajtu N, čo je náhodou správne pri 8 bitoch na komponent a potichu nesprávne všade inde. 8-bitový RGB scan sa dekóduje dokonale, potom ten istý kód zničí 4-bitový indexovaný obrázok pri prvom výskyte v produkcii

Správna aritmetika pracuje vnútri bitového poľa. HotPDF prechádza vzorky od indexu Colors do Colors * Columns - 1, extrahuje vzorku a jej rovnakého-komponentu ľavého suseda s maskou (1 shl BitsPerComponent) - 1 na príslušnom posune, sčíta ich modulo túto masku a zapíše výsledok späť bez narušenia ostatných vzoriek zabalených v tom istom bajte. Aj koniec záleží: riadok je doplnený na hranicu bajtu, takže padding bity za poslednou vzorkou musia prežiť nedotknuté namiesto toho, aby boli zahrnuté do aritmetiky. Pri 16 bitoch na komponent je každá vzorka big-endian pár bajtov a sčítanie sa zalomí na $FFFF cez pár namiesto nezávislého prenosu medzi bajtmi; pri 8 bitoch je jednoduchá bajtová rekurencia správna, s krokom Colors, takže červená sa akumuluje voči červenej a alpha voči alphe. V každej variante je prvý pixel riadku literál, nikdy rozdiel, a rekurencia sa reštartuje na každej hranici riadku — TIFF predikcia nikdy nečíta riadok vyššie, čo je celý rozdiel medzi ňou a rodinou PNG

Čo skutočne riadi EarlyChange v LZWDecode?

Riadi, kedy reader rozšíri veľkosť kódu o jeden bit, a byť o jeden kód mimo poriadku poškodí všetko, čo nasleduje. HotPDF vyjadruje pravidlo ako jeden invariant: po pridaní záznamu slovníka ďalšie čítanie rozšíri, keď NextCode dosiahne (1 shl CodeSize) - Ord(EarlyChange). S /EarlyChange 1, predvoľbou ISO 32000-1 §7.4.4, sa prepnutie stane o jeden kód skôr; s /EarlyChange 0 sa stane presne na hranici. Oboje sa objavuje v reálnych súboroch a nič v bitstreame vám nepovie, ktorý z nich enkodér použil. Zvyšok stavového automatu sa musí pohybovať v súlade: clear kód resetuje veľkosť kódu, bitovú masku, ďalší voľný kód a úložisko fráz spolu, a end-of-information kód sa číta pri akejkoľvek aktuálnej šírke v danom momente, nie pri počiatočných 9 bitoch. HotPDF začína na InitialCodeSize 9, obmedzuje veľkosť kódu na 12 a slovník na 4096 záznamov, a predvolene nastavuje FillOrder na foTop, pretože PDF balí kódy s vysokým bitom prvý — foBottom existuje pre streamy v štýle TIFF, ktoré tak nerobia

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;

Štatistiky existujú pre triage, nie pre parádu. Keď sa súbor dekóduje na správnu dĺžku, ale s nesprávnymi pixelmi, PeakCodeSize a DictionaryAdds vám okamžite povedia, či reader vôbec niekedy rozšíril tam, kde rozšíril writer. Preklopte EarlyChange, dekódujte znova, porovnajte oba: ak sa čísla pohnú, máte odpoveď v jednom behu namiesto krokovania cez bit reader

Vetva KwKwK a kedy by mal stream jednoducho zlyhať

Jediný legálny prípad, ktorý vyzerá nelegálne, je Code = NextCode, a HotPDF ho spracuje tak, že zostrojí záznam pred jeho vydaním. Enkodér môže vydať kód pre frázu, ktorú práve v tom istom kroku definuje, čo sa stane vždy, keď vstup obsahuje vzor tvaru K w K w K; dekodér tento kód nevie vyhľadať, pretože ešte neexistuje, takže musí zostrojiť Previous + First(Previous), pridať ho ako nový záznam a vydať práve zostrojený záznam. HotPDF ich počíta v KwKwKExpansions a krížovo kontroluje, že kód, ktorý pridal, je kód, o ktorý bol požiadaný. Všetko nad NextCode je korupcia, a tam by mal dekodér zastaviť namiesto improvizácie: HotPDF vyvolá výnimku pri budúcom kóde, pri prefixe slovníka ukazujúcom mimo arénu fráz, pri plnom slovníku a pri prvom kóde, ktorý nie je literál. Dva prepínače prísnosti sú predvolene zámerne vypnuté, RequireInitialClear a RequireEndOfInformation, pretože mnoho produkčných PDF vynecháva úvodný clear kód alebo minie dáta bez terminátora. Zapnite ich pri validácii vlastného výstupu, nechajte vypnuté pri konzumácii súborov z divočiny

Kde sa /DecodeParms skutočne číta na strane načítaného dokumentu

HotPDF rieši /DecodeParms alebo jeho skratku /DP na slovníku streamu obrázka, akceptuje buď slovník alebo pole a berie posledný prvok, keď je to pole, potom prenesie Predictor, Colors, BitsPerComponent, Columns a EarlyChange do cesty rastra. Prípad poľa je ten, na ktorý ľudia zabúdajú: stream filtrovaný cez [/ASCII85Decode /FlateDecode] nesie paralelné pole parametrov, a nastavenia predictora patria poslednému filtru, nie prvému

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;

Za zmienku stojí jedna historická chyba na tejto ceste, pretože trieda chyby sa opakuje. Stará rutina Flate s parametrami vytvorila dekompresný stream a potom kopírovala z pôvodného komprimovaného vstupu, takže etapa predictora dostávala komprimované bajty a poctivo ich „un-predikovala": vždy zle, nikdy chyba. Súčasný kód číta len z dekodéra, kým výsledok neodovzdá zdieľanému predictoru, a odmietne raster kratší, než je vypočítaná veľkosť, namiesto toho, aby padol späť na stále komprimované bajty — fallback, ktorý predtým menil zlyhanie dekódovania na poškodenú bitmapu. Rovnaká implementácia predictora teraz slúži aj cross-reference streamom, čo je užitočná konzistencia, ak pracujete aj s object streamami a inkrementálnymi aktualizáciami, a okolitá extrakčná mašinéria je pokrytá v sprievodnom článku o extrakcii načítaných obrázkov a ich decode filtroch. Obrázky prichádzajúce ako DCTDecode alebo JPXDecode sa k predictoru nikdy nedostanú; nesú si vlastný komprimovaný pixelový model

Priepustnosť: súvislá aréna fráz oproti reťazcom per záznam

Nahradenie slovníka reťazcov per záznam súvislou arénou fráz namerali približne 1,61-násobne rýchlejšie na patologickom vstupe: 1558 MiB/s oproti 969 MiB/s na benchmarku, ktorého jediná najdlhšia fráza dosahuje 7 370 880 bajtov. Tvar tohto vstupu vysvetľuje rozdiel, pretože klasické implementácie volia jeden z dvoch zlých obchodov. Slovník hodnôt AnsiString alokuje a kopíruje čerstvý reťazec pre každý z až 4096 záznamov, pričom každý nový záznam kopíruje svojho rodiča celého; zásobník prefix/suffix sa tejto pamäti vyhne, ale rekonštruuje každú frázu prechodom reťazca spätne bajt po bajte a jeho obrátením, čo je v poriadku pre bežný text a bolestivé, keď jedna fráza dosahuje megabajty. HotPDF pripája každú frázu súvisle do geometricky rastúcej arény, indexuje záznamy podľa offsetu a dĺžky, a vydá frázu jediným Move do výstupného bufferu. Poctivá cena je pamäť: aréna držiaca každú frázu celú je ohraničená súčtom všetkých dĺžok fráz, nie počtom záznamov, čo je presne dôvod, prečo MaxOutputBytes existuje na dekompresore aj na predictore. Odvoďte tento limit z toho, čo slovník obrázka tvrdí, že raster má byť, a klamlivý stream zlyhá rýchlo

LZW dekompresor, zdieľaný predictor a cesta extrakcie načítaného obrázka tu ukázané sa dodávajú ako súčasť štandardnej HotPDF Component pre Delphi a C++Builder, s kompletnou referenciou filtrov a DecodeParms na produktovej stránke