Artikel Teknis

Memuat PDF Referensi Hibrida Dari Word dan Excel di Delphi

Buka PDF yang dihasilkan Microsoft Word atau Excel, telusuri halamannya, dan tak ada yang terlihat janggal. Muat ke dalam program Delphi, baca kembali jumlah halamannya, dan angkanya benar. Lalu simpan ulang dengan enkripsi diaktifkan dan pekerjaan gagal dengan EListError, atau keluarannya terbuka dengan peringatan cross-reference rusak. File itu tidak pernah korup. Ini file referensi hibrida, dan struktur yang memungkinkan viewer berusia lima belas tahun membukanya justru itulah struktur yang mengalahkan loader yang berhenti membaca terlalu dini

Ini salah satu cara paling umum sebuah pipeline PDF yang lolos semua uji internal bertemu file yang tak bisa ia round-trip. Semua input dihasilkan sendiri di dalam organisasi, jadi tak pernah ada yang hibrida. File hibrida pertama tiba di hari seorang pelanggan meneruskan faktur yang diekspor dari spreadsheet

Apa yang sebenarnya ditulis Word dan Excel

ISO 32000-1 mendeskripsikan tata letak referensi hibrida pada §7.5.8.4. Aplikasi yang menginginkan fitur PDF 1.5 seperti object stream namun tetap ingin pembaca PDF 1.4 bisa membuka filenya menulis informasi cross-reference dua kali. Ada tabel cross-reference klasik, baris ASCII berlebar tetap yang mengakhiri setiap PDF hingga versi 1.4, dan ada cross-reference stream yang mengindeks sisanya. Trailer bagian klasik membawa entri /XRefStm yang nilainya adalah offset byte stream tersebut

Pembagian kerjanya disengaja. Objek yang harus bisa dijangkau pembaca lama, termasuk katalog dan page tree, dapat dialamatkan dari tabel klasik. Objek yang dilipat ke dalam object stream terkompresi ditandai bebas di tabel klasik dengan entri bertipe f, sehingga pembaca 1.4 melompatinya begitu saja dan tak pernah tersandung struktur yang tak bisa diurainya. Lokasi asli mereka hanya tercatat di cross-reference stream. Ciri khas file semacam ini ada di ekornya: bagian klasik pendek, sering tak lebih dari xref diikuti header subbagian 0 0, yang trailer-nya menunjuk /XRefStm tempat data pemulihan sesungguhnya berada

Diagram HotPDF tentang ekor PDF referensi-hibrida Word atau Excel di mana trailer xref klasik membawa /XRefStm 87325, menunjuk kembali ke cross-reference stream yang mengindeks bidang formulir dan struktur bertag yang tak terlihat oleh loader yang berhenti di tabel klasik
Ekor hybrid menyimpan tabel klasik token yang satu-satunya muatannya adalah offset /XRefStm sementara sisi stream memegang indeks objek yang sesungguhnya

Mengapa jumlah halaman yang benar tak membuktikan apa-apa

Karena katalog dan page tree memang sengaja dapat dijangkau dari tabel klasik, loader yang hanya membaca tabel itu menemukan /Root, menelusuri page tree, dan melaporkan jumlah halaman yang benar. Semua yang dibutuhkan pembaca lama tersedia, sehingga file tampak sehat. Objek yang raib adalah yang dikemas ke dalam object stream: dictionary bidang AcroForm, elemen struktur PDF bertag, dan barisan panjang dictionary kecil yang memang tak perlu terlihat oleh viewer lawas

Anda tak menyadari celah itu sampai ada sesuatu menyentuh objek tersebut, dan penyimpanan ulang penuh menyentuh semuanya. Menelusuri dokumen untuk mengenkripsi ulang atau menulis ulang justru merupakan operasi yang meminta setiap nomor objek bergiliran, itulah sebabnya gejalanya muncul saat penyimpanan alih-alih saat pemuatan, jauh dari penyebabnya

Jebakannya: detektor yang melihat xref lalu berhenti

