Artikel Teknis

Memvalidasi PDF Terkompresi: Aliran Objek dan XRef

Anda menulis sebuah validator kecil. Ia membuka sebuah PDF, mencari ke bagian akhir, menemukan startxref, membaca offset-nya, dan mengharapkan untuk mendarat pada kata kunci xref dengan sebuah tabel referensi silang berlebar tetap di bawahnya. Dari tabel tersebut ia mengumpulkan offset objek, lalu memindai mundur mencari kata kunci trailer untuk mempelajari /Root dan /Size. Ini bekerja dengan sempurna pada setiap file yang Anda hasilkan untuk mengujinya. Kemudian sebuah file yang dihasilkan oleh versi terkini Word, atau oleh sebuah pustaka yang menargetkan PDF 1.5, tiba, dan validator tersebut menyatakannya rusak. Tidak ada kata kunci xref di tempat offset tersebut menunjuk, tidak ada dictionary trailer di mana pun, dan tabel objek yang dibangun validator tersebut hampir kosong. File tersebut valid. Validator tersebut membacanya melalui kacamata berumur lima belas tahun

Inilah alasan tunggal paling umum mengapa sebuah pemeriksaan PDF tingkat byte yang ditulis berdasarkan tata letak klasik gagal pada dokumen modern. Struktur yang menjadi sandarannya, tabel referensi silang plaintext dan kata kunci trailer, dijadikan opsional di PDF 1.5 dan sering kali tidak ada. Dua fitur menggantikannya: cross-reference stream dan compressed object stream. Keduanya dijelaskan dalam ISO 32000-1, dan sebuah validator yang tidak mengetahui keduanya melihat sebuah file yang sehat sebagai tumpukan objek yang hilang

Apa yang diubah PDF 1.5 pada bagian akhir file

ISO 32000-1 §7.5.8 mendefinisikan cross-reference stream, dan §7.5.7 mendefinisikan object stream bertipe /ObjStm. Bersama-sama keduanya memungkinkan sebuah writer melepaskan dua struktur yang menjadi kunci bagi sebuah parser klasik. Sebuah file PDF 1.5 mungkin berakhir tanpa tabel xref sama sekali. Sebagai gantinya, objek yang ditunjuk oleh startxref adalah sebuah objek stream biasa yang dictionary-nya membawa /Type /XRef, dan stream tersebut menyimpan data referensi silang dalam bentuk biner yang kompak. Tidak ada kata kunci trailer juga, karena trailer tersebut sekarang adalah dictionary milik stream itu sendiri. Kunci-kunci yang diburu oleh parser klasik, /Root, /Size dan /ID, hidup di dalam dictionary tersebut

Perubahan kedua memindahkan objek-objek itu sendiri. Alih-alih menulis setiap objek tidak langsung pada offset byte-nya sendiri, sebuah writer dapat mengemas banyak objek kecil, dictionary halaman, dictionary anotasi, structure tree, ke dalam satu object stream tunggal dan mengompresi seluruh wadah tersebut dengan Flate. Objek-objek individual tersebut tidak lagi memiliki offset byte dalam file. Mereka memiliki sebuah posisi di dalam sebuah blob terkompresi. Sebuah validator yang memindai byte mentah mencari 1 0 obj tidak akan pernah menemukannya, karena teks tersebut hanya ada setelah inflasi (dekompresi). Bagi sebuah parser klasik, setengah dokumen tersebut begitu saja lenyap

Diagram yang membandingkan ekor berkas PDF klasik dengan tabel xref plaintext dan kata kunci trailer terhadap cross-reference stream PDF 1.5 yang kamusnya mempertahankan /Root, /Size, dan /ID dalam plaintext untuk validator Delphi
Ekor file klasik membawa tabel xref plaintext dan keyword trailer, sementara ekor PDF 1.5 menggantikan keduanya dengan objek cross-reference stream yang dictionary-nya tetap memaparkan /Root, /Size, dan /ID dengan jelas

Kunci trailer bersifat plaintext, bahkan dalam file terkompresi

Bagian yang melegakan adalah membaca trailer dari sebuah cross-reference stream tidak membutuhkan inflasi apa pun. Sebuah objek stream ditulis sebagai sebuah dictionary yang diikuti oleh kata kunci stream dan kemudian byte-byte terkompresi. Dictionary tersebut bersifat plaintext. Jadi ketika startxref menunjuk ke sebuah cross-reference stream, byte-byte tepat setelah nomor objek terlihat seperti sebuah dictionary biasa, dan /Root, /Size dan /ID duduk di sana dalam keadaan jelas, sebelum kata kunci stream dan data Flate dimulai

Itu berarti sebuah validator dapat mempelajari tiga fakta yang paling dibutuhkannya, di mana katalog tersebut berada, berapa banyak objek yang diklaim file tersebut, dan identifier file tersebut, hanya dengan mengurai dictionary stream tersebut. Ia tidak perlu mendekompresi data referensi silang tersebut, dan ia tidak perlu menafsirkan entri biner di dalamnya. Pekerjaan yang mengalahkan sebuah parser naif bukanlah membaca trailer; itu adalah menemukan objek-objek tersebut. Keduanya adalah dua masalah yang dapat dipisahkan, dan menyelesaikan yang pertama itu murah

