Artikel Teknis

Mengapa Sebagian Object Stream PDF Ter-decode Menjadi Sampah di Delphi

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

Di Delphi, stream objek PDF yang hanya menjalankan FlateDecode masih memegang byte terdiferensi-prediktor PNG atau TIFF sampai langkah pembalikan Predictor menghasilkan data objek mentah
FlateDecode hanya meng-inflate; membalik /Predictor yang dideklarasikan di /DecodeParms itulah yang mengubah byte hasil differencing kembali menjadi objek yang dapat dibaca parser

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 = tanpa prediksi, tidak ada yang dibatalkan
  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;                                   // tolak geometri row yang berbahaya
  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;                                 // hanya layout 8-bit yang direkonstruksi
    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 diteruskan ke rekonstruksi row-filter PNG di bawah
end;
// Berlanjut di dalam PdfApplyPredictor setelah Predictor>= 10 (filter row PNG)
// Rows:= Length(Src) div (RowLen+ 1); setiap row adalah tag filter 1 byte
// diikuti byte data RowLen, didekode dari kiri ke kanan
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 di sebelah kiri
    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) menambahkan yang mana pun dari A, B, atau byte di kiri-atas yang berada
    // paling dekat dengan predictor linear A+ B- C; tag 0 (None) menyalin
    // byte terfilter tanpa perubahan
    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

Wadah ObjStm PDF 1.5 mengemas objek katalog, OutputIntents, dan Metadata yang tetap tak terlihat oleh pemindaian PDFiumPas di Delphi ketika baris prediktor tak pernah direkonstruksi
ObjStm yang sama bisa menyerahkan objek terkemasnya atau menyembunyikannya secara senyap dari pemindaian struktural, bergantung pada apakah baris predictor direkonstruksi
// Di dalam PdfReadAndDecodeStream, tepat setelah PdfInflate() selesai dijalankan
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

Baris prediktor PNG dalam stream objek PDF masing-masing membawa byte tag filter, dan rekonstruksi Delphi atas Sub, Up, Average, dan Paeth mencampur byte tetangga A, B, dan C per baris
Setiap baris PNG membawa filter tag miliknya sendiri, dan rekonstruksi mencampur byte kiri, atas, dan kiri-atas persis seperti yang diwajibkan tag itu