Artikel Teknis

Kepatuhan Arsip PDF/A di Delphi dengan PDFium VCL

Anda mengirimkan sebuah converter yang menandai setiap file sebagai PDF/A-1b, sistem rekam pelanggan mengimpor semuanya selama setahun, lalu saat audit menjalankan seluruh batch itu melalui veraPDF, sepertiganya ternyata tidak sesuai. Tidak ada yang crash, tidak ada exception yang dilempar, file terbuka normal di setiap viewer di meja Anda. Hanya saja, file itu bukan standar yang Anda klaim. Inilah kegagalan yang paling umum pada PDF arsip, dan alasan mengapa "kami sudah setel flag-nya" tidak pernah sama dengan "sudah tervalidasi"

Hal pertama yang perlu dipahami tentang PDFium dan PDF/A adalah bahwa mesin PDFium tidak ada hubungannya dengan hal ini. PDFium merender, mem-parse, dan menulis PDF, tetapi surface publiknya tidak punya ConvertToPDFA, tidak punya penulis OutputIntent, dan tidak punya API XMP. Seluruh kepatuhan arsip, paket XMP, OutputIntent dan profil ICC-nya, marker katalog, dan validasinya, berada di PDFiumPas sendiri, dalam unit Pascal murni sekitar 2.000 baris (FPdfPdfa.pas) yang membaca byte hasil simpan lalu menulis ulang melalui incremental update. Mengetahui di mana pekerjaan itu terjadi memberi tahu Anda di mana bug bersembunyi, dan bug itu bukan bersembunyi di PDFium

Apa yang sebenarnya dituntut PDF/A, dan di mana ia menjebak

PDF/A bukan satu format. ISO 19005 mendefinisikan tiga bagian (PDF/A-1, -2, -3) dan, di dalam masing-masingnya, level kepatuhan yang menjanjikan hal berbeda. Level B (basic) hanya menjamin tampilan visual dapat direproduksi. Level A (accessible) menambahkan structure tree bertag dan pemetaan Unicode di atas B. Level U, yang hanya ada untuk bagian 2 dan 3, berada di tengah: teks Unicode yang andal tanpa structure tree penuh. ISO 19005-1 tidak punya Level U, batasan yang di-encode langsung oleh library

Sejumlah aturan format inilah yang paling sering menjebak dalam praktik. Enkripsi dilarang mutlak (ISO 19005-1 §6.1.3 dan penerusnya): file PDF/A tidak boleh membawa dictionary /Encrypt. Dokumen harus mendeklarasikan kondisi rendering keluaran melalui OutputIntent dengan destinasi berupa profil ICC yang valid (§6.2.3.2). Klaim kepatuhan itu sendiri harus muncul sebagai metadata XMP di bawah skema identifikasi PDF/A. Level A juga mensyaratkan struktur logis §6.8, tag tree yang membuat dokumen dapat dibaca mesin. Jika salah satu hilang, verifier kepatuhan menolak file itu walaupun tampilannya sempurna

Satu pemanggilan yang menghasilkan arsip

PDFiumPas mengekspos seluruh alur di balik TPdf.SaveAsPdfA. Overload sederhana menerima target conformance dan default-nya adalah PDF/A-1b, yang merupakan default yang tepat untuk kasus umum "buat ini bisa dirender selamanya"

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Di balik layar ini adalah langkah dua tahap. SaveAsPdfA lebih dulu meminta PDFium menserialisasi dokumen dengan FPDF_SaveAsCopy, lalu menyerahkan aliran byte itu ke InjectPdfAMarkers, yang menambahkan metadata XMP, OutputIntent sRGB dengan profil ICC tertanam, dan katalog yang ditulis ulang sebagai incremental update. Sumber dibaca dari posisi nol dan tujuan ditulis dari posisi nol; tree objek asli tetap utuh dan marker masuk setelah %%EOF yang sudah ada. Jika Anda butuh byte-nya, bukan file, SaveAsPdfAToStream menerima TStream dan opsi yang sama

Memilih conformance dengan record opsi

