Artikel Teknis

Lampiran PDF di Delphi dengan PDFium Component: Baca, Tambah, Hapus

Lampiran berkas PDF disimpan di dalam pohon berkas tertanam (embedded-file tree) dokumen, sebuah struktur yang sebagian besar viewer tampilkan sebagai panel klip kertas atau bilah sisi lampiran. Dari kode Delphi, PDFium Component mengekspos pohon tersebut melalui sekumpulan kecil properti terindeks pada TPdf: Anda melakukan iterasi dengan indeks integer, membaca nama dan payload byte, membuat slot baru, dan menghapus slot yang sudah ada. Permukaan API-nya sempit; hanya ada beberapa batasan pengurutan dan satu aturan sanitasi yang layak diketahui sebelum Anda menulis kode produksi di sekitarnya

Membaca lampiran dari dokumen yang terbuka

AttachmentCount memberikan jumlah berkas tertanam yang dideklarasikan oleh dokumen. Properti ini membaca langsung dari panggilan dasar PDFium, sehingga hanya mencerminkan apa yang sebenarnya terkandung dalam PDF. Dari sana, AttachmentName[Index] mengembalikan nama tampilan sebagai WString, dan Attachment[Index] mengirimkan byte mentah sebagai array TBytes. Keduanya berbasis nol. Dokumen harus dalam keadaan terbuka (Pdf.Active = True) sebelum Anda mengueri salah satu properti; memanggilnya pada dokumen yang tertutup memberi Anda hasil nol atau kosong tanpa memicu pengecualian

Satu hal yang perlu diingat: Attachment[Index] mengalokasikan dan mengembalikan payload berkas lengkap pada setiap pembacaan. Untuk dokumen yang membawa aset tertanam yang besar, melakukan iterasi melalui semua lampiran untuk membangun daftar tampilan berarti membayar biaya alokasi tersebut pada setiap panggilan. Jika Anda hanya memerlukan nama untuk tujuan tampilan, baca AttachmentName terlebih dahulu dan tunda pengambilan byte sampai pengguna benar-benar meminta berkas tersebut

procedure ListAttachments(Pdf: TPdf);
var
  I: Integer;
  Data: TBytes;
begin
  if not Pdf.Active then
    Exit;

  for I := 0 to Pdf.AttachmentCount - 1 do
  begin
    Data := Pdf.Attachment[I];
    Writeln(Format('%d: %s (%d bytes)',
      [I, Pdf.AttachmentName[I], Length(Data)]));
  end;
end;

Mengekstrak lampiran ke disk

Tidak ada helper SaveAttachment. Anda membaca byte dan menulisnya di mana pun Anda butuhkan, yang menempatkan pembuatan jalur dan sanitasi sepenuhnya pada kode Anda. Hal itu penting ketika nama lampiran berasal dari dokumen yang tidak tepercaya. Nama lampiran PDF adalah string yang disimpan di dalam berkas; mereka dapat berisi pemisah jalur, Unicode serupa, dan karakter lain yang akan menghasilkan hasil yang tidak terduga jika Anda meneruskannya langsung ke TFileStream.Create. Selalu jalankan nama tersebut melalui ExtractFileName sebelum membangun jalur output apa pun, dan pertimbangkan untuk menolak nama yang dimulai dengan titik atau berisi karakter di luar apa yang diharapkan sistem Anda

Array byte yang dikembalikan oleh Attachment[Index] dimiliki oleh pemanggil. Tuliskan dengan TFileStream biasa dan terserah Anda untuk menggunakannya seperti yang Anda suka, termasuk memeriksa beberapa byte pertama untuk memverify format berkas yang sebenarnya daripada mempercayai nama yang dideklarasikan

procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
  SafeName: string;
  OutPath: string;
  Data: TBytes;
  FS: TFileStream;
begin
  SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
  if SafeName = '' then
    SafeName := Format('attachment_%d', [Index]);

  OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
  Data := Pdf.Attachment[Index];

  FS := TFileStream.Create(OutPath, fmCreate);
  try
    if Length(Data) > 0 then
      FS.WriteBuffer(Data[0], Length(Data));
  finally
    FS.Free;
  end;
end;

Menambahkan lampiran dan penulisan dua langkah

Membuat lampiran membutuhkan dua panggilan, bukan satu. CreateAttachment(Name) mendaftarkan slot baru di pohon berkas tertanam dan mengembalikan nilai True jika berhasil. Slot tersebut dimulai dalam keadaan kosong. Anda kemudian menetapkan payload dengan menulis to Attachment[AttachmentCount - 1], menargetkan entri yang paling baru dibuat. Jika CreateAttachment mengembalikan nilai False, slot tidak dibuat dan penetapan tersebut akan merusak lampiran pada indeks mana pun yang kebetulan berada di urutan terakhir

procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
  FS: TFileStream;
  Data: TBytes;
  AttachName: string;
begin
  if not Pdf.Active then
    Exit;

  FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Data, FS.Size);
    if FS.Size > 0 then
      FS.ReadBuffer(Data[0], FS.Size);
  finally
    FS.Free;
  end;

  AttachName := ExtractFileName(FilePath);
  if Pdf.CreateAttachment(AttachName) then
    Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;

Setelah mengubah daftar lampiran, perubahan hanya hidup di memori. Panggil SaveAs to menulis berkas baru dengan pohon berkas tertanam yang diperbarui. PDFium Component tidak mendukung penyimpanan kembali ke berkas yang sama yang sedang terbuka, karena mesin memegang pegangan baca (read handle) ke sumbernya. Pola standar untuk pembaruan in-place adalah menyimpan ke jalur sementara, menutup dokumen, menghapus atau mengganti nama yang asli, lalu mengganti nama berkas sementara ke posisinya dan membuka kembali

Informasi tipe lampiran

Selain nama dan payload byte, AttachmentType[Index] mengembalikan string tipe MIME yang disimpan dalam kamus berkas tertanam PDF, jika ada yang dicatat saat berkas tersebut pertama kali dilampirkan. Banyak generator membiarkan bidang ini kosong atau menyetelnya ke nilai generik seperti application/octet-stream, sehingga Anda tidak dapat mengandalkannya untuk deteksi format dalam alur kerja produksi. Untuk identifikasi yang andal, baca beberapa byte pertama dari payload dan periksa tanda tangan berkas yang dikenal: %PDF untuk PDF bersarang, header berkas lokal ZIP PK\x03\x04 untuk dokumen Office Open XML documents, \xD0\xCF\x11\xE0 untuk biner berkas senyawa warisan. Informasi tipe dari kamus tidak masalah untuk ditampilkan dalam label UI, tetapi tidak boleh mendorong keputusan pemrosesan saat Anda memiliki byte yang sebenarnya tersedia

Menghapus lampiran

DeleteAttachment(Index) menghapus entri pada posisi tersebut dan mengembalikan nilai True jika berhasil. Setelah penghapusan, entri yang tersisa bergeser ke bawah, jadi jika Anda menghapus beberapa lampiran dalam loop, Anda harus melakukan iterasi dari indeks terakhir ke bawah, bukan ke depan, untuk menghindari terlewatnya entri setelah setiap pergeseran. Perubahan tersebut ada di memori sampai Anda memanggil SaveAs

Skenario umum dalam pipa pemrosesan dokumen adalah mencopot semua lampiran dari PDF yang masuk sebelum meneruskannya ke hilir, untuk alasan keamanan atau ukuran. Hitung sekali sebelum loop dan ulangi secara terbalik:

procedure StripAllAttachments(Pdf: TPdf);
var
  I: Integer;
begin
  for I := Pdf.AttachmentCount - 1 downto 0 do
    Pdf.DeleteAttachment(I);
end;

Di mana lampiran PDF muncul dalam praktiknya

API lampiran berfungsi pada PDF apa pun yang dapat dibuka oleh PDFium, tetapi dokumen di mana Anda benar-benar menemukan berkas tertanam berkerumun di sekitar beberapa kasus tertentu. PDF/A-3 (ISO 19005-3) secara eksplisit mengizinkan berkas tertanam yang patuh sebagai mekanisme untuk membundel data sumber bersama dengan penyajian arsip; faktur elektronik ZUGFeRD dan Factur-X mengandalkan hal ini untuk menyematkan payload XML terstruktur di dalam tata letak PDF yang dapat dibaca manusia. PDF yang berasal dari email terkadang membawa lampiran pesan asli mereka yang diteruskan ke dalam pohon berkas tertanam. Dokumentasi teknis yang berasal dari sistem kepenulisan terstruktur terkadang membundel aset pendukung dengan cara yang sama

Ketika aplikasi Anda memproses PDF masuk dari luar organisasi Anda, memeriksa AttachmentCount sebagai bagian dari asupan dokumen patut dilakukan karena dua alasan independen. Pertama, berkas tertanam mungkin membawa data yang ingin Anda ekstrak dan proses, seperti XML di dalam PDF faktur. Kedua, berkas tertanam dapat membawa konten executable sembarangan, jadi mengetahui apa yang ada sangatlah penting bahkan ketika Anda tidak pernah berniat untuk mengekstraknya. Kedua alasan tersebut tidak mengharuskan Anda melakukan sesuatu yang rumit: baca jumlahnya, periksa namanya, dan putuskan apa yang akan dilakukan dengan byte tersebut

Properti lampiran yang ditunjukkan di sini adalah bagian dari PDFium Component untuk Delphi and C++Builder