Stream označený /Predictor 12 neznamená, že každý řádek používá PNG filtr 2. HotPDF, nativní VCL PDF komponenta pro Delphi a C++Builder, zachází s hodnotami predictoru 10 až 15 jako s jednou rodinou: skutečná značka filtru, 0 až 4, je první bajt každého zakódovaného řádku, a HPDFDecodePredictor tuto značku čte a validuje řádek po řádku. Toto rozlišení je tvarem téměř každé chyby v tomto koutě PDF, protože když se to udělá špatně, nic nevyhodí výjimku. Řetěz filtrů proběhne, raster má očekávanou velikost a obrázek vyjde jako diagonální šum nebo přechod, který se s každou skenovací řádkou vzdaluje čím dál víc. Pět čísel v /DecodeParms (ISO 32000-1 §7.4.4) většinou mění význam bajtů, nikoli jejich délku, takže špatná hodnota vyprodukuje věrohodný nesmysl místo chyby
Proč /Predictor 12 neznamená PNG filtr 2 na každém řádku?
Protože číslo predictoru pouze říká „používá se PNG predikce“, neurčuje který filtr. PNG enkodéry volí filtr pro každou skenovací řádku a PDF filtr to zdědí, takže hodnoty predictoru 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) a 15 (Optimum) se všechny dekódují stejně: dekodér se musí řídit vedoucím bajtem značky každého řádku. Důsledek pro rozvržení je stejně důležitý jako sémantika. Každý zakódovaný řádek má délku 1 + RowBytes bajtů, vstup tedy přesahuje výstup přesně o počet řádků, a stream, jehož délka není celočíselným násobkem RowBytes + 1, je z definice zkrácený. HotPDF tuto hranici kontroluje ještě předtím, než sáhne na jediný bajt, odmítá jakoukoli značku nad 4 s hláškou Invalid PNG predictor row tag a předchozí řádek čte přímo z jediného výstupního bufferu místo materializace dvourozměrného pole řádků. Filtry 1 a 3 sahají zpět o BytesPerPixel v rámci aktuálního řádku, filtr 2 čte přímo nahoru, filtr 4 provádí volbu Paeth přes levý, horní a levý-horní pixel – a všechny čtyři pracují nad už rekonstruovaným výstupem, a proto horní řádek musí být dekódovaný řádek, nikdy 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 není ozdoba. Fáze predictoru je dekompresní fáze v přestrojení a nepřátelská nebo prostě rozbitá hodnota /Columns promění pár kilobajtů vstupu na požadavek na alokaci mnoha gigabajtů. HotPDF nejprve spočítá bity na řádek, bajty na řádek a celkovou velikost rastru v Int64, odmítne geometrii, která přeteče, a respektuje strop dodaný volajícím. Předáte-li reálnou mez odvozenou ze slovníku obrázku, selhání se stane zalogovanou zprávou místo dialogu o nedostatku paměti na počítači zákazníka
Proč TIFF Predictor 2 poškozuje 4bitové obrázky?
Protože Predictor 2 je horizontální rozdílování na úrovni vzorku, nikoli bajtu, a při 1, 2 nebo 4 bitech na komponentu sdílí několik vzorků jeden bajt. Běžná implementace přičítá bajt N-Colors k bajtu N, což je náhodou správně při 8 bitech na komponentu a potichu špatně všude jinde. 8bitový RGB sken se dekóduje bezvadně, a pak stejný kód zničí 4bitový indexovaný obrázek hned, jakmile se nějaký poprvé objeví v produkci
Správná aritmetika pracuje uvnitř bitového pole. HotPDF prochází vzorky od indexu Colors do Colors * Columns - 1, extrahuje vzorek a jeho levého souseda ve stejné komponentě pomocí masky (1 shl BitsPerComponent) - 1 na příslušném posunu, sečte je modulo tuto masku a výsledek zapíše zpět, aniž by narušil ostatní vzorky zabalené do stejného bajtu. Záleží i na konci řádku: řádek je doplněn na hranici bajtu, takže vycpávkové bity za posledním vzorkem musí přežít nedotčené, místo aby byly vtaženy do aritmetiky. Při 16 bitech na komponentu je každý vzorek big-endian párem bajtů a sčítání se přetáčí na $FFFF napříč tímto párem, nikoli s přenosem mezi bajty nezávisle; při 8 bitech je jednoduchá bajtová rekurence správná, s krokem Colors, takže se červená kumuluje proti červené a alfa proti alfě. V každé variantě je první pixel řádku literál, nikdy rozdíl, a rekurence se restartuje na každé hranici řádku – TIFF predikce nikdy nečte řádek nad sebou, a to je celý rozdíl mezi ní a rodinou PNG
Co EarlyChange ve skutečnosti řídí v LZWDecode?
Řídí, kdy čtenář rozšíří velikost svého kódu o jeden bit, a být o jeden kód mimo krok poškodí vše, co následuje. HotPDF vyjadřuje toto pravidlo jako jediný invariant: po přidání položky do slovníku se další čtení rozšíří, jakmile NextCode dosáhne (1 shl CodeSize) - Ord(EarlyChange). S /EarlyChange 1, výchozí hodnotou dle ISO 32000-1 §7.4.4, dojde k přepnutí o jeden kód dřív; s /EarlyChange 0 se to stane přesně na hranici. Obě varianty se objevují v reálných souborech a nic v bitovém proudu neříká, kterou z nich enkodér použil. Zbytek stavového automatu se musí pohybovat v zákrytu: clear kód resetuje velikost kódu, bitovou masku, další volný kód i úložiště frází společně, a kód konce informace (end-of-information) se čte na jakékoli šířce, která je právě aktuální, nikoli na počátečních 9 bitech. HotPDF začíná na InitialCodeSize 9, omezuje velikost kódu na 12 a slovník na 4096 položek a výchozí FillOrder nastavuje na foTop, protože PDF balí kódy nejvyšším bitem napřed – foBottom existuje pro streamy ve stylu TIFF, které to nedělají
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;
Statistiky existují pro triáž, ne pro parádu. Když se soubor dekóduje na správnou délku, ale se špatnými pixely, PeakCodeSize a DictionaryAdds vám okamžitě řeknou, zda se čtenář vůbec rozšířil tam, kde to udělal writer. Přepněte EarlyChange, dekódujte znovu, obě porovnejte: pokud se čísla změní, máte odpověď na jeden běh místo krokování bitovým čtenářem
Větev KwKwK a kdy má stream prostě selhat
Jediný legální případ, který vypadá nelegálně, je Code = NextCode, a HotPDF jej řeší tak, že položku sestaví ještě před jejím vydáním. Enkodér může vydat kód pro frázi, kterou ve stejném kroku právě definuje, což nastává vždy, když vstup obsahuje vzor tvaru K w K w K; dekodér tento kód nemůže vyhledat, protože ještě neexistuje, takže musí sestavit Previous + First(Previous), přidat to jako novou položku a vydat právě vytvořenou položku. HotPDF tyto případy počítá v KwKwKExpansions a křížově ověřuje, že kód, který přidal, je kódem, o který byl požádán. Vše nad NextCode je poškození dat, a tam by měl dekodér spíš zastavit než improvizovat: HotPDF vyhodí výjimku u budoucího kódu, u prefixu slovníku ukazujícího mimo arénu frází, u plného slovníku a u prvního kódu, který není literál. Dva přepínače přísnosti jsou záměrně ve výchozím stavu vypnuté, RequireInitialClear a RequireEndOfInformation, protože spousta produkčních PDF vynechává úvodní clear kód nebo dojdou data bez terminátoru. Zapněte je při validaci vlastního výstupu, nechte je vypnuté při konzumaci souborů z divočiny
Kde se /DecodeParms skutečně čte na straně načteného dokumentu
HotPDF rozřeší /DecodeParms nebo jeho zkratku /DP na slovníku streamu obrázku, přijme buď slovník, nebo pole, a když je to pole, vezme poslední prvek, poté přenese Predictor, Colors, BitsPerComponent, Columns a EarlyChange do cesty rastru. Případ pole je ten, na který lidé zapomínají: stream filtrovaný [/ASCII85Decode /FlateDecode] nese paralelní pole parametrů a nastavení predictoru patří k poslednímu filtru, nikoli k první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 zmínku stojí jedna historická vada na této cestě, protože se tato třída chyby opakuje. Stará rutina pro Flate s parametry vytvořila dekompresní stream a pak kopírovala ze zdrojového komprimovaného vstupu, takže fáze predictoru dostávala komprimované bajty a poslušně je „odpredikovala“: vždy špatně, nikdy s výjimkou. Současný kód čte pouze z dekodéru, než výsledek předá sdílenému predictoru, a odmítne raster kratší než spočítaná velikost místo návratu k ještě komprimovaným bajtům – návratu, který dřív měnil selhání dekódování na poškozenou bitmapu. Tatáž implementace predictoru teď obsluhuje i cross-reference streamy, což je užitečná konzistence, pokud pracujete i s object streamy a přírůstkovými aktualizacemi, a okolní mechanismus extrakce je popsán v doprovodném článku o extrakci načtených obrázků a jejich dekódovacích filtrech. Obrázky přicházející jako DCTDecode nebo JPXDecode se k predictoru vůbec nedostanou; nesou si svůj vlastní komprimovaný model pixelů
Propustnost: souvislá aréna frází versus řetězce po jednotlivých položkách
Nahrazení slovníku řetězců po jednotlivých položkách souvislou arénou frází naměřilo asi 1,61krát vyšší rychlost na patologickém vstupu: 1558 MiB/s proti 969 MiB/s na benchmarku, jehož jediná nejdelší fráze dosahuje 7 370 880 bajtů. Tvar tohoto vstupu vysvětluje rozdíl, protože klasické implementace volí jeden ze dvou špatných kompromisů. Slovník hodnot AnsiString alokuje a kopíruje čerstvý řetězec pro každou z až 4096 položek, přičemž každá nová položka kopíruje celého svého rodiče; zásobník prefix/suffix se této paměti zcela vyhne, ale každou frázi rekonstruuje procházením řetězu zpětně po jednom bajtu a jeho obracením, což je v pořádku pro běžný text a bolestivé, když jedna fráze dosáhne megabajtů. HotPDF připojuje každou frázi souvisle do geometricky rostoucí arény, indexuje položky podle offsetu a délky a vydává frázi jediným Move do výstupního bufferu. Poctivou cenou je paměť: aréna držící každou frázi celou je omezena součtem délek všech frází, nikoli počtem položek, a přesně proto existuje MaxOutputBytes jak u dekompresoru, tak u predictoru. Odvoďte tento limit z toho, co slovník obrázku tvrdí, že by raster měl být, a lživý stream selže rychle
Zde ukázaný LZW dekompresor, sdílený predictor a cesta extrakce načtených obrázků jsou součástí standardní HotPDF Component pro Delphi a C++Builder, s kompletní referencí filtrů a DecodeParms na produktové stránce