Cara murah memutuskan bagaimana sebuah file diindeks adalah mengikuti startxref dan memeriksa byte pertama yang ditunjuknya. Kata kunci xref berarti tabel klasik; objek stream berarti cross-reference stream. Uji itu benar untuk file mana pun yang berpegang pada satu skema. Ia keliru untuk file hibrida, yang startxref-nya mengarah ke bagian klasik semata-mata untuk memuaskan pembaca lama, sementara /XRefStm di trailer bagian itulah tempat sebagian besar dokumen sebenarnya diindeks. Detektor yang mengembalikan "klasik" pada xref pertama yang ditemuinya tak pernah membaca /XRefStm, dan setiap objek yang hanya hidup di stream menjadi tak terlihat

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // jumlahnya benar
    // periksa atau edit dokumen yang dimuat di sini
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // menelusuri setiap objek
  finally
    Pdf.Free;
  end;
end;

Dengan detektor yang keluar dini itu terpasang, pemuatan tampak baik-baik saja dan penyimpanan ulanglah tempat objek yang absen menunjukkan diri. Perbaikannya bukan membaca lebih banyak byte di awal, melainkan mengenali trailer hibrida dan mengikuti /XRefStm sebelum menyimpulkan file selesai dibaca

Urutan penggabungan tak bisa ditawar

Setelah kedua indeks dibaca, keduanya hanya bisa digabungkan dalam satu arah. Cross-reference stream harus digabungkan lebih dulu, dengan entri klasik diisikan mengelilinginya. Alasannya adalah tipu kecil di jantung format ini. File hibrida menandai objek terkompresinya sebagai bebas di tabel klasik agar pembaca lama mengabaikannya. Loader yang menghormati kebijakan first-seen-wins dan membaca tabel klasik lebih dulu akan mencatat nomor objek itu sebagai bebas, lalu membuang entri stream yang sebenarnya menemukan lokasinya, karena slotnya sudah terisi. Balik urutannya, dan entri tipe 2 dari stream, masing-masing berupa nomor object stream ditambah sebuah indeks, memenangkan slot yang memang seharusnya mereka miliki, dengan entri klasik mengisi sisa di sekitarnya

Disiplin yang sama mencegah revisi lebih lama menghidupkan kembali objek yang dihapus. Incremental update membentuk rantai mundur melalui /Prev, dan entri bebas bertipe 0 adalah penanda bahwa bagian yang lebih baru telah memensiunkan sebuah nomor objek. Bagian yang lebih tua di rantai tak boleh dibiarkan menimpa penanda itu dengan lokasi basi. Perlakukan first-seen sebagai otoritatif untuk penanda bebas, dan objek yang dihapus tetap terhapus; perlakukan sembarangan, dan sejarah file sendiri menghidupkan kembali konten yang dibuang oleh revisi terbaru

Diagram HotPDF yang mengontraskan dua urutan penggabungan untuk data xref hibrida: membaca tabel klasik lebih dulu menandai objek 12 bebas dan membuang entri tipe 2-nya yang datang belakangan sehingga penyimpanan ulang gagal dengan EListError, sementara menggabungkan cross-reference stream lebih dulu membiarkan setiap entri mengklaim slotnya dan tabel klasik menyesuaikan diri di sekitarnya
Membaca stream lebih dulu membiarkan entri tipe 2 mengklaim slotnya sehingga tabel klasik menetap di sekeliling mereka alih-alih menghapus mereka

Apa artinya semua ini di HotPDF

Mesin ini menuntaskan file referensi hibrida untuk Anda, pada setiap jalur yang harus mengurai data cross-reference. Muat dokumen dengan LoadFromFile atau LoadFromStream, lakukan perubahan Anda, lalu panggil SaveLoadedDocument; atau jalankan operasi sekali jalan seperti EncryptFile yang membaca input dan menulis output. Apa pun jalurnya, pemulihan ini membaca /XRefStm, menggabungkan bagian stream di depan entri klasik, dan menuntaskan objek yang hidup di stream sebelum penulisan mendaftarkan semuanya. Jalur enkripsi AES-256 adalah tempat masalah ini pertama kali tampak, karena mengenkripsi dokumen berarti menulis ulang setiap objek sehingga menuntut setiap objek sudah ditemukan lokasinya

// Sekali jalan: baca input hibrida, tulis salinan terenkripsi AES-256
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

Detail yang patut dibawa pulang berada di hulu API. File yang datang dari Word, Excel, PowerPoint, dan daftar panjang pipeline "Save as PDF" umumnya hibrida, jadi loader yang hanya Anda uji terhadap keluaran generator sendiri mungkin tak pernah bertemu satu pun selama pengujian. Isi fixture Anda dengan dokumen yang diekspor dari aplikasi Office sungguhan, bukan hanya file buatan kode Anda sendiri

Memeriksa file yang Anda curigai

Dua pemeriksaan menuntaskan pertanyaan itu dengan cepat. Buka file di tampilan hex dan baca byte setelah startxref terakhir; file hibrida menampilkan bagian klasik pendek yang dictionary trailer-nya memuat /XRefStm. Atau bandingkan jumlah objek yang dilaporkan parse penuh dengan nomor objek tertinggi yang dideklarasikan /Size di trailer. Celah lebar berarti ada objek bersembunyi di stream yang belum dibuka loader, kekurangan yang sama yang belakangan berubah menjadi kegagalan saat penyimpanan

Ekor ekspor Excel khas menjadikan pemeriksaan pertama itu konkret. Semua setelah kata kunci xref terakhir adalah ASCII polos, sehingga ciri khasnya langsung terbaca dari tampilan hex (offset ilustratif, anotasi ditambahkan)

xref
0 0                          % subbagian klasik kosong: tidak ada baris sama sekali
trailer
<< /Size 216                 % satu di atas nomor object tertinggi yang digunakan
   /Root 1 0 R
   /Info 15 0 R
   /ID [<5C9A...> <5C9A...>]
   /XRefStm 87325            % offset byte stream cross-reference
>>
startxref
88710                        % menunjuk ke bagian klasik di atas
%%EOF

Subbagian 0 0 adalah petunjuknya: tabel klasik dengan nol entri hanya ada untuk membawa trailer, dan trailer itu terutama ada untuk mengatakan /XRefStm 87325. Detektor yang berhenti di kata kunci xref, pada titik ini, hanya melihat indeks kosong. Bila Anda lebih memilih membuat skrip daripada melihatnya langsung, penanda itu selalu berada dalam beberapa kilobyte terakhir file, sehingga pembacaan mundur yang dibatasi saja sudah cukup

// Mengembalikan offset /XRefStm dari ekor file, atau -1 jika
// penandanya absen (file tidak hibrida, atau bukan PDF sama sekali)
function FindXRefStm(const FileName: string): Int64;
var
  FS: TFileStream;
  Tail: AnsiString;
  Len, P: Integer;
begin
  Result := -1;
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Len := 2048;                        // trailer berada di ekor file
    if FS.Size < Len then
      Len := Integer(FS.Size);
    FS.Position := FS.Size - Len;       // pembacaan mundur terbatas: maks 2 KB
    SetLength(Tail, Len);
    FS.ReadBuffer(Tail[1], Len);
  finally
    FS.Free;
  end;
  P := Pos(AnsiString('/XRefStm'), Tail);
  if P = 0 then
    Exit;                               // tidak ada penanda hibrida di ekor file
  Inc(P, Length('/XRefStm'));
  while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
    Inc(P);                             // lewati whitespace setelah kunci
  Result := 0;
  while (P <= Len) and (Tail[P] in ['0'..'9']) do
  begin
    Result := Result * 10 + Ord(Tail[P]) - Ord('0');
    Inc(P);
  end;
end;

// Penggunaan: hasil non-negatif menunjuk byte tempat stream dimulai
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
  Writeln('hybrid-reference file: resave will need the /XRefStm section');

Perlakukan pemeriksaan cepat ini sebagai triase, bukan sebagai parser: ia hanya memberi tahu file mana dalam satu batch yang layak diperhatikan sebelum pekerjaan penyimpanan ulang berjalan, tidak lebih. Apa yang harus dilakukan loader dengan offset yang ditemukannya, yaitu mengikuti rantai bagian, menggabungkan entri stream di depan entri klasik, dan menghormati penanda entri bebas, dijabarkan langkah demi langkah dalam artikel pendamping kami tentang menangani PDF referensi hibrida dari aplikasi Office

Sisi penulis dari kisah ini, bagaimana object stream dan cross-reference terkompresi diproduksi sejak awal, dibahas dalam artikel kami tentang object stream dan incremental update. Ketika file hibrida yang bersangkutan juga sangat besar, teknik pemuatan dalam panduan Direct File API untuk alur kerja PDF besar memungkinkan Anda memeriksanya tanpa membaca seluruhnya ke memori. Keduanya berpasangan secara alami dengan pemulihan yang dijelaskan di sini, yang dikirimkan sebagai bagian dari HotPDF Delphi Component untuk Delphi dan C++Builder, bersama API pemuatan, penyuntingan, enkripsi, dan penandatanganan yang dibahas di bagian lain blog ini