Artikel Teknis

Ekuivalensi Metadata Info dan XMP PDF/A di Delphi

PDFium Component memeriksa ekuivalensi metadata Info-ke-XMP PDF/A dengan TPdf.InspectPdfAMetadata dan memperbaikinya dengan TPdf.NormalizePdfAMetadata. ISO 19005-1 (sebagaimana dikoreksi Cor.1) mensyaratkan masing-masing dari delapan entri Info yang dipetakan, dari Title sampai ModDate, membawa nilai yang sama dengan properti XMP-nya, bukan sekadar ada; pemeriksaannya membaca bentuk RDF yang benar, mencocokkan namespace lewat URI, dan membandingkan tanggal sebagai instan

Bug report yang biasanya membuka percakapan ini tampak tak berbahaya. Sistem manajemen dokumen mengecap /ModDate baru ke dictionary Info di setiap simpanan inkremental, membiarkan paket XMP begitu saja, dan enam bulan kemudian audit arsip menandai ribuan berkas sebagai non-konforman. Kedua tanggalnya ada. Mereka cuma berhenti sepakat sejak suntingan pertama, dan pemeriksaan kehadiran tak pernah menyadarinya. Suntingan Title lewat API khusus Info, dan string Author seperti Finance; Controlling yang oleh tool tertentu dipecah jadi dua item dc:creator, gagal dengan cara yang sama

Mengapa PDF/A menolak metadata yang ada di kedua tempat?

PDF/A menolaknya karena ISO 19005-1 §6.7.3 adalah aturan nilai, bukan aturan kehadiran: Tabel 1 memetakan delapan key Info ke properti XMP, dan begitu sebuah key Info hadir, properti XMP yang dipetakan harus memegang nilai yang setara. Scanner level byte yang dibahas di validasi preflight PDF/A dengan PDFium Component hanya memastikan bahwa xmp:CreateDate dan xmp:ModifyDate ada (pvaiMissingXmpDates). Sejak v3.72.0, TPdf.ValidatePdfA menjalankan pula perbandingan nilai penuh dan menambahkan pvaiInfoXmpValueMismatch ke set isu ketika paket XMP ada tapi tak sepakat dengan Info (paket yang tak bisa diurai dihitung tak sepakat). Paket yang hilang tetap dilaporkan sebagai pvaiMissingXmpMetadata, sehingga kedua isu tak pernah menghitung ganda cacat yang sama

Bentuk RDF apa yang dibutuhkan tiap properti XMP yang dipetakan?

Kedelapan pemetaan masing-masing punya tipe XMP tetap, dan nilai yang benar di wadah yang salah tetap gagal. ComparePdfAInfoAndXmp di FPdfPdfa.pas mencari properti berdasarkan URI namespace, sehingga paket yang mengikat http://purl.org/dc/elements/1.1/ ke prefiks tak biasa dibaca persis seperti yang memakai dc. Bentuk yang disyaratkan:

  • Title → dc:title dan Subject → dc:description: alternatif bahasa rdf:Alt, dibandingkan hanya terhadap item x-default-nya (tag bahasa dicocokkan tanpa membedakan huruf besar-kecil); Alt tanpa x-default dihitung hilang
  • Author → dc:creator: rdf:Seq dengan tepat satu item teks yang menampung seluruh string Info, sehingga daftar penulis berpemisah titik koma tetap satu entri
  • Keywords → pdf:Keywords dan Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): properti teks sederhana
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): properti teks sederhana
Delapan pasangan terpeta yang diperiksa ComparePdfAInfoAndXmp untuk ekuivalensi metadata PDF/A di Delphi: Title dan Subject butuh rdf:Alt dengan item x-default, Author rdf:Seq satu item, Keywords Producer Creator dan dua tanggal adalah teks sederhana, masing-masing dicari lewat URI namespace XMP alih-alih prefiks
Begitu sebuah key Info hadir, ISO 19005-1 mensyaratkan properti XMP terpeta memegang nilai setara dalam bentuk RDF yang disyaratkan, sehingga nilai di wadah yang salah tetap gagal
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report 2026</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance; Controlling</rdf:li></rdf:Seq></dc:creator>
<pdf:Producer>PDFium Component</pdf:Producer>

Nilai teks dibandingkan sebagai sekuens code point Unicode yang persis, tanpa trimming, case folding, atau normalisasi. Spasi di ekor, atau é precomposed di satu sisi dan e terdekomposisi plus aksen penggabung di sisi lain, adalah ketidakcocokan sungguhan. Sisi Info selalu berasal dari decoding PDFDocEncoding dan teks UTF-16 milik PDFium sendiri lewat FPDF_GetMetaText, yang mencegah library menulis ulang decoding string dan salah secara halus; sisi XMP hanya sebersih byte yang menghasilkannya, dan itulah kenapa jebakan codepage yang merusak metadata XMP di Free Pascal juga relevan di sini

