Anda menulis validator kecil. Ia membuka PDF, mencari hingga akhir, menemukan startxref, membaca offset, dan mengharapkan mendarat pada kata kunci xref dengan tabel referensi silang (cross-reference table) lebar-tetap di bawahnya. Dari tabel tersebut ia mengumpulkan offset objek, lalu memindai ke belakang untuk kata kunci trailer untuk mengetahui /Root dan /Size. Ini berfungsi dengan sempurna pada setiap berkas yang Anda hasilkan untuk mengujinya. Kemudian berkas yang dihasilkan oleh versi Word saat ini, atau oleh pustaka yang menargetkan PDF 1.5, tiba, dan validator menyatakannya rusak. Tidak ada kata kunci xref di mana offset menunjuk, tidak ada kamus trailer di mana pun, dan tabel objek yang dibangun validator hampir kosong. Berkas tersebut sah. Validator membacanya melalui lensa berusia lima belas tahun.
Ini adalah satu-satunya alasan paling umum mengapa pemeriksaan PDF tingkat-byte yang ditulis terhadap tata letak klasik gagal pada dokumen modern. Struktur yang menjadi dependensinya, tabel referensi silang teks polos dan kata kunci trailer, dijadikan opsional dalam PDF 1.5 dan sering kali tidak ada. Dua fitur menggantikannya: aliran referensi silang (cross-reference stream) dan aliran objek terkompresi (compressed object stream). Keduanya dijelaskan dalam ISO 32000-1, dan validator yang tidak mengetahuinya melihat berkas yang sehat sebagai tumpukan objek yang hilang.
Apa yang diubah PDF 1.5 tentang ekor berkas
ISO 32000-1 §7.5.8 mendefinisikan aliran referensi silang (cross-reference stream), dan §7.5.7 mendefinisikan aliran objek dari tipe /ObjStm. Bersama-sama mereka membiarkan penulis menjatuhkan dua struktur yang menjadi kunci bagi parser klasik. Berkas PDF 1.5 mungkin berakhir tanpa tabel xref sama sekali. Di tempatnya, objek yang ditunjuk startxref adalah objek aliran biasa yang kamusnya membawa /Type /XRef, dan aliran tersebut menyimpan data referensi silang dalam bentuk biner yang ringkas. Tidak ada kata kunci trailer juga, karena trailer sekarang adalah kamus aliran itu sendiri. Kunci yang diburu parser klasik, /Root, /Size, dan /ID, hidup di dalam kamus tersebut.
Perubahan kedua memindahkan objek itu sendiri. Alih-alih menulis setiap objek tidak langsung (indirect object) pada offset byte-nya sendiri, penulis dapat mengemas banyak objek kecil, kamus halaman, kamus anotasi, pohon struktur, ke dalam satu aliran objek (object stream) tunggal dan mengompresi seluruh wadah dengan Flate. Objek individual tidak lagi memiliki offset byte di dalam berkas. Mereka memiliki posisi di dalam blob terkompresi. Validator yang memindai byte mentah untuk mencari 1 0 obj tidak pernah menemukan mereka, karena teks tersebut hanya ada setelah inflasi (dekompresi). Bagi parser klasik, setengah dokumen telah hilang begitu saja.
Kunci trailer adalah teks polos, bahkan dalam berkas terkompresi
Bagian yang menenangkan adalah bahwa membaca trailer dari aliran referensi silang tidak memerlukan dekompresi apa pun. Objek aliran ditulis sebagai kamus diikuti oleh kata kunci stream dan kemudian byte terkompresi. Kamus tersebut berupa teks polos (plaintext). Jadi ketika startxref menunjuk ke aliran referensi silang, byte segera setelah nomor objek terlihat seperti kamus biasa, dan /Root, /Size, dan /ID duduk di sana dengan jelas, sebelum kata kunci stream dan data Flate dimulai.
Itu berarti validator dapat mempelajari tiga fakta yang paling dibutuhkannya, di mana katalog berada, berapa banyak objek yang diklaim berkas, dan pengidentifikasi berkas, hanya dengan menguraikan kamus aliran. Ia tidak harus mendekode data referensi silang, dan tidak harus menerjemahkan entri biner di dalamnya. Pekerjaan yang mengalahkan parser naif bukanlah membaca trailer; melainkan menemukan objek. Itu adalah dua masalah yang dapat dipisahkan, dan menyelesaikan yang pertama murah.
Aliran objek: header, lalu blob Flate
Aliran objek adalah wadah. Kamusnya membawa /Type /ObjStm, entri /N yang memberikan jumlah objek yang dikemas di dalamnya, dan entri /First yang memberikan offset byte, di dalam data yang didekompresi, tempat tubuh objek pertama dimulai. Payload terkompresi, setelah didekompresi, dimulai dengan header kecil pasangan integer /N. Setiap pasangan adalah nomor objek dan offset dari tubuh objek tersebut relatif terhadap /First. Setelah header datang tubuh objek itu sendiri, digabungkan.
Memperluas satu aliran adalah mekanis setelah byte didekompresi. Anda membaca kamus untuk mendapatkan /N and /First, mendekompresi aliran dengan dekoder Flate, menelusuri pasangan utama /N untuk mengetahui nomor objek mana yang hidup di offset mana, lalu mengangkat setiap tubuh keluar seolah-olah itu adalah objek tidak langsung biasa. Satu-satunya dependensi nyata adalah dekoder Flate, dan Anda sudah memilikinya: Delphi menyertakan System.ZLib, dan Free Pascal menyertakan unit zstream, yang keduanya membungkus zlib dan mendekompresi aliran Flate mentah tanpa kode pihak ketiga. Rutinitas yang menambahkan setiap objek yang diekstraksi ke tabel objek validator membuat validator lainnya, bagian yang berjalan di /Root dan memeriksa pohon halaman, berperilaku persis seperti pada berkas klasik.
Apa yang tidak harus Anda terapkan
Sangat mudah untuk melebih-lebihkan pekerjaan tersebut. Membaca kunci trailer dari berkas terkompresi tidak memerlukan dekode entri biner dari aliran referensi silang. Aliran referensi silang §7.5.8 menggunakan tiga tipe entri, dan entri tipe 2, yang berbunyi objek ini hidup di dalam aliran objek N pada indeks i
, adalah apa yang akan Anda dekode untuk membangun peta offset penuh. Anda memerlukan peta tersebut untuk menyelesaikan objek arbitrer berdasarkan nomor. Anda tidak memerlukannya untuk membaca /Root, /Size, dan /ID, yang berada dalam kamus teks polos, dan Anda tidak memerlukannya untuk memperluas aliran objek, karena setiap /ObjStm mengumumkan isinya sendiri melalui /N dan /First.
Anda juga tidak perlu menangani fungsi prediktor PNG dan TIFF yang mungkin diterapkan oleh aliran referensi silang melalui /DecodeParms-nya hanya untuk mendapatkan kunci trailer. Prediktor menyaring baris referensi silang biner untuk membuatnya terkompresi lebih baik; mereka tidak ada hubungannya dengan kamus yang mendahului aliran. Oleh karena itu, peningkatan minimal yang membuat validator klasik sadar akan PDF-modern sangatlah kecil: ketika startxref mendarat pada aliran alih-alih kata kunci xref, uraikan kamus aliran untuk mendapatkan kunci trailer, dan perluas objek /ObjStm apa pun yang Anda temui sehingga isinya masuk ke dalam tabel objek. Mendekode entri tipe 2 dan prediktor adalah tugas terpisah yang lebih besar yang dapat Anda tunda sampai Anda benar-benar membutuhkan resolusi objek acak.
Mengapa pemeriksaan kepatuhan harus memperluas aliran terlebih dahulu
Ini berhenti menjadi akademis saat Anda menjalankan pemeriksaan profil (profile check). Validator PDF/A atau PDF/X memeriksa objek tertentu: katalog dokumen untuk array /OutputIntents, aliran /Metadata untuk paket XMP dengan pengidentifikasi yang benar, setiap deskriptor font untuk berkas font yang disematkan, trailer untuk /ID. Dalam berkas terkompresi, sebagian besar objek tersebut berada di dalam aliran objek. Validator yang belum memperluas aliran objek tidak dapat melihat kunci katalog, tidak dapat menemukan metadata, dan tidak dapat menghitung font. Ia akan melaporkan dokumen yang sepenuhnya sesuai sebagai kehilangan maksud keluaran (output intent) miliknya, kehilangan XMP miliknya, dan kehilangan setengah strukturnya, karena bukti yang dibutuhkannya masih berada dalam blob Flate yang tidak pernah didekompresi.
Urutan sangat penting. Ekspansi harus terjadi sebelum pemeriksaan berjalan, bukan bersamanya, karena setiap pemeriksaan mengasumsikan ia dapat menjangkau objek berdasarkan nomor. Jika Anda menghubungkan pemeriksaan profil secara langsung ke pemindaian byte mentah, ia mewarisi kebutaan parser klasik dan menghasilkan pelanggaran palsu pada berkas modern yang paling mungkin terbentuk dengan baik, karena mereka keluar dari rantai alat (toolchains) yang cukup baru untuk menulis aliran referensi silang sejak awal.
Membiarkan PDFium melakukan penguraian untuk Anda
Komponen PDFium mengurai aliran referensi silang dan aliran objek sebagai bagian dari memuat dokumen, yang merupakan cara praktis untuk menghindari pembuatan langkah inflate-and-expand secara manual. Ketika Anda memuat berkas dengan komponen TPdf, objek yang dikemas ke dalam wadah /ObjStm sudah diselesaikan, dan titik masuk validasi melihat dokumen yang sepenuhnya diperluas. ValidatePdfA mengembalikan rekaman TPdfAValidationResult yang bidang Conformance-nya adalah nilai TPdfAConformance seperti pac1b atau pacNone, yang bidang Issues-nya adalah kumpulan masalah spesifik yang ditemukan, dan metode IsCompliant-nya bernilai true hanya ketika tingkat kepatuhan terdeteksi dan kumpulan masalah kosong. Karena objek diperluas selama pemuatan, array /OutputIntents atau font tersemat yang hidup di dalam aliran objek ditemukan, tidak dilaporkan hilang.
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
var
Pdf: TPdf;
R : TPdfXValidationResult;
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: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Setelah struktur diperluas, validator dapat menjalankan sisa alur kerja di atasnya. Untuk harness preflight baris perintah (command-line preflight harness) yang melaporkan kepatuhan di seluruh folder masukan, lihat panduan kami tentang membangun laporan preflight batch CLI. Ketika validasi adalah gerbang sebelum memecah dokumen besar, teknik dalam panduan kami tentang membagi dokumen PDF menjadi beberapa berkas berpasangan secara alami dengan pola load-and-check yang ditunjukkan di sini. Keduanya dibangun di atas permukaan pemuatan dan validasi dari PDFium Component untuk Delphi dan C++Builder.