Untuk mengetahui ke mana sebenarnya ukuran file PDF pergi, losLab PDF Library menyediakan AuditDocumentSpace, yang mengklasifikasikan setiap objek tak langsung ke dua belas kategori — gambar, program font, dictionary font, content stream, form XObject, object stream, embedded file, metadata, structure tree, anotasi, page tree, lainnya — dan melaporkan jumlah objek, byte tersimpan dan persentase kontribusi tiap kategori
Situasi yang melatarbelakangi fitur ini sangat familiar. Laporan 40 halaman keluar dari generator Anda berukuran 80 MB, pelanggan bertanya kenapa, dan yang bisa Anda tawarkan hanyalah tebakan. Mungkin gambarnya. Mungkin fontnya. Jadi Anda mengaktifkan downsampling, kirim, dan filenya turun jadi 74 MB karena bobot sebenarnya ada di tempat lain sama sekali. Artikel pendamping kami tentang font subsetting dan image downsampling membahas cara memperkecil PDF; artikel ini membahas langkah yang seharusnya datang lebih dulu, yaitu mengukur apa yang hendak Anda perkecil
Kenapa harus mengukur sebelum mengompres?
Karena tiga proses optimasi standar punya hasil yang sangat berbeda pada file tertentu mana pun, dan tidak ada yang bisa memberi tahu Anda mana yang berlaku sampai Anda menghitungnya. Melakukan subsetting font pada dokumen yang fontnya sudah hanya 2% dari total byte-nya adalah menghabiskan sore hari untuk memindahkan galat pembulatan. Downsampling gambar pada file yang bulk-nya adalah content stream tak terkompresi menghasilkan kekecewaan yang sama. Optimizer bukan bagian yang sulit — setiap pustaka punya satu. Mengetahui optimizer mana yang harus diarahkan ke file ini adalah bagian sulitnya, dan itu pertanyaan akuntansi, bukan pertanyaan kompresi. Audit juga menangkap kasus-kasus di mana jawabannya bukan optimizer sama sekali: file yang ternyata 60% lampiran tertanam tidak butuh kompresi lebih baik, ia butuh percakapan soal apakah lampiran-lampiran itu memang seharusnya ada di dokumen, dan file yang 30% structure tree sedang membayar biaya penandaan aksesibilitas, yang biasanya adalah biaya yang disengaja dan tidak boleh Anda hapus diam-diam. Begitu byte-nya sudah diatribusikan, Anda membuat keputusan produk dengan angka di baliknya, bukan meraih switch mana pun yang paling dekat
Apa isi laporan dua belas kategori itu?
AuditDocumentSpace mengembalikan handle string list, bukan record, sehingga laporan tetap utuh melintasi facade DLL datar dan COM. List tersebut menampung satu baris ringkasan Total,Objects,Bytes,100.0 diikuti tepat dua belas baris Category,Objects,Bytes,Percent dalam urutan tetap yang menjadi bagian dari kontraknya: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Tiga belas baris, selalu, bahkan saat satu kategori kosong
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
Satu detail Delphi dalam loop itu akan menggigit Anda tepat sekali. GetStringListItem menggunakan indeks item berbasis satu, sesuai dengan GetStringListCount, dan indeks di luar rentang mengembalikan string kosong alih-alih raise. Tulis loop for I := 0 to Count - 1 karena kebiasaan dan Anda mendapat baris pertama kosong, baris terakhir yang diam-diam hilang, dan tidak ada exception di mana pun yang memberi tahu Anda bahwa pengindeksannya salah. Laporannya sendiri akan terlihat hampir benar, yang merupakan mode kegagalan terburuk yang bisa dimiliki alat diagnostik
Kenapa audit ini memakai panjang tersimpan alih-alih ukuran terdekode?
Karena panjang tersimpan adalah angka yang Anda inginkan sekaligus angka yang murah untuk diperoleh. Setiap objek tak langsung membawa TPDFIndObj.FLength, panjang byte mentah yang ditempati objek tersebut dalam file sebagaimana yang di-parse. Menggunakannya berarti gambar DCTDecode 900 KB dilaporkan sebagai 900 KB — byte yang benar-benar Anda bayar di disk — bukan 40 MB sampel RGB hasil dekodenya. Ini juga berarti audit tidak pernah perlu mendekode apa pun: objek yang dimuat secara lazy tetap lazy, filter tetap tidak dijalankan, dan mengaudit file 500 MB adalah satu pass melintasi header objek, bukan siklus dekompresi penuh
Aturan kedua adalah pertahanan terhadap penghitungan ganda. Ketika sebuah objek berada di dalam object stream terkompresi, ditandai dengan FObjStrNum yang bukan nol, jumlah byte-nya dicatat sebagai nol. Penyimpanannya sudah dibayar sekali oleh stream kontainer, yang oleh ISO 32000-1 §7.5.7 didefinisikan sebagai stream /Type /ObjStm yang menampung banyak objek dalam satu payload terkompresi Flate. Membebankan bagian masing-masing anggota lalu membebankan kontainernya lagi akan membengkakkan total melebihi ukuran file sebenarnya. Ini punya konsekuensi langsung pada cara Anda membaca output, dibahas di bawah dan lebih mendalam di artikel kami tentang object stream dan cross-reference stream
Kenapa program font tidak bisa mengklasifikasikan dirinya sendiri?
Karena file font TrueType yang tertanam dalam PDF tidak punya penanda yang menyatakan demikian. ISO 32000-1 §9.8.1 mendefinisikan program font tertanam sebagai nilai /FontFile, /FontFile2 atau /FontFile3 dalam font descriptor, dan stream dictionary di ujung lain referensi itu membawa /Length1 dan key filter tapi tidak ada /Type dan tidak ada /Subtype yang mengidentifikasinya sebagai font. Dilihat sendirian, ia adalah stream biner anonim. Hanya descriptor yang menunjuk padanya yang tahu apa itu. Asimetri yang sama muncul untuk anotasi: §12.5.2 membuat /Type /Annot opsional dalam dictionary anotasi, jadi sinyal yang bisa diandalkan adalah keanggotaan dalam array /Annots halaman, bukan dictionary itu sendiri
Jadi klasifikasi berjalan dua kali. Pass pertama membaca /Type dan /Subtype milik objek itu sendiri dan mengambil kemenangan yang mudah: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font dan /Type /FontDescriptor, /Metadata, /EmbeddedFile dan /Filespec, /StructTreeRoot dan /StructElem, /Annot, /Page dan /Pages. Semua yang lain mendarat sementara di Other. Pass kedua kemudian menelusuri sisi yang mereferensikan dan meng-override: setiap dictionary halaman menetapkan ulang /Contents-nya ke content stream, entri /Annots-nya ke anotasi, dan /Thumb-nya ke gambar, sementara setiap dictionary font menelusuri rantai descriptor-nya sendiri
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
Membaca laporan dan memilih langkah berikutnya
Baca dulu persentasenya, baru jumlah objeknya, dan perlakukan celah besar apa pun di antara keduanya sebagai sinyal. PDF modern menaruh sebagian besar dictionary kecilnya di dalam object stream, jadi Page tree dan Structure tree rutin menunjukkan puluhan objek dengan byte yang hampir nol — biaya sebenarnya sudah terlipat ke dalam baris Object streams. Jika Object streams sendiri besar, filenya padat dengan struktur mirip metadata, bukan konten, dan tuasnya adalah pemangkasan objek, bukan kompresi. Appearance stream anotasi berperilaku serupa: mereka membawa /Subtype /Form, jadi dokumen yang penuh stempel menunjukkan bobotnya di bawah Form XObjects sementara baris Annotations tetap kecil
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
Ada dua fakta pemformatan yang penting jika Anda mem-parsing persentasenya alih-alih hanya menampilkannya. Separator desimal selalu titik literal terlepas dari locale mesin, jadi mem-parsing dengan FormatSettings ambien di workstation Jerman atau Prancis akan gagal atau, lebih buruk, salah baca. Dan angka nol di belakang koma dipangkas, jadi kategori yang memegang persis 40% dari byte tercetak sebagai 40, bukan 40.0 — jangan pernah asumsikan jumlah desimal tetap. Dengan angka persentase di tangan, routing-nya jadi mekanis: dominasi Images mengarah ke DownsampleImages, dominasi Font programs ke SubsetEmbeddedFonts, dan Content streams yang gemuk ke CompressContent
Apa yang sengaja tidak diberitahukan audit ini
Totalnya adalah jumlah atas objek tak langsung, dan file PDF sedikit lebih dari sekadar objeknya. Header file, trailer, whitespace antar-objek dan tabel cross-reference klasik bukan objek tak langsung, jadi byte-byte itu tidak diatribusikan ke mana pun dan total audit mendarat sedikit di bawah ukuran on-disk. Cross-reference stream berbeda — ia objek sungguhan dengan /Type /XRef, jadi pada file modern byte-byte itu memang muncul, di kategori Other. Tidak ada satu pun dari perilaku ini yang merupakan cacat, tapi jika Anda mencocokkan audit terhadap jumlah byte dari sistem berkas, dari situlah celahnya berasal
Dua batasan lagi layak dinyatakan secara jelas. Pertama, angka-angka itu menggambarkan file yang dimuat, bukan yang sedang disusun: untuk objek yang dibangun di memori dan belum punya panjang tersimpan, ukurannya jatuh kembali ke output serialisasi dengan tunjangan nominal untuk stream dictionary, yang merupakan estimasi dari penulisan yang akan terjadi, bukan pengukuran. Audit setelah save-and-reload jika Anda menginginkan angka yang pasti. Kedua, baris Other yang gemuk adalah temuan, bukan laporan bug — biasanya berarti objek yatim yang sudah tidak direferensikan siapa pun lagi, dan itu pekerjaan untuk mark-and-sweep garbage collection, bukan untuk kompresi apa pun
Dipakai dengan cara ini, audit mengubah bentuk percakapannya. Alih-alih menebak-nebak laporan 80 MB, Anda membukanya, menjalankan satu pemanggilan, dan membaca bahwa gambar 8%, program font 61%, dan dokumen itu menanamkan sembilan program font lengkap untuk gaya rumah yang hanya memakai tiga typeface. Itu jawaban yang bisa diperbaiki dan punya angka yang menyertainya. AuditDocumentSpace, bersama proses optimasi yang diarahkannya, hadir dalam losLab PDF Library untuk Delphi dan C++Builder, di mana halaman referensinya mendokumentasikan daftar kategori lengkap dan API string-list di sekitarnya