Kapan tanggal PDF dan tanggal XMP dikatakan sama?

Tanggal PDF dan tanggal XMP dikatakan sama ketika keduanya mendeskripsikan instan yang sama sampai ke detik, dengan pengetahuan zona waktu yang sama di kedua sisi. Kedua parser menerima presisi tereduksi yang sah, jadi D:2026 dan 2026 sama-sama berarti 1 Januari 2026, 00:00:00. Ketika kedua nilai membawa zona, keduanya dikonversi ke UTC sebelum dibandingkan: D:20260827093659+08'00' sama dengan 2026-08-27T01:36:59Z. Ketika tak ada yang membawa zona, komponen lokal dibandingkan apa adanya. Ketika hanya satu sisi yang punya zona, hasilnya pamsValueMismatch, karena mengarang offset berarti menebak. Detik pecahan non-nol seperti .250 di XMP juga memaksa ketidakcocokan, karena tanggal PDF tak punya cara mengekspresikannya dan membulatkannya diam-diam akan menyembunyikan ketidaksepakatan nyata; .000 diterima. Nilai yang tak bisa diurai dilaporkan terpisah sebagai pamsInvalidInfoDate atau pamsInvalidXmpDate

Cara PDFium Component memutuskan bahwa tanggal Info PDF dan tanggal XMP sama di Delphi: dua zona dikonversi ke UTC dan instannya dibandingkan, dua nilai tanpa zona dibandingkan apa adanya, satu zona saja adalah pamsValueMismatch, detik pecahan non-nol tak bisa diekspresikan, dan nilai yang tak bisa diurai dilaporkan terpisah
Kesamaan berarti instan yang sama sampai ke detik dengan pengetahuan zona waktu yang sama di kedua sisi, sehingga mengarang offset atau membulatkan detik pecahan akan menyembunyikan ketidaksepakatan nyata

Kehadiran punya aturannya sendiri. TPdfAMetadataValues.Present adalah set yang diisi dengan menelusuri dictionary /Info milik trailer aktif, dan ia memisahkan "key absen" dari "key hadir dengan string kosong". Key yang absen menghasilkan pamsNotRequired dan tak menuntut apa pun dari XMP; /Title () dihitung hadir, sehingga paket XMP harus membawa judul x-default kosong juga

Bagaimana menginspeksi metadata Info dan XMP sebelum menyimpan?

TPdf.InspectPdfAMetadata mengembalikan TPdfAMetadataReport dengan satu TPdfAMetadataComparison per field, masing-masing menampung nilai Info, nilai XMP, dan sebuah TPdfAMetadataState, sehingga kegagalan bisa dijelaskan tanpa me-reverse-engineer satu flag validasi. MismatchFields meringkas set yang gagal, HasXmpPacket memberi tahu apakah ada paket yang ditemukan, dan XmpParseError membawa pesan parser ketika paket ada tapi tak bisa dibaca

uses
  System.SysUtils, PDFium, FPdfPdfa;

const
  FieldNames: array[TPdfAMetadataField] of string = (
    'Title', 'Author', 'Subject', 'Keywords',
    'Creator', 'Producer', 'CreationDate', 'ModDate');
  StateNames: array[TPdfAMetadataState] of string = (
    'not required', 'equivalent', 'XMP missing', 'XMP type mismatch',
    'value mismatch', 'invalid Info date', 'invalid XMP date');

procedure ReportMetadata(Pdf: TPdf);
var
  Report: TPdfAMetadataReport;
  Item: TPdfAMetadataComparison;
begin
  Report := Pdf.InspectPdfAMetadata;
  if Report.XmpParseError <> '' then
    Writeln('XMP packet unreadable: ', Report.XmpParseError)
  else if not Report.HasXmpPacket then
    Writeln('No XMP packet at all');
  for Item in Report.Comparisons do
    if not Item.IsEquivalent then
      Writeln(Format('%-12s %-18s Info="%s" XMP="%s"',
        [FieldNames[Item.Field], StateNames[Item.State],
         Item.InfoValue, Item.XmpValue]));
end;

Apa yang diubah NormalizePdfAMetadata, dan apa yang ditolaknya?

