Sebuah stream yang ditandai /Predictor 12 tidak berarti setiap baris memakai PNG filter 2. HotPDF, komponen PDF VCL native untuk Delphi dan C++Builder, memperlakukan nilai predictor 10 hingga 15 sebagai satu keluarga: tag filter sesungguhnya, 0 hingga 4, adalah byte pertama dari setiap baris yang di-encode, dan HPDFDecodePredictor membaca serta memvalidasi tag itu baris demi baris. Perbedaan itulah bentuk hampir semua bug di sudut PDF ini, karena tidak ada apa pun yang memunculkan error ketika Anda salah menanganinya. Rantai filter tetap berjalan, raster berukuran sesuai harapan, dan gambar keluar sebagai static diagonal atau gradien yang makin melenceng setiap scanline. Kelima angka dalam /DecodeParms (ISO 32000-1 §7.4.4) sebagian besar mengubah makna byte-nya, bukan panjangnya, sehingga satu angka yang salah menghasilkan sampah yang tampak masuk akal, bukan sebuah error
Kenapa /Predictor 12 tidak berarti PNG filter 2 di setiap baris?
Karena nomor predictor hanya menyatakan "PNG prediction sedang dipakai", bukan filter mana. Encoder PNG memilih sebuah filter per scanline dan filter PDF mewarisi itu, sehingga nilai predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth), dan 15 (Optimum) semuanya didekode secara identik: byte tag di depan setiap baris itulah yang harus dipatuhi decoder. Konsekuensi layout-nya sama pentingnya dengan semantiknya. Setiap baris yang di-encode panjangnya 1 + RowBytes byte, sehingga input melebihi output persis sejumlah baris, dan sebuah stream yang panjangnya bukan kelipatan bulat dari RowBytes + 1 menurut definisi sudah terpotong. HotPDF memeriksa batas itu sebelum menyentuh satu byte pun, menolak tag apa pun di atas 4 dengan Invalid PNG predictor row tag, dan membaca baris sebelumnya langsung dari buffer output tunggal alih-alih mewujudkan array baris dua dimensi. Filter 1 dan 3 melihat kembali sejauh BytesPerPixel di dalam baris saat ini, filter 2 membaca lurus ke atas, filter 4 menjalankan pilihan Paeth atas kiri, atas, dan atas-kiri — dan keempatnya beroperasi pada output yang sudah direkonstruksi, yang menjadi alasan mengapa baris-atas harus berupa baris yang sudah didekode dan tidak pernah berupa input yang masih terfilter
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;
Argumen MaxOutputBytes bukan sekadar hiasan. Tahap predictor sebenarnya adalah tahap dekompresi yang menyamar, dan sebuah nilai /Columns yang jahat atau sekadar rusak mengubah beberapa kilobyte input menjadi permintaan alokasi multi-gigabyte. HotPDF menghitung bit per baris, byte per baris, dan ukuran raster total dalam Int64 terlebih dahulu, menolak geometri yang overflow, dan menghormati batas atas yang diberikan caller. Berikan sebuah batas nyata yang diturunkan dari image dictionary, dan mode kegagalannya menjadi sebuah pesan yang di-log, bukan dialog out-of-memory di mesin pelanggan
Kenapa TIFF Predictor 2 merusak gambar 4-bit?
Karena Predictor 2 adalah differencing horizontal per sample, bukan per byte, dan pada 1, 2, atau 4 bit per komponen, beberapa sample berbagi satu byte. Implementasi umum menambahkan byte N-Colors ke byte N, yang kebetulan benar pada 8 bit per komponen dan diam-diam salah di semua kasus lain. Sebuah scan RGB 8-bit didekode dengan sempurna, lalu kode yang sama merusak sebuah gambar indexed 4-bit begitu satu muncul pertama kali di produksi
Aritmatika yang benar bekerja di dalam bit field. HotPDF menelusuri sample dari indeks Colors hingga Colors * Columns - 1, mengekstrak sample dan tetangga kirinya yang komponennya sama dengan sebuah mask (1 shl BitsPerComponent) - 1 pada shift yang sesuai, menjumlahkan keduanya modulo mask tersebut, dan menulis hasilnya kembali tanpa mengganggu sample lain yang dipadatkan dalam byte yang sama. Bagian ekornya juga penting: sebuah baris di-padding ke batas byte, sehingga bit padding setelah sample terakhir harus bertahan tanpa disentuh, bukan ikut dilipat ke dalam aritmatika. Pada 16 bit per komponen, setiap sample adalah pasangan byte big-endian dan penjumlahannya wrap pada $FFFF lintas pasangan tersebut, bukan carry antar byte secara independen; pada 8 bit, rekurensi byte sederhana sudah benar, melangkah sebesar Colors sehingga merah terakumulasi terhadap merah dan alpha terhadap alpha. Pada setiap varian, piksel pertama dari sebuah baris adalah literal, tidak pernah sebuah difference, dan rekurensinya dimulai ulang pada setiap batas baris — TIFF prediction tidak pernah membaca baris di atasnya, dan itulah keseluruhan perbedaan antara ini dan keluarga PNG
Apa yang sebenarnya dikontrol EarlyChange dalam LZWDecode?
Ia mengontrol kapan reader melebarkan ukuran kodenya satu bit, dan meleset satu kode saja merusak segala sesuatu yang mengikutinya. HotPDF menyatakan aturan ini sebagai satu invarian tunggal: setelah menambahkan sebuah entri dictionary, pembacaan berikutnya melebar ketika NextCode mencapai (1 shl CodeSize) - Ord(EarlyChange). Dengan /EarlyChange 1, default ISO 32000-1 §7.4.4, pergantian terjadi satu kode lebih awal; dengan /EarlyChange 0 pergantian terjadi tepat di batasnya. Keduanya muncul di berkas nyata dan tidak ada apa pun dalam bitstream yang memberi tahu Anda mana yang dipakai encoder. Sisa state machine harus bergerak selaras: sebuah clear code mereset ukuran kode, bit mask, kode bebas berikutnya, dan penyimpanan phrase secara bersamaan, dan kode end-of-information dibaca pada lebar apa pun yang sedang berlaku saat itu, bukan pada 9 bit awal. HotPDF mulai pada InitialCodeSize 9, membatasi ukuran kode maksimal 12 dan dictionary maksimal 4096 entri, dan men-default FillOrder ke foTop karena PDF memadatkan kode dengan high-order bit lebih dulu — foBottom ada untuk stream bergaya TIFF yang tidak melakukan itu
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;
Statistik ini ada untuk triase, bukan untuk gaya-gayaan. Ketika sebuah berkas didekode ke panjang yang benar tapi piksel yang salah, PeakCodeSize dan DictionaryAdds langsung memberi tahu Anda apakah reader pernah melebar di tempat yang sama dengan writer. Balik EarlyChange, dekode lagi, bandingkan keduanya: jika angkanya berubah, Anda sudah dapat jawaban dalam satu kali jalan, tanpa perlu melangkah lewat bit reader satu per satu
Cabang KwKwK, dan kapan sebuah stream seharusnya gagal saja
Satu-satunya kasus legal yang terlihat ilegal adalah Code = NextCode, dan HotPDF menanganinya dengan membangun entri itu sebelum menerbitkannya. Sebuah encoder boleh menerbitkan kode untuk sebuah phrase yang sedang didefinisikannya pada langkah yang sama, yang terjadi setiap kali input mengandung pola berbentuk K w K w K; decoder tidak bisa mencari kode itu karena belum ada, sehingga ia harus membangun Previous + First(Previous), menambahkannya sebagai entri baru, dan menerbitkan entri yang baru saja dibuatnya. HotPDF menghitung kejadian ini di KwKwKExpansions dan melakukan cross-check bahwa kode yang ditambahkannya adalah kode yang diminta. Segala sesuatu di atas NextCode adalah korupsi, dan di situ seorang decoder seharusnya berhenti, bukan berimprovisasi: HotPDF memunculkan error pada kode masa depan, pada prefix dictionary yang menunjuk ke luar phrase arena, pada dictionary yang penuh, dan pada kode pertama yang bukan literal. Dua sakelar ketat sengaja dimatikan secara default, RequireInitialClear dan RequireEndOfInformation, karena banyak PDF produksi yang menghilangkan clear code di depan atau kehabisan data tanpa terminator. Nyalakan keduanya saat memvalidasi output Anda sendiri, biarkan mati saat mengonsumsi berkas dari luar sana
Di mana /DecodeParms sebenarnya dibaca pada sisi dokumen yang dimuat
HotPDF meresolusi /DecodeParms atau singkatannya /DP pada image stream dictionary, menerima baik sebuah dictionary maupun sebuah array dan mengambil elemen terakhir ketika itu berupa array, lalu membawa Predictor, Colors, BitsPerComponent, Columns, dan EarlyChange ke dalam jalur raster. Kasus array adalah yang sering dilupakan orang: sebuah stream yang difilter oleh [/ASCII85Decode /FlateDecode] membawa array parameter paralel, dan setelan predictor-nya milik filter terakhir, bukan yang pertama
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;
Ada satu defek historis pada jalur itu yang layak disebutkan, karena kelas bug ini berulang. Rutin Flate-dengan-parameter yang lama membuat sebuah stream dekompresi lalu menyalin dari input terkompresi asli, sehingga tahap predictor menerima byte terkompresi dan dengan patuh melakukan un-predict atasnya: selalu salah, tidak pernah memunculkan error. Kode saat ini hanya membaca dari decoder sebelum menyerahkan hasilnya ke predictor bersama, dan ia menolak sebuah raster yang lebih pendek dari ukuran yang dihitung, alih-alih jatuh kembali ke byte yang masih terkompresi — sebuah fallback yang dulu mengubah kegagalan decode menjadi bitmap yang korup. Implementasi predictor yang sama itu kini juga melayani cross-reference stream, yang merupakan konsistensi yang berguna bila Anda juga bekerja dengan object stream dan incremental update, dan mesin ekstraksi di sekitarnya dibahas di tulisan pendamping tentang mengekstrak gambar yang dimuat dan decode filter-nya. Gambar yang datang sebagai DCTDecode atau JPXDecode tidak pernah mencapai predictor sama sekali; mereka membawa model piksel terkompresi mereka sendiri
Throughput: phrase arena kontigu versus string per-entri
Mengganti dictionary string per-entri dengan phrase arena kontigu terukur sekitar 1,61 kali lebih cepat pada input patologis: 1558 MiB/s melawan 969 MiB/s pada sebuah benchmark yang phrase terpanjang tunggalnya mencapai 7.370.880 byte. Bentuk input itulah yang menjelaskan kesenjangannya, karena implementasi klasik memilih salah satu dari dua trade-off yang buruk. Sebuah dictionary nilai AnsiString mengalokasikan dan menyalin sebuah string baru untuk setiap satu dari hingga 4096 entri, setiap entri baru menyalin induknya secara utuh; sebuah prefix/suffix stack menghindari memori itu sepenuhnya tapi merekonstruksi setiap phrase dengan menelusuri rantai mundur satu byte pada satu waktu lalu membaliknya, yang baik-baik saja untuk teks biasa dan menyakitkan ketika satu phrase mencapai ukuran megabyte. HotPDF menambahkan setiap phrase secara kontigu ke sebuah arena yang tumbuh secara geometris, mengindeks entri berdasarkan offset dan panjang, dan menerbitkan sebuah phrase dengan satu Move tunggal ke dalam buffer output. Biaya jujurnya adalah memori: sebuah arena yang menyimpan setiap phrase secara utuh dibatasi oleh jumlah seluruh panjang phrase, bukan oleh jumlah entri, dan itulah tepatnya alasan MaxOutputBytes ada baik pada decompressor maupun predictor. Turunkan batas itu dari apa yang diklaim image dictionary tentang seharusnya seberapa besar raster, dan sebuah stream yang berbohong akan gagal dengan cepat
LZW decompressor, predictor bersama, dan jalur ekstraksi gambar-yang-dimuat yang ditunjukkan di sini hadir sebagai bagian dari HotPDF Component standar untuk Delphi dan C++Builder, dengan referensi lengkap filter dan DecodeParms di halaman produk