Sebuah object stream PDF yang mengembang tanpa error tetapi tetap terbaca sebagai noise biasanya kehilangan satu langkah: membalik ISO 32000-1 Predictor. Ketika dictionary /DecodeParms sebuah stream membawa /Predictor 2 atau lebih tinggi, byte yang dikembalikan FlateDecode bukan data asli — mereka adalah nilai yang di-difference gaya-PNG per-baris atau di-difference horizontal gaya-TIFF yang membutuhkan sebuah langkah rekonstruksi kedua sebelum lookup dictionary apa pun masuk akal. PDFiumPas, library komponen PDF VCL native untuk Delphi dan C++Builder, menambahkan langkah rekonstruksi itu di v2.16.0, secara khusus karena object stream PDF 1.5+ mengembang menjadi byte ter-difference yang tidak bisa dibaca parser dictionary mana pun
Mengapa FlateDecode saja tidak cukup
FlateDecode itu sendiri hanya dekompresi DEFLATE (ISO 32000-1 §7.4.4.1): ia mereproduksi byte apa pun yang diserahkan encoder ke compressor, tidak lebih. Predictor hidup satu lapis di atasnya, dalam dictionary /DecodeParms milik stream, dan ia mendeskripsikan sebuah transform yang diterapkan encoder sebelum kompresi — differencing mengubah rangkaian panjang nilai terstruktur yang mirip, seperti integer yang dikemas rapat di dalam sebuah cross-reference stream atau object stream, menjadi rangkaian panjang angka kecil yang jauh lebih baik dikompresi DEFLATE. ISO 32000-1 §7.4.4.3 (Table 8) eksplisit bahwa membatalkan transform ini adalah bagian dari mendekode sebuah stream terfilter, bukan sebuah langkah pembersihan opsional, namun mudah menulis sebuah helper FlateDecode yang hanya memanggil inflate dan berhenti di situ
Gejalanya khas begitu Anda tahu apa yang dicari. Byte yang di-difference-Predictor bukan noise acak — mereka masih membawa bentuk sebuah stream terkompresi, sehingga sebuah parser naif seringkali melewati beberapa token yang terlihat valid sebelum menabrak sebuah urutan byte yang sama sekali tidak mungkin sebuah nama, angka, atau delimiter PDF, dan baris berbeda gagal pada offset berbeda bergantung seberapa banyak nilai yang mendasari kebetulan berbeda dari tetangganya. Ketidakkonsistenan itulah yang membuat bug ini sulit dipastikan dari satu file yang gagal: dua PDF dari produser yang sama bisa hanya berbeda dalam nilai mana yang kebetulan berulang, sehingga satu ter-parse nyaris secara kebetulan sementara yang lain gagal total
Apa sebenarnya yang dilakukan parameter Predictor PDF?
Entry /Predictor dalam /DecodeParms memberi tahu sebuah reader yang sesuai standar pembalikan mana yang harus dijalankan, dan ISO 32000-1 Table 8 mendefinisikan nilai yang penting dalam praktik: 1 berarti tidak ada prediksi yang diterapkan, 2 memilih TIFF Predictor 2 (differencing horizontal), dan nilai apa pun dari 10 hingga 15 memilih prediksi gaya-PNG. Tiga key lagi bepergian bersamanya — /Colors, /BitsPerComponent, dan /Columns — dan bersama-sama mendeskripsikan geometri baris yang menjadi dasar penghitungan differencing tersebut, bahkan ketika stream itu tidak memegang data gambar sama sekali: sebuah object stream bukan sebuah gambar, tetapi PDF writer menggunakan kembali mesin predictor berbasis-baris yang sama untuknya karena delta-lalu-deflate mengompres integer dan offset objek yang dikemas rapat lebih baik daripada men-deflate mereka mentah
TIFF Predictor 2 adalah yang lebih sederhana dari kedua skema: setiap komponen disimpan sebagai selisih dari komponen yang sama pada piksel sebelumnya di baris yang sama, dan setiap baris di-reset di tepi kirinya alih-alih membawa sebuah selisih dari baris di atasnya. Prediksi PNG lebih rumit, karena filter sesungguhnya bisa berubah dari baris ke baris: setiap baris dimulai dengan satu byte tag tunggal — 0 untuk None, 1 untuk Sub, 2 untuk Up, 3 untuk Average, 4 untuk Paeth — dan tag itu, bukan nilai /Predictor yang dideklarasikan, yang memutuskan bagaimana baris spesifik itu direkonstruksi. Sebuah /Predictor sebesar 12 sebenarnya hanya petunjuk encoder bahwa ia lebih menyukai filter Up, di mana setiap byte dipulihkan dengan menambahkan byte tepat di atasnya pada baris sebelumnya, tetapi sebuah decoder yang benar tetap harus membaca tag pada setiap baris alih-alih mengasumsikan Up sepanjang jalan
Mengapa object stream membuat sebuah Predictor yang terlewat tak terlihat?
Object stream memperparah masalah alih-alih sekadar mengulanginya. ISO 32000-1 §7.5.7 mengizinkan sebuah writer PDF 1.5+ mengemas beberapa objek indirect ke dalam satu container terkompresi tunggal, sebuah /ObjStm, dan umum bagi persis objek yang paling dibutuhkan sebuah validator — katalog, /OutputIntents, atau sebuah stream /Metadata XMP — bepergian lewat container itu dengan /Predictor 12 tersemat, karena objek-objek itu cukup pendek dan repetitif untuk mendapat manfaat dari row differencing. Ketika langkah predictor hilang, mengembangkan object stream tersebut tidak memunculkan sebuah error: ia menghasilkan sebuah urutan byte yang terlihat secara dangkal masuk akal tetapi tidak ter-tokenize menjadi objek yang diharapkan, sehingga apa pun yang dikemas di dalamnya sekadar tidak muncul. Rendering jarang memperhatikan, karena sebuah mesin rendering yang sesuai standar sudah merekonstruksi data ter-difference-predictor sebelum ia pernah sampai ke layout; kode yang memperhatikan persis jenis yang menyembunyikan bug ini — sebuah validator, signer, atau pemeriksa versi yang menelusuri byte PDF mentah itu sendiri untuk menjawab sebuah pertanyaan struktural, tanpa fallback begitu pandangannya sendiri terhadap object stream tersebut kembali salah
PDFiumPas menabrak persis kegagalan ini sebelum v2.16.0. Object stream yang dibangun dengan /Predictor 12, kasus umum untuk writer PDF 1.5+, mengembang lewat PdfExpandObjectStreams menjadi byte ter-difference yang tidak bisa di-parse pemindai struktural, sehingga objek katalog, /OutputIntents, dan /Metadata yang dikemas di dalamnya secara efektif tak terlihat oleh pemindaian kesesuaian — tanpa exception, tanpa peringatan, hanya sebuah pemindaian yang diam-diam berperilaku seolah objek itu tidak ada. Mekanika lebih dalam tentang bagaimana PDFiumPas menyelesaikan sebuah object stream terhadap tabel cross-reference aktif, termasuk kasus xref-stream hybrid dan murni, dibahas terpisah di artikel tentang memvalidasi object dan xref stream dengan PDFiumPas; langkah predictor yang dijelaskan di sini berjalan setelah penyelesaian itu, pada byte yang benar-benar dikandung setiap objek terkompresi
Membalik baris Predictor PNG dan TIFF dalam Pascal
PDFiumPas membalik differencing dalam satu rutin tunggal, PdfApplyPredictor, dan matematika geometrinya layak diketahui baik Anda memanggilnya atau mengimplementasikan ulang idenya dalam kode Delphi Anda sendiri. Lebar baris dalam byte adalah ceil(Columns × Colors × BitsPerComponent ÷ 8) dan lebar byte per-piksel yang digunakan kedua algoritma adalah ceil(Colors × BitsPerComponent ÷ 8) — salahkan salah satu pembulatan itu dan rekonstruksinya membaca melintasi batas baris alih-alih di dalam satu baris. Sebuah /Predictor di bawah 2 dibiarkan tidak tersentuh, karena 1 berarti encoder tidak menerapkan transform sama sekali; 2 memilih cabang TIFF yang ditunjukkan di bawah, dan apa pun dari 10 ke atas jatuh ke rekonstruksi row-filter PNG, di mana byte tag di awal setiap baris — bukan nilai /Predictor yang dideklarasikan — yang memutuskan bagaimana baris spesifik itu dibatalkan
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
Apa yang diubah PDFiumPas di v2.16.0
Perbaikan yang dirilis di PDFiumPas v2.16.0 berada di dalam PdfReadAndDecodeStream, rutin yang membaca byte mentah sebuah stream dan mendekodenya untuk setiap pemanggil yang perlu memeriksa struktur PDF pada tingkat byte, termasuk ekspansi object stream; ia hanya mencoba rekonstruksi setelah mengonfirmasi /Filter adalah FlateDecode telanjang, tidak pernah sebuah cascade, karena sebuah filter berantai tidak bisa dikoreksi-predictor dengan aman pada lapisan ini. Membaca kembali /Predictor, /Colors, /BitsPerComponent, dan /Columns dari dictionary stream tersebut juga tidak membutuhkan sebuah parser dictionary umum: PdfDictRefNum menemukan setiap key lewat pencarian name-token langsung di dalam rentang byte satu dictionary itu, yang aman di sini persis karena keempat key itu tidak bisa berulang atau bersarang di dalam satu dictionary stream tunggal. Pencarian name-token yang sama itu jauh lebih berisiko begitu diarahkan ke sebuah region file PDF yang lebih besar atau kurang terbatas, yang menjadi topik artikel pendamping tentang mem-parse dictionary PDF dengan aman
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
Sebelum v2.16.0, sebuah object stream yang dibangun dengan /Predictor 12 mengembang menjadi byte ter-difference tanpa error yang dimunculkan, sehingga objek katalog, /OutputIntents, atau /Metadata apa pun yang dikemas di dalamnya hilang dari pemindaian struktural PDFiumPas tanpa peringatan apa pun. Setelah perbaikan, object stream yang sama mengembang lalu merekonstruksi dengan benar, dan objek yang dikemas di dalamnya menjadi terlihat kembali oleh pemindaian tersebut. Batas defensif ikut bepergian bersama perbaikan itu: PdfApplyPredictor sekarang menolak /Colors di atas 64, /BitsPerComponent di atas 32, dan /Columns di atas 2^24 secara langsung, karena kombinasi itu mendeskripsikan geometri baris yang tidak dibutuhkan produser PDF sungguhan mana pun dan ada terutama untuk membuat sebuah decoder mengalokasikan memori jauh lebih banyak dari yang dibenarkan byte input
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
Batasan yang layak diketahui
Rekonstruksi predictor milik PDFiumPas memiliki dua tepi yang layak diketahui sebelum Anda mengandalkannya. Rekonstruksi TIFF Predictor 2 hanya mencakup kasus 8-bit-per-komponen; PDF mengizinkan pengemasan yang lebih sempit, tetapi data ter-difference-TIFF sub-byte melewati tanpa direkonstruksi alih-alih ditebak, sehingga sebuah stream yang mendeklarasikan /Predictor 2 dengan /BitsPerComponent 1, 2, atau 4 tidak akan ter-decode dengan benar lewat jalur ini hari ini. Prediksi PNG tidak memiliki batasan semacam itu — setiap baris menyediakan tag filternya sendiri, dan kelima tipe yang didefinisikan direkonstruksi terlepas dari apa pun nilai /Predictor yang dideklarasikan antara 10 dan 15, yang cocok dengan bagaimana filtering gaya-PNG sebenarnya bekerja: nilai yang dideklarasikan lebih dekat ke sebuah petunjuk tentang apa yang sebagian besar digunakan encoder daripada sebuah janji tentang setiap baris
Mesin rendering native PDFium sudah merekonstruksi data gambar dan content-stream ter-difference-predictor dengan benar, yang persis mengapa sebuah file bisa dirender sempurna di viewer biasa mana pun sementara sebuah validator, signer, atau pemeriksa versi tingkat-byte yang dibangun di atasnya membaca byte yang sama secara salah. Decoding sadar-predictor yang dijelaskan di sini mendukung fitur validasi PDF/A, pemindaian struktural, dan signing dari PDFiumPas, komponen PDFium VCL native untuk Delphi dan C++Builder