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

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