TPdf.NormalizePdfAMetadata memperlakukan dictionary Info sebagai sumber kebenaran dan hanya menulis ulang properti XMP yang field-nya mendarat di MismatchFields; segala hal lain di paket selamat. Title dan Subject ditulis ke item x-default sementara alternatif bahasa lain tetap utuh, Author menjadi rdf:Seq satu item, namespace tak dikenal dan properti tak terkait dipertahankan, dan properti XMP untuk key Info yang absen dibiarkan tak tersentuh. Tanggal Info berzona ditulis sebagai tanggal XMP UTC kanonik dengan sufiks Z; yang tanpa zona mempertahankan komponen lokalnya. Overload berkas menyimpan lewat berkas sementara dan replace atomik, dan pembaruan XMP sendiri ditambahkan sebagai incremental update

Apa yang ditulis ulang NormalizePdfAMetadata saat memperbaiki metadata PDF/A di Delphi dengan PDFium Component: Info adalah sumber kebenaran, hanya entri MismatchFields yang ditulis kembali sebagai teks Alt x-default, Seq satu item, atau tanggal UTC kanonik, sementara namespace tak dikenal, properti tak terkait, dan properti key-absen selamat tak tersentuh
Perbaikan ini menolak paket XMP yang hilang, tanggal Info yang cacat, dan dokumen bertanda tangan, karena menyusun set metadata PDF/A lengkap adalah tugas SaveAsPdfA, bukan tugas perbaikan ekuivalensi yang tertarget

Penolakan-penolakan itu disengaja. Tanpa paket XMP, metode melempar EPdfError, karena menyusun set identifikasi dan metadata PDF/A lengkap adalah tugas SaveAsPdfA, yang dibahas di membuat berkas arsip PDF/A dengan PDFium Component. Tanggal Info yang cacat melempar EPdfXmpError alih-alih menulis nilai salah yang tampak masuk akal, dan tak ada yang tersimpan. Dokumen bertanda tangan ditolak kecuali caller memberikan AllowSignedDocument = True. Ekuivalensi juga salah satu aturan ISO 19005-1, jadi berkas yang ternormalisasi bukan otomatis konforman

uses
  System.SysUtils, PDFium, FPdfPdfa, FPdfXmp;

procedure NormalizeArchive(const Source, Target: string);
var
  Pdf: TPdf;
  Report: TPdfAMetadataReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := Source;
    Pdf.Active := True;
    Report := Pdf.InspectPdfAMetadata;
    if Report.IsEquivalent then
      Exit;                      // sudah konsisten, biarkan berkasnya
    if not Report.HasXmpPacket then
      raise Exception.Create('No XMP packet: convert with SaveAsPdfA instead');
    try
      if not Pdf.NormalizePdfAMetadata(Target) then
        raise Exception.Create('Normalized save failed');
    except
      on E: EPdfXmpError do      // tanggal Info cacat atau paket tak terbaca
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Menjalankan perbandingan pada paket XMP Anda sendiri

ComparePdfAInfoAndXmp dan SynchronizePdfAInfoToXmp adalah fungsi polos di FPdfPdfa yang bekerja pada TPdfXmpPacket tanpa dokumen termuat, cocok untuk unit test dan pipeline yang merakit XMP dari template. Satu jebakannya adalah Present: record yang diinisialisasi dengan Default(TPdfAMetadataValues) punya set kosong, semua field lalu melaporkan pamsNotRequired, dan perbandingan lolos dengan hampa apa pun nilai yang Anda isi

uses
  System.SysUtils, System.IOUtils, FPdfPdfa, FPdfXmp;

procedure AlignTemplate(const TemplateFile: string);
var
  Info: TPdfAMetadataValues;
  Packet: TPdfXmpPacket;
  Changed: TPdfAMetadataFields;
begin
  Info := Default(TPdfAMetadataValues);
  Info.Title := 'Quarterly Report 2026';
  Info.Author := 'Finance; Controlling';
  Info.ModDate := 'D:20260827093659+08''00''';
  // Present menentukan field mana yang wajib; nilai saja diabaikan
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

  Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
  try
    Changed := SynchronizePdfAInfoToXmp(Info, Packet);
    // xmp:ModifyDate kini 2026-08-27T01:36:59Z, dc:creator rdf:Seq satu item
    if Changed <> [] then
      TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
  finally
    Packet.Free;
  end;
end;

Kalau pipeline Anda mengarsipkan dokumen yang terus disunting sistem lain, pasangkan sapuan InspectPdfAMetadata malam-harian dengan NormalizePdfAMetadata untuk berkas yang mengembar, dan jadikan ValidatePdfA gerbang sebelum apa pun berangkat ke penyimpanan jangka panjang. Report bertipe, jalur perbaikan, dan sisa perkakas PDF/A terkirim bersama PDFium Component untuk Delphi dan C++Builder