Tehnički članak

Dekodiranje PDF prediktora i LZWDecode u Delphiju s HotPDF-om

Tok označen s /Predictor 12 ne znači da svaki redak koristi PNG filtar 2. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, tretira vrijednosti prediktora od 10 do 15 kao jednu obitelj: pravi filtar, oznaka 0 do 4, je prvi bajt svakog kodiranog retka, a HPDFDecodePredictor čita i validira tu oznaku redak po redak. Ta razlika je oblik gotovo svakog buga u ovom kutu PDF-a, jer ništa ne javlja grešku kad ju pogrešno postavite. Lanac filtara se pokrene, raster je veličine koju ste očekivali, a slika izlazi kao dijagonalni šum ili gradijent koji sve više odstupa sa svakim retkom. Pet brojeva u /DecodeParms (ISO 32000-1 §7.4.4) uglavnom mijenjaju značenje bajtova, a ne njihovu duljinu, pa pogrešan proizvodi vjerojatno smeće umjesto pogreške

Zašto /Predictor 12 ne znači PNG filtar 2 na svakom retku?

Zato što broj prediktora govori samo "PNG predikcija je u upotrebi", a ne koji filtar. PNG kodiranje bira filtar po retku, a PDF filtar to nasljeđuje, pa se vrijednosti prediktora 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) i 15 (Optimum) dekodiraju identično: vodeći bajt oznake svakog retka je ono što dekoder mora poštovati. Posljedica na razini rasporeda jednako je bitna. Svaki kodirani redak duljine je 1 + RowBytes bajtova, ulaz stoga premašuje izlaz za točno broj redaka, a tok čija duljina nije cjelobrojni umnožak od RowBytes + 1 po definiciji je skraćen. HotPDF provjerava tu granicu prije nego dotakne ijedan bajt, odbija bilo koju oznaku iznad 4 s porukom Invalid PNG predictor row tag, i čita prethodni redak izravno iz jedinog izlaznog spremnika umjesto da materijalizira dvodimenzionalno polje redaka. Filtri 1 i 3 sežu unatrag BytesPerPixel unutar trenutnog retka, filtar 2 čita ravno gore, filtar 4 pokreće Paeth izbor preko lijevog, gornjeg i gornje-lijevog — a sva četiri djeluju na već rekonstruirani izlaz, zato gornji redak mora biti dekodirani redak, a nikad filtrirani ulaz

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 nije ukras. Faza prediktora je faza dekompresije prerušena, a neprijateljska ili tek pokvarena vrijednost /Columns pretvara nekoliko kilobajta ulaza u zahtjev za alokacijom od nekoliko gigabajta. HotPDF prvo računa bitove po retku, bajtove po retku i ukupnu veličinu rastera u Int64, odbija geometriju koja prekoračuje raspon i poštuje strop koji je zadao pozivatelj. Proslijedite pravu granicu izvedenu iz rječnika slike, i način neuspjeha postaje zabilježena poruka umjesto dijaloga o nedostatku memorije na klijentovom stroju

Zašto TIFF Predictor 2 oštećuje 4-bitne slike?

Zato što je Predictor 2 horizontalno diferenciranje po uzorku, a ne po bajtu, a pri 1, 2 ili 4 bita po komponenti nekoliko uzoraka dijeli bajt. Uobičajena implementacija dodaje bajt N-Colors bajtu N, što je slučajno ispravno pri 8 bita po komponenti, a tiho pogrešno svugdje drugdje. 8-bitni RGB sken se dekodira savršeno, a zatim isti kôd uništi 4-bitnu indeksiranu sliku prvi put kad se pojavi u produkciji

Ispravna aritmetika radi unutar bitnog polja. HotPDF prolazi uzorke od indeksa Colors do Colors * Columns - 1, izdvaja uzorak i njegova susjeda iste komponente slijeva pomoću maske od (1 shl BitsPerComponent) - 1 na odgovarajućem pomaku, zbraja ih po modulu te maske i upisuje rezultat natrag bez ometanja drugih uzoraka upakiranih u isti bajt. Rep je jednako bitan: redak se dopunjuje do granice bajta, pa bitovi dopune nakon posljednjeg uzorka moraju preživjeti netaknuti umjesto da se stope u aritmetiku. Pri 16 bita po komponenti svaki je uzorak big-endian par bajtova, a zbrajanje se prelijeva na $FFFF preko para umjesto da nosi između bajtova neovisno; pri 8 bita jednostavna rekurzija po bajtu je ispravna, koračajući po Colors tako da se crvena akumulira prema crvenoj, a alfa prema alfi. U svakoj varijanti prvi piksel retka je literal, nikad razlika, a rekurzija se ponovno pokreće na svakoj granici retka — TIFF predikcija nikad ne čita redak iznad, što je cijela razlika između nje i PNG obitelji

Što EarlyChange stvarno kontrolira u LZWDecode?

Kontrolira kad čitač proširuje veličinu kôda za jedan bit, a odstupanje od jednog kôda kvari sve što slijedi. HotPDF izražava pravilo kao jedinu invarijantu: nakon dodavanja unosa u rječnik, sljedeće čitanje se proširuje kad NextCode dosegne (1 shl CodeSize) - Ord(EarlyChange). S /EarlyChange 1, zadanom vrijednosti prema ISO 32000-1 §7.4.4, prekidač se pokreće jedan kôd ranije; s /EarlyChange 0 pokreće se točno na granici. Oba se pojavljuju u stvarnim datotekama, a ništa u nizu bitova ne govori koji je enkoder koristio. Ostatak stroja stanja mora se kretati usklađeno: kôd čišćenja resetira veličinu kôda, masku bitova, sljedeći slobodni kôd i pohranu fraza zajedno, a kôd kraja informacije čita se na trenutnoj širini u tom trenutku, ne na početnih 9 bita. HotPDF počinje na InitialCodeSize 9, ograničava veličinu kôda na 12 i rječnik na 4096 unosa, i prema zadanom postavlja FillOrder na foTop jer PDF pakira kôdove s visokim bitom prvo — foBottom postoji za tokove u TIFF stilu koji to ne rade

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;

Statistike postoje za triažu, ne za taštinu. Kad se datoteka dekodira na ispravnu duljinu, ali s pogrešnim pikselima, PeakCodeSize i DictionaryAdds odmah vam kažu je li se čitač ikad proširio ondje gdje se pisac proširio. Preokrenite EarlyChange, dekodirajte ponovno, usporedite oboje: ako se brojevi pomaknu, imate odgovor u jednom pokretanju umjesto koračanja kroz čitač bitova

Grana KwKwK i kad tok jednostavno treba propasti

Jedan legalan slučaj koji izgleda ilegalno je Code = NextCode, a HotPDF ga obrađuje izgradnjom unosa prije nego ga emitira. Enkoder može emitirati kôd za frazu koju definira u istom koraku, što se događa kad god ulaz sadrži uzorak oblika K w K w K; dekoder ne može taj kôd potražiti jer još ne postoji, pa mora izgraditi Previous + First(Previous), dodati ga kao novi unos i emitirati unos koji je upravo stvorio. HotPDF ih broji u KwKwKExpansions i unakrsno provjerava da je kôd koji je dodao kôd koji je bio zatražen. Sve iznad NextCode je oštećenje, a tu dekoder treba stati umjesto improvizirati: HotPDF podiže iznimku na budući kôd, na prefiks rječnika koji pokazuje izvan arene fraza, na pun rječnik i na prvi kôd koji nije literal. Dva prekidača strogosti namjerno su isključena prema zadanom, RequireInitialClear i RequireEndOfInformation, jer mnogi produkcijski PDF-ovi izostavljaju vodeći kôd čišćenja ili ponestanu podataka bez terminatora. Uključite ih pri validaciji vlastitog izlaza, ostavite ih isključenima pri konzumiranju datoteka iz divljine

Gdje se /DecodeParms zapravo čita na strani učitanog dokumenta

HotPDF razrješava /DecodeParms ili njegovu skraćenicu /DP na rječniku toka slike, prihvaća rječnik ili polje i uzima posljednji element kad je polje, zatim nosi Predictor, Colors, BitsPerComponent, Columns i EarlyChange u put rastera. Slučaj polja je ono što ljudi zaborave: tok filtriran s [/ASCII85Decode /FlateDecode] nosi paralelno polje parametara, a postavke prediktora pripadaju posljednjem filtru, a ne prvom

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;

Vrijedi navesti jednu povijesnu manu na tom putu, jer se klasa buga ponavlja. Stara rutina Flate-s-parametrima stvorila je tok dekompresije, a zatim kopirala iz izvornog komprimiranog ulaza, pa je faza prediktora primala komprimirane bajtove i savjesno ih de-predviđala: uvijek pogrešno, nikad prijavljeno. Trenutni kôd čita samo iz dekodera prije nego preda rezultat dijeljenom prediktoru, i odbija raster kraći od izračunate veličine umjesto da padne natrag na još komprimirane bajtove — rezervni put koji je znao pretvoriti neuspjeh dekodiranja u oštećenu bitmapu. Ista implementacija prediktora sada služi i cross-reference tokovima, što je korisna dosljednost ako radite i s tokovima objekata i inkrementalnim ažuriranjima, a okolna mehanika izdvajanja pokrivena je u popratnom članku o izdvajanju učitanih slika i njihovih filtara dekodiranja. Slike koje dolaze kao DCTDecode ili JPXDecode nikad ne dosegnu prediktor; nose vlastiti komprimirani model piksela

Propusnost: kontinuirana arena fraza naspram nizova po unosu

Zamjena rječnika niza po unosu kontinuiranom arenom fraza izmjerena je kao otprilike 1,61 puta brža na patološkom ulazu: 1558 MiB/s naspram 969 MiB/s na benchmarku čija najdulja pojedinačna fraza doseže 7 370 880 bajtova. Oblik tog ulaza objašnjava razliku, jer klasične implementacije biraju jednu od dvije loše razmjene. Rječnik vrijednosti AnsiString alocira i kopira svježi niz za svaki od do 4096 unosa, pri čemu svaki novi unos kopira cijelog svog roditelja; stog prefiksa/sufiksa izbjegava tu memoriju u potpunosti, ali rekonstruira svaku frazu koračajući lancem unatrag bajt po bajt i obrćući ga, što je u redu za obični tekst, a bolno kad jedna fraza doseže megabajte. HotPDF dodaje svaku frazu kontinuirano u geometrijski rastuću arenu, indeksira unose po pomaku i duljini, i emitira frazu jednim Move u izlazni spremnik. Iskrena cijena je memorija: arena koja drži svaku frazu u cijelosti ograničena je zbrojem svih duljina fraza, a ne brojem unosa, što je upravo razlog zašto MaxOutputBytes postoji i na dekompresoru i na prediktoru. Izvedite tu granicu iz onoga što rječnik slike tvrdi da bi raster trebao biti, i tok koji laže brzo propada

LZW dekompresor, zajednički prediktor i put izdvajanja učitane slike prikazani ovdje isporučuju se kao dio standardnog HotPDF Component za Delphi i C++Builder, s potpunom referencom filtara i DecodeParms na produktnoj stranici