Object stream: sebuah header, lalu sebuah blob Flate

Sebuah object stream adalah sebuah wadah. Dictionary-nya membawa /Type /ObjStm, sebuah entri /N yang memberikan jumlah objek yang dikemas di dalamnya, dan sebuah entri /First yang memberikan offset byte, di dalam data yang telah diinflasi, tempat isi objek pertama dimulai. Payload terkompresi tersebut, begitu diinflasi, dimulai dengan sebuah header kecil berisi /N pasangan integer. Setiap pasangan adalah sebuah nomor objek dan offset isi objek tersebut relatif terhadap /First. Setelah header tersebut datang isi objek itu sendiri, digabungkan menjadi satu

Mengekspansi salah satunya bersifat mekanis begitu byte-byte tersebut diinflasi. Anda membaca dictionary tersebut untuk mendapatkan /N dan /First, menginflasi stream tersebut dengan sebuah decoder Flate, menyusuri pasangan /N di depan untuk mempelajari nomor objek mana yang berada pada offset mana, dan kemudian mengangkat setiap isi keluar seolah-olah itu adalah objek tidak langsung biasa. Satu-satunya dependensi sungguhan adalah decoder Flate tersebut, dan Anda sudah memilikinya: Delphi mengirimkan System.ZLib, dan Free Pascal mengirimkan unit zstream, keduanya membungkus zlib dan menginflasi sebuah raw Flate stream tanpa kode pihak ketiga apa pun. Sebuah rutin yang menambahkan setiap objek yang diekstraksi ke tabel objek validator tersebut membuat sisa validator itu, bagian yang menyusuri /Root dan memeriksa page tree, berperilaku persis seperti pada file klasik

Apa yang tidak perlu Anda implementasikan

Sangat mudah untuk melebih-lebihkan pekerjaan tersebut. Membaca kunci-kunci trailer dari sebuah file terkompresi tidak membutuhkan pendekodean entri biner cross-reference stream tersebut. Cross-reference stream §7.5.8 menggunakan tiga tipe entri, dan entri tipe 2, yaitu yang mengatakan objek ini hidup di dalam object stream N pada indeks i, adalah yang akan Anda dekode untuk membangun sebuah peta offset lengkap. Anda membutuhkan peta tersebut untuk menyelesaikan objek sembarang berdasarkan nomornya. Anda tidak membutuhkannya untuk membaca /Root, /Size dan /ID, yang berada dalam dictionary plaintext, dan Anda tidak membutuhkannya untuk mengekspansi object stream, karena setiap /ObjStm mengumumkan isinya sendiri melalui /N dan /First

Anda juga tidak perlu menangani fungsi predictor PNG dan TIFF yang mungkin diterapkan sebuah cross-reference stream melalui /DecodeParms-nya hanya untuk mendapatkan kunci-kunci trailer. Predictor tersebut menyaring baris-baris referensi silang biner agar terkompresi lebih baik; mereka tidak ada hubungannya dengan dictionary yang mendahului stream tersebut. Peningkatan minimal yang membuat sebuah validator klasik menyadari PDF modern karena itu kecil: ketika startxref mendarat pada sebuah stream alih-alih kata kunci xref, urai dictionary stream tersebut untuk mencari kunci-kunci trailer, dan ekspansi setiap objek /ObjStm yang Anda temui sehingga isinya masuk ke tabel objek. Mendekode entri tipe 2 dan predictor adalah sebuah tugas terpisah yang lebih besar yang dapat Anda tunda sampai Anda benar-benar membutuhkan resolusi objek acak

Mengapa sebuah pemeriksaan kepatuhan harus mengekspansi stream terlebih dahulu

Ini berhenti menjadi hal akademis pada saat Anda menjalankan sebuah pemeriksaan profil. Sebuah validator PDF/A atau PDF/X memeriksa objek-objek tertentu: katalog dokumen untuk sebuah array /OutputIntents, stream /Metadata untuk sebuah paket XMP dengan identifier yang tepat, setiap font descriptor untuk sebuah file font yang tertanam, trailer untuk sebuah /ID. Dalam sebuah file terkompresi, sebagian besar objek-objek tersebut berada di dalam object stream. Sebuah validator yang belum mengekspansi object stream tersebut tidak dapat melihat kunci-kunci katalog tersebut, tidak dapat menemukan metadata, dan tidak dapat mendaftar font-font tersebut. Ia akan melaporkan sebuah dokumen yang sepenuhnya patuh sebagai kehilangan output intent-nya, kehilangan XMP-nya, dan kehilangan setengah strukturnya, karena bukti yang dibutuhkannya masih duduk di dalam sebuah blob Flate yang tidak pernah diinflasinya

Urutan itu penting. Ekspansi harus terjadi sebelum pemeriksaan berjalan, bukan bersamaan dengannya, karena setiap pemeriksaan mengasumsikan ia dapat mencapai sebuah objek berdasarkan nomornya. Jika Anda menghubungkan sebuah pemeriksaan profil langsung ke sebuah pemindaian byte mentah, ia mewarisi kebutaan parser klasik tersebut dan menghasilkan pelanggaran palsu tepat pada file-file modern yang paling mungkin terbentuk dengan baik, karena mereka berasal dari toolchain yang cukup baru untuk menulis cross-reference stream sejak awal

Membiarkan PDFium melakukan penguraian untuk Anda

PDFium Component mengurai cross-reference stream dan object stream sebagai bagian dari pemuatan sebuah dokumen, yang merupakan cara praktis untuk menghindari membuat sendiri langkah inflate-dan-ekspansi tersebut. Ketika Anda memuat sebuah file dengan komponen TPdf, objek-objek yang dikemas ke dalam wadah /ObjStm sudah diselesaikan, dan titik-titik masuk validasi melihat dokumen yang telah terekspansi sepenuhnya. ValidatePdfA mengembalikan sebuah record TPdfAValidationResult yang field Conformance-nya adalah sebuah nilai TPdfAConformance seperti pac1b atau pacNone, yang field Issues-nya adalah sebuah set masalah tertentu yang ditemukan, dan yang metode IsCompliant-nya benar hanya ketika sebuah tingkat kesesuaian terdeteksi dan set masalah tersebut kosong. Karena objek-objek tersebut telah diekspansi selama pemuatan, sebuah array /OutputIntents atau sebuah font tertanam yang berada di dalam sebuah object stream ditemukan, tidak dilaporkan hilang

Diagram stream objek ObjStm dalam PDF terkompresi: entri kamus /N dan /First, langkah inflate Flate via System.ZLib di Delphi, dan payload terinflasi berisi pasangan header plus badan objek yang dirangkai
Meng-inflate ObjStm dengan System.ZLib mengungkap header berisi N pasangan nomor-objek dan offset diikuti body-body yang dirangkai, yang langsung jatuh ke tabel objek milik validator
uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // mengurai xref/object stream saat pemuatan
    Result := Pdf.ValidatePdfA;    // melihat tabel objek yang telah terekspansi
  finally
    Pdf.Free;
  end;
end;

Hal yang sama berlaku untuk ValidatePdfX, yang mengembalikan sebuah TPdfXValidationResult dengan bentuk yang sama. Inti dari merutekan melalui PDFium adalah bahwa dekompresi struktural yang dijelaskan di atas terjadi satu kali, dengan benar, di dalam loader tersebut, sehingga kode validasi Anda tidak pernah melihat perbedaan antara sebuah file klasik dan sebuah file yang sepenuhnya terkompresi. Keduanya tiba di validator sebagai sebuah set objek yang telah diselesaikan

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues adalah sebuah set: hitung anggotanya
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Jika byte-byte tersebut sudah berada di memori alih-alih di disk, urutan muat-lalu-validasi yang sama bekerja melalui overload LoadDocument(const Data: TBytes), yang menerima konten file mentah dan mengurai cross-reference dan object stream-nya dengan cara yang sama seperti jalur file. Pelajaran yang dapat diambil untuk sebuah validator buatan tangan adalah aturan strukturalnya, bukan API-nya: baca kunci-kunci trailer dari dictionary stream tersebut dalam bentuk plaintext, ekspansi setiap /ObjStm dengan sebuah decoder Flate sebelum Anda menyusuri dokumen tersebut, dan perlakukan pendekodean entri biner cross-reference tersebut sebagai tugas opsional yang lebih besar sebagaimana adanya

Begitu struktur tersebut terekspansi, sebuah validator dapat menjalankan sisa sebuah alur kerja di atasnya. Untuk sebuah harness preflight command-line yang melaporkan kesesuaian di seluruh sebuah folder input, lihat panduan kami tentang membangun sebuah CLI laporan preflight batch. Ketika validasi adalah sebuah gerbang sebelum memecah sebuah dokumen besar, teknik-teknik dalam panduan kami untuk memecah dokumen PDF menjadi beberapa file berpasangan secara alami dengan pola muat-dan-periksa yang ditunjukkan di sini. Keduanya dibangun di atas permukaan pemuatan dan validasi PDFium Component untuk Delphi dan C++Builder

Diagram yang menunjukkan validator PDF/A dan PDF/X Delphi membentangkan setiap ObjStm sebelum pemeriksaan profil berjalan, sehingga output intent katalog, metadata XMP, dan font tersemat ditemukan alih-alih keliru dilaporkan hilang
Ekspansi harus berjalan sebelum pemeriksaan profil, karena setiap pemeriksaan mengasumsikan ia bisa menjangkau objek berdasar nomor; melewatinya menghasilkan pelanggaran palsu pada file terkompresi yang sehat