Untuk menargetkan bagian dan level tertentu, berikan record TPdfASaveOptions. Field Conformance-nya menerima nilai TPdfAConformance. Enumerasi ini mencakup setiap kombinasi yang valid dan tidak lebih: pac1b, pac1a untuk bagian 1; pac2b, pac2u, pac2a untuk bagian 2; pac3b, pac3u, pac3a untuk bagian 3, plus pacUnknown dan pacNone untuk sisi validasi. Tidak ada pac1u, karena level itu memang tidak ada dalam standar

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Sebagian besar record bisa dibiarkan kosong. Biarkan Title, Author, Subject, Keywords, Creator, dan Producer kosong, lalu SaveAsPdfA akan mengisinya otomatis dari dictionary Info dokumen lewat FPDF_GetMetaText. Biarkan CreationDate dan ModDate kosong, lalu ia akan memakai waktu UTC saat ini untuk kedua tanggal XMP. Biarkan DocumentId dan InstanceId kosong, lalu library akan mengisinya dari FPDF_GetFileIdentifier, atau jatuh balik ke ID deterministik yang diturunkan dari byte sumber. Satu field yang mungkin ingin Anda timpa secara sengaja adalah IccProfileData: kosong berarti profil sRGB IEC61966-2.1 bawaan, sedangkan alur kerja CMYK atau grayscale sebaiknya menyediakan profilnya sendiri

Mengapa Level A diturunkan, dan mengapa itu pilihan yang jujur

Ada satu nuansa yang sering menjatuhkan orang yang mengira sebuah flag adalah jaminan. Anda bisa meminta pac1a pada dokumen yang tidak punya tag tree, tetapi PDF/A-1a mensyaratkan struktur logis §6.8, dan library tidak bisa menciptakan structure tree dari PDF yang tidak bertag. Daripada mengeluarkan file yang mengklaim Level A padahal gagal, SaveAsPdfA memeriksa apakah ada struktur bertag yang benar (/StructTreeRoot plus /MarkInfo dengan /Marked true) dan, jika tidak ada, menurunkan klaim: pac1a menjadi pac1b, pac2a menjadi pac2b, dan seterusnya di semua tiga bagian. Helper internalnya adalah PdfAIsLevelA dan PdfADowngradeToLevelB

Alasan di baliknya layak dijelaskan terus terang: file yang jujur menyatakan level yang benar-benar dipenuhi lebih berguna daripada file yang berbohong soal level yang tidak dipenuhinya. Level U ditangani berbeda. Mendeteksi cakupan Unicode yang benar berarti harus memakai tes naif "apakah ada /ToUnicode", yang justru terlalu agresif menurunkan dokumen yang sah (WinAnsi dan encoding serupa dikecualikan), jadi sisi save memancarkan klaim U sesuai yang dinyatakan caller dan membiarkan ketidaksesuaian ditandai di sisi validasi. Jika Anda butuh arsip Level A yang pasti, beri tag pada dokumen sebelum dikonversi; converter tidak akan menciptakan struktur yang memang tidak ada

Jebakan ICC yang hanya tertangkap validator sungguhan

Inilah kegagalan yang mengajarkan pelajaran paling keras, karena checker bawaan library lolos sementara veraPDF, validator referensi ISO 19005, tidak. PDF/A mensyaratkan profil destinasi OutputIntent berupa stream ICCBased yang valid, dan §6.2.3.2 mewajibkan verifier memvalidasi stream itu sebagai color space. Stream ICCBased harus mendeklarasikan /N, yaitu jumlah komponen warna. Versi awal injector menulis dictionary stream ICC hanya dengan /Length tanpa /N, dan veraPDF menolak hasilnya dengan "The N entry (value null)... is missing"

Yang membuatnya licin adalah penolakan itu hanya muncul untuk PDF/A-1b dan -1a. Model kepatuhan bagian 2 dan bagian 3 tidak menjalankan pemeriksaan khusus itu pada profil destinasi, sehingga struktur yang sama lolos validasi pada pac2b, pac3b, dan pac2u, tetapi gagal pada pac1b hanya karena nilai pdfaid:part. Unit test tidak akan pernah menangkapnya, karena ValidatePdfACompliance bawaan library hanya memeriksa bahwa kunci /DestOutputProfile ada, bukan isi stream dictionary di dalamnya. Tes internal tetap hijau; validasi arsip sungguhan gagal

Perbaikannya adalah IccComponentCount, yang membaca signature data colour space pada offset 16 di header ICC dan memetakannya ke jumlah komponen: GRAY bernilai 1, RGB , Lab , dan XYZ bernilai 3, CMYK bernilai 4, dengan profil yang tidak dikenal default ke 3. Jumlah itu dimasukkan ke dictionary stream sebagai /N. Nilainya dihitung, bukan di-hard-code menjadi 3, supaya caller yang memberikan profil CMYK atau grayscale melalui IccProfileData tetap mendapat nilai yang benar. Pelajaran yang lebih luas bersifat metodologis: checker di dalam library dan validator otoritatif sama-sama punya titik buta, dan keluaran PDF/A harus diuji end-to-end terhadap implementasi referensi seperti veraPDF, bukan dipercaya dari self-check. Disiplin incremental update yang sama di balik arsip bersih dibahas di validating compressed object and xref streams, yang penting karena PDF modern yang diproses injector sering dibangun di atas cross-reference stream

Enkripsi, xref streams, dan kasus tepi lainnya

Karena ISO 19005 melarang enkripsi, jalur save menghapusnya sebelum menulis. SaveAsPdfA menerapkan FPDF_REMOVE_SECURITY saat menserialisasi, jadi sumber yang terenkripsi (dibuka dengan passwordnya) didekripsi saat masuk ke arsip. Pada dokumen yang tidak terenkripsi, ini no-op dan tidak mengubah apa pun. Konsekuensinya sama dengan batasan yang ditegakkan HotPDF dari arah sebaliknya: satu file tidak bisa sekaligus terenkripsi dan PDF/A. Jika workflow membutuhkan keduanya, solusinya adalah dua artefak, satu salinan terenkripsi untuk distribusi dan satu salinan bersih terpisah untuk arsip

Satu edge case lagi tidak terlihat sampai benar-benar menggigit: dokumen PDF 1.5+ yang memakai pure cross-reference stream dan tidak membawa keyword trailer. Injector membaca trailer untuk mencari /Info sumber dan menambahkan incremental update-nya, dan ia harus menerima bentuk xref-stream, kalau tidak dokumen seperti itu akan disalin lewat begitu saja dengan marker yang diam-diam hilang. ISO 32000-1 §7.5.6 secara eksplisit mengizinkan incremental update trailer klasik mengikuti dokumen xref-stream, dengan /Prev menunjuk ke offset xref-stream, yang memang merupakan struktur yang dihasilkan injector. FPDF_SaveAsCopy milik PDFium sendiri selalu menulis trailer klasik, jadi dalam pipeline normal injector tidak pernah bertemu sumber xref-stream murni, tetapi jalur baca menangani dokumen yang datang dari tempat lain

Memverifikasi sebelum Anda mempercayai klaimnya

Library ini menyediakan checker tingkat byte, TPdf.ValidatePdfA, yang mengembalikan TPdfAValidationResult. Field Conformance-nya melaporkan level yang terdeteksi dan Issues adalah set nilai TPdfAValidationIssue; method kenyamanan IsCompliant bernilai true hanya ketika level nyata terdeteksi dan set issue kosong. Jalankan ini sebagai gerbang pertama yang cepat dalam batch

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Jujurlah soal apa yang Anda dapatkan dari sini. Checker tingkat byte menangkap masalah struktural dengan keyakinan tinggi, seperti OutputIntent yang hilang, action yang dilarang, /Encrypt yang masih ada, atau transparansi di tempat part 1 melarangnya, dan deteksi embedding font memakai heuristik hitungan yang sengaja hanya melaporkan sinyal berkeyakinan tinggi alih-alih mengejar cakupan per-glyph. Yang tidak dilakukan adalah analisis operator content-stream, yang akan membutuhkan parser konten penuh dan memang di luar cakupan. Untuk release gate, pasangkan checker di dalam library dengan veraPDF: checker instan dan berjalan di mana saja tanpa DLL, veraPDF bersifat otoritatif. Menyambungkan pasangan itu ke batch run dibahas dalam CLI laporan preflight batch, dan di sanalah validasi ini seharusnya berada dalam workflow arsip sungguhan

API SaveAsPdfA, InjectPdfAMarkers, dan ValidatePdfA yang ditampilkan di sini disertakan bersama PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC. Halaman produk menautkan referensi API lengkap, termasuk enumerasi conformance yang utuh dan record opsi di balik contoh-contoh ini