Tehnički članak

PDF Predictor i LZWDecode dekodiranje u Delphiju sa HotPDF-om

Tok obeležen sa /Predictor 12 ne znači da svaki red koristi PNG filter 2. HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, tretira vrednosti predictora od 10 do 15 kao jednu porodicu: pravi tag filtera, od 0 do 4, je prvi bajt svakog enkodiranog reda, a HPDFDecodePredictor čita i validira taj tag red po red. Ta razlika je oblik gotovo svakog baga u ovom uglu PDF-a, jer ništa ne prijavi grešku kad je pogrešite. Lanac filtera se izvrši, raster je veličine koju ste očekivali, a slika izađe kao dijagonalni šum ili gradijent koji sve više skreće sa svakim skenlajnom. Pet brojeva u /DecodeParms (ISO 32000-1 §7.4.4) uglavnom menja značenje bajtova, a ne njihovu dužinu, pa pogrešna vrednost proizvodi verodostojno smeće umesto greške

Zašto /Predictor 12 ne znači PNG filter 2 na svakom redu?

Zato što broj predictora samo kaže „PNG predikcija je u upotrebi", a ne koji filter. PNG enkoderi biraju filter po skenlajnu, a PDF filter to nasleđuje, pa vrednosti predictora 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) i 15 (Optimum) sve dekoduju identično: vodeći tag bajt svakog reda je ono čemu dekoder mora da se povinuje. Posledica po raspored je jednako bitna koliko i semantika. Svaki enkodiran red je dug 1 + RowBytes bajtova, ulaz stoga premašuje izlaz za tačno broj redova, a tok čija dužina nije ceo umnožak RowBytes + 1 je po definiciji skraćen. HotPDF proverava tu granicu pre nego što dotakne ijedan bajt, odbacuje svaki tag iznad 4 uz Invalid PNG predictor row tag, i čita prethodni red direktno iz jedinstvenog izlaznog bafera umesto da materijalizuje dvodimenzioni niz redova. Filteri 1 i 3 sežu unazad BytesPerPixel unutar tekućeg reda, filter 2 čita pravo nagore, filter 4 izvršava Paeth izbor preko levo, gore i gore-levo — a sva četiri operišu na već rekonstruisanom izlazu, zato gornji red mora biti dekodovan red, a nikad filtriran 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. Predictor faza je faza dekompresije prerušena, a neprijateljska ili prosto pokvarena vrednost /Columns pretvara nekoliko kilobajta ulaza u zahtev za alokaciju od nekoliko gigabajta. HotPDF prvo izračunava bitove po redu, bajtove po redu i ukupnu veličinu rastera u Int64, odbija geometriju koja prekoračuje, i poštuje plafon koji je pozivalac prosledio. Prosledite stvaran limit izveden iz rečnika slike, i način otkazivanja postaje logovana poruka, a ne dijalog nedostatka memorije na mašini korisnika

Zašto TIFF Predictor 2 kvari 4-bitne slike?

Zato što je Predictor 2 horizontalno diferenciranje po uzorku, ne po bajtu, a na 1, 2 ili 4 bita po komponenti nekoliko uzoraka deli isti bajt. Uobičajena implementacija dodaje bajt N-Colors bajtu N, što je slučajno ispravno na 8 bita po komponenti, a tiho pogrešno svuda drugde. 8-bitni RGB sken se dekoduje savršeno, a zatim isti kod uništi 4-bitnu indeksiranu sliku prvi put kad se takva pojavi u produkciji

Ispravna aritmetika radi unutar bitskog polja. HotPDF obilazi uzorke od indeksa Colors do Colors * Columns - 1, izvlači uzorak i njegovog levog suseda iste komponente maskom od (1 shl BitsPerComponent) - 1 na odgovarajućem pomeraju, sabira ih po modulu te maske, i upisuje rezultat nazad bez uznemiravanja ostalih uzoraka upakovanih u isti bajt. Rep je takođe bitan: red se popunjava do granice bajta, pa bitovi popune posle poslednjeg uzorka moraju preživeti netaknuti umesto da budu uvučeni u aritmetiku. Na 16 bita po komponenti svaki uzorak je big-endian par bajtova, a sabiranje se prelama na $FFFF preko para, a ne noseći se između bajtova nezavisno; na 8 bita jednostavna bajt rekurencija je ispravna, koračajući po Colors tako da se crveno akumulira naspram crvenog, a alfa naspram alfe. U svakoj varijanti prvi piksel reda je literal, nikad razlika, a rekurencija se ponovo pokreće na svakoj granici reda — TIFF predikcija nikad ne čita red iznad, što je cela razlika između nje i PNG porodice

Šta zapravo kontroliše EarlyChange u LZWDecode-u?

Kontroliše kada čitač proširuje veličinu koda za jedan bit, a jedan kod van koraka kvari sve što sledi. HotPDF izražava pravilo kao jednu invarijantu: nakon dodavanja stavke u rečnik, sledeće čitanje se proširuje kada NextCode dostigne (1 shl CodeSize) - Ord(EarlyChange). Sa /EarlyChange 1, podrazumevanim po ISO 32000-1 §7.4.4, promena se dešava jedan kod ranije; sa /EarlyChange 0 dešava se tačno na granici. Oba se pojavljuju u stvarnim fajlovima, a ništa u bitstreamu vam ne govori koji je enkoder koristio. Ostatak automata stanja mora se kretati usklađeno: kod čišćenja resetuje veličinu koda, bitsku masku, sledeći slobodan kod i skladište fraza zajedno, a kod kraja informacije se čita na kojoj god je širini trenutno, ne na početnih 9 bita. HotPDF počinje na InitialCodeSize 9, ograničava veličinu koda na 12, a rečnik na 4096 stavki, i podrazumeva FillOrder na foTop jer PDF pakuje kodove sa bitom najvišeg reda prvim — 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;

Statistika postoji radi trijaže, ne taštine. Kada se fajl dekoduje na tačnu dužinu, ali sa pogrešnim pikselima, PeakCodeSize i DictionaryAdds vam odmah kažu da li se čitač ikada proširio tamo gde se writer proširio. Prebacite EarlyChange, dekodujte ponovo, uporedite to dvoje: ako se brojevi pomere, imate odgovor u jednom pokretanju umesto koračanja kroz bit čitač

KwKwK grana, i kada tok jednostavno treba da otkaže

Jedini legalan slučaj koji izgleda nelegalno je Code = NextCode, a HotPDF ga obrađuje tako što izgradi stavku pre nego što je emituje. Enkoder može emitovati kod za frazu koju upravo definiše u istom koraku, što se dešava kad god ulaz sadrži šablon oblika K w K w K; dekoder ne može pronaći taj kod jer još ne postoji, pa mora izgraditi Previous + First(Previous), dodati ga kao novu stavku, i emitovati stavku koju je upravo napravio. HotPDF broji te slučajeve u KwKwKExpansions i unakrsno proverava da je kod koji je dodao zaista kod koji je tražen. Sve iznad NextCode je korupcija, i tu dekoder treba da stane, a ne da improvizuje: HotPDF prijavljuje grešku na budućem kodu, na prefiksu rečnika koji pokazuje van arene fraza, na punom rečniku, i na prvom kodu koji nije literal. Dva prekidača strogosti su namerno isključena podrazumevano, RequireInitialClear i RequireEndOfInformation, jer mnogi produkcioni PDF-ovi izostavljaju vodeći kod čišćenja ili ostanu bez podataka bez terminatora. Uključite ih kad validirate sopstveni izlaz, ostavite ih isključenim kada konsumirate fajlove iz divljine

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

HotPDF razrešava /DecodeParms ili njegovu skraćenicu /DP na rečniku toka slike, prihvata ili rečnik ili niz i uzima poslednji element kad je niz, zatim prenosi Predictor, Colors, BitsPerComponent, Columns i EarlyChange u putanju rastera. Slučaj niza je onaj koji ljudi zaborave: tok filtriran sa [/ASCII85Decode /FlateDecode] nosi paralelni niz parametara, a podešavanja predictora pripadaju poslednjem filteru, 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;

Jedan istorijski defekt na toj putanji vredi pomenuti, jer se ta klasa baga ponavlja. Stara Flate-sa-parametrima rutina je kreirala tok dekompresije, a zatim kopirala iz originalnog kompresovanog ulaza, pa je predictor faza primala kompresovane bajtove i savesno ih de-predikovala: uvek pogrešno, nikad prijavljeno. Trenutni kod čita samo od dekodera pre nego što preda rezultat deljenom predictoru, i odbija raster kraći od izračunate veličine umesto da se vrati na još uvek kompresovane bajtove — fallback koji je nekad pretvarao neuspeh dekodiranja u oštećenu bitmapu. Ista implementacija predictora sada služi i cross-reference tokovima, što je korisna doslednost ako takođe radite sa tokovima objekata i inkrementalnim ažuriranjima, a okolna mašinerija ekstrakcije pokrivena je u pratećem tekstu o ekstrakciji učitanih slika i njihovih filtera dekodiranja. Slike koje stižu kao DCTDecode ili JPXDecode nikad ne stižu do predictora; nose sopstveni kompresovani model piksela

Propusnost: kontinualna arena fraza naspram string-ova po stavci

Zamena rečnika string-ova po stavci kontinualnom arenom fraza izmerena je kao oko 1,61 puta brža na patološkom ulazu: 1558 MiB/s naspram 969 MiB/s na benčmarku čija najduža pojedinačna fraza dostiže 7.370.880 bajtova. Oblik tog ulaza objašnjava tu razliku, jer klasične implementacije biraju jednu od dve loše trampe. Rečnik vrednosti AnsiString alocira i kopira svež string za svaku od do 4096 stavki, gde svaka nova stavka kopira svog roditelja u celosti; stek prefiksa/sufiksa izbegava tu memoriju u potpunosti, ali rekonstruiše svaku frazu obilazeći lanac unazad bajt po bajt i obrćući ga, što je u redu za običan tekst, a bolno kad jedna fraza dosegne megabajte. HotPDF dodaje svaku frazu kontinualno u geometrijski rastuću arenu, indeksira stavke po ofsetu i dužini, i emituje frazu jednim Move u izlazni bafer. Iskrena cena je memorija: arena koja drži svaku frazu u celosti ograničena je sumom svih dužina fraza, a ne brojem stavki, što je tačno razlog zašto MaxOutputBytes postoji i na dekompresoru i na predictoru. Izvedite tu granicu iz onoga što rečnik slike tvrdi da bi raster trebalo da bude, i tok koji laže brzo otkazuje

LZW dekompresor, deljeni predictor i putanja ekstrakcije učitanih slika prikazani ovde isporučuju se kao deo standardne HotPDF Component za Delphi i C++Builder, sa kompletnom referencom filtera i DecodeParms na proizvodnoj stranici