Teknik Makale

Delphi'de PDF/A Info ile XMP Metadata Eşdeğerliği

PDFium Component, PDF/A Info-XMP metadata eşdeğerliğini TPdf.InspectPdfAMetadata ile kontrol eder ve TPdf.NormalizePdfAMetadata ile onarır. ISO 19005-1 (Cor.1 düzeltmesiyle), sekiz eşlenmiş Info girdisinin her birinin — Title'dan ModDate'e — yalnızca var olmasını değil, XMP özelliğiyle aynı değeri taşımasını ister; kontrol doğru RDF biçimini okur, namespace'leri URI üzerinden eşler ve tarihleri an olarak karşılaştırır

Bu konuşmayı genellikle başlatan hata raporu zararsız görünür. Bir doküman yönetim sistemi her artımlı kayıtta Info sözlüğüne yeni bir /ModDate damgalıyor, XMP paketine dokunmuyor ve altı ay sonra bir arşiv denetimi binlerce dosyayı uyumsuz işaretliyor. İki tarih de orada. İlk düzenlemede anlaşmayı kestiler ve varlık kontrolü bunu hiç fark etmedi. Yalnızca Info üzerinden yapılan Title düzenlemeleri ve bir aracın iki dc:creator öğesine böldüğü Finance; Controlling gibi bir Author string'i de aynı şekilde düşer

PDF/A, her iki yerde de var olan metadayı neden reddediyor?

PDF/A reddeder, çünkü ISO 19005-1 §6.7.3 bir değer kuralıdır, varlık kuralı değil: Tablo 1 sekiz Info anahtarını XMP özelliklerine eşler ve bir Info anahtarı mevcut olduğunda, eşlenen XMP özelliği eşdeğer bir değer taşımak zorundadır. PDFium Component ile PDF/A preflight doğrulaması yazısındaki bayt düzeyi tarayıcı yalnızca xmp:CreateDate ve xmp:ModifyDate'in var olduğunu doğrular (pvaiMissingXmpDates). v3.72.0'dan beri TPdf.ValidatePdfA ayrıca tam değer karşılaştırmasını çalıştırır ve bir XMP paketi varken Info ile anlaşmazlığa düşerse (parse edilemeyen bir paket anlaşmazlık sayılır) sorun kümesine pvaiInfoXmpValueMismatch ekler. Eksik paket pvaiMissingXmpMetadata olarak raporlanmaya devam eder; iki sorun aynı kusuru asla çift saymaz

Eşlenen her XMP özelliği hangi RDF biçimine ihtiyaç duyar?

Sekiz eşlemenin her birinin sabit bir XMP tipi vardır ve yanlış kaptaki doğru bir değer de düşer. FPdfPdfa.pas içindeki ComparePdfAInfoAndXmp özellikleri namespace URI ile arar; http://purl.org/dc/elements/1.1/'i sıra dışı bir öneke bağlayan bir paket, dc kullanan bir paket gibi aynen okunur. Gerekli biçimler şunlar:

  • Title → dc:title ve Subject → dc:description: bir rdf:Alt dil alternatifi, yalnızca x-default öğesiyle karşılaştırılır (dil etiketi büyük-küçük harf duyarsız eşlenir); x-default'u olmayan bir Alt eksik sayılır
  • Author → dc:creator: tam olarak bir metin öğesi taşıyan, tüm Info string'ini içinde tutan bir rdf:Seq; noktalı virgülle ayrılmış yazar listesi tek girdi kalır
  • Keywords → pdf:Keywords ve Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): basit metin özellikleri
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): basit metin özellikleri
ComparePdfAInfoAndXmp'ın Delphi'de PDF/A metadata eşdeğerliği için denetlediği sekiz eşlenmiş çift: Title ile Subject x-default öğeli bir rdf:Alt ister, Author tek öğeli bir rdf:Seq, Keywords, Producer, Creator ve iki tarih basit metindir; her biri önek yerine XMP namespace URI ile aranır
Bir Info anahtarı mevcut olduğunda ISO 19005-1, eşlenen XMP özelliğinin gerekli RDF biçiminde eşdeğer bir değer taşımasını ister; yanlış kaptaki bir değer de düşer
<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>

Metin değerleri tam Unicode kod-noktası dizileri olarak karşılaştırılır; boşluk kırpma, harf katlama ya da normalizasyon yoktur. Sondaki bir boşluk ya da bir tarafta önceden birleştirilmiş é, diğer tarafta birleşik aksanlı e, gerçek bir uyuşmazlıktır. Info tarafı her zaman PDFium'un PDFDocEncoding ve UTF-16 metnini FPDF_GetMetaText üzerinden kendi yaptığı çözümlemeden gelir; bu, kütüphanenin string çözümlemeyi yeniden icat edip ince bir hata yapmasını önler. XMP tarafı ise onu üreten baytlar kadar temizdir; Free Pascal altında XMP metadatını bozan kod sayfası tuzaklarının burada da önemli olması bu yüzden

Bir PDF tarihi ile bir XMP tarihi ne zaman eşittir?

Bir PDF tarihi ile bir XMP tarihi, saniye hassasiyetiyle aynı anı ve iki tarafta da aynı saat-dili bilgisini tarif ettiklerinde eşittir. İki parser da yasal azaltılmış hassasiyeti kabul eder; D:2026 de 2026 de 1 Ocak 2026, 00:00:00 demektir. İki değer de saat dili taşıdığında karşılaştırmadan önce UTC'ye çevrilir: D:20260827093659+08'00', 2026-08-27T01:36:59Z'ye eşittir. Ne bir tarafın ne diğerinin saat dili taşımadığında yerel bileşenler yazıldığı gibi karşılaştırılır. Yalnızca bir taraf saat dili taşıdığında sonuç pamsValueMismatch'tur; bir ofset uydurmak tahmin olur. XMP'deki .250 gibi sıfırdan farklı bir kesirli saniye de uyuşmazlık zorlar, çünkü bir PDF tarihi onu ifade edemez ve sessizce yuvarlamak gerçek bir anlaşmazlığı saklardı; .000 kabul edilir. Parse edilemeyen değerler ayrıca pamsInvalidInfoDate ya da pamsInvalidXmpDate olarak raporlanır

PDFium Component'ın Delphi'de bir PDF Info tarihi ile bir XMP tarihinin eşit olduğuna nasıl karar verdiği: iki saat dili UTC'ye çevrilip anlar karşılaştırılır, saat dilsiz iki değer yazıldığı gibi karşılaştırılır, tek başına saat dili pamsValueMismatch demektir, sıfırdan farklı kesirli saniye ifade edilemez ve parse edilemeyen değerler ayrıca raporlanır
Eşitlik, saniye hassasiyetiyle aynı anı ve iki tarafta da aynı saat-dili bilgisini ifade eder; bir ofset uydurmak ya da kesirli saniyeyi yuvarlamak gerçek bir anlaşmazlığı saklardı

Varlığın kendi kuralı vardır. TPdfAMetadataValues.Present, etkin trailer'ın /Info sözlüğü gezilerek doldurulan bir kümedir ve "anahtar yok"u "boş string ile mevcut anahtar"dan ayırır. Olmayan bir anahtar pamsNotRequired verir ve XMP'den hiçbir şey istemez; /Title () mevcuttur, dolayısıyla XMP paketi boş bir x-default başlığı da taşımak zorundadır

Kaydetmeden önce Info ve XMP metadatını nasıl incelersiniz?

TPdf.InspectPdfAMetadata, alan başına bir TPdfAMetadataComparison taşıyan bir TPdfAMetadataReport döndürür; her biri Info değerini, XMP değerini ve bir TPdfAMetadataState tutar, böylece bir başarısızlık tek bir doğrulama bayrağını tersine mühendislik yapmadan açıklanabilir. MismatchFields düşen kümeyi özetler, HasXmpPacket paket bulunup bulunmadığını söyler ve XmpParseError, paket varken okunamadığında parser mesajını taşır

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;

NormalizePdfAMetadata neyi değiştirir, neyi reddeder?

TPdf.NormalizePdfAMetadata, Info sözlüğünü doğruluğun kaynağı sayar ve alanı MismatchFields'e düşen XMP özelliklerini yeniden yazar; paketteki her şey başka olduğu gibi kalır. Title ve Subject x-default öğesine yazılırken diğer dil alternatifleri bozulmaz, Author tek öğeli bir rdf:Seq'e dönüşür, bilinmeyen namespace'ler ve alakasız özellikler korunur ve olmayan Info anahtarlarına karşılık gelen XMP özellikleri dokunulmadan bırakılır. Saat dilli bir Info tarihi, Z ekli kanonik bir UTC XMP tarihi olarak yazılır; saat dilsiz olan yerel bileşenlerini korur. Dosya overload'u geçici bir dosya ve atomik bir değişimle kaydeder; XMP güncellemesinin kendisi artımlı bir güncelleme olarak eklenir

PDFium Component ile Delphi'de PDF/A metadatını onarırken NormalizePdfAMetadata'ın yeniden yazdığı şey: Info doğruluğun kaynağıdır, yalnızca MismatchFields girdileri x-default Alt metni, tek öğeli Seq ya da kanonik UTC tarihi olarak geri yazılır; bilinmeyen namespace'ler, alakasız özellikler ve olmayan-anahtar özellikleri dokunulmadan kalır
Onarım, eksik bir XMP paketini, bozuk bir Info tarihini ve imzalı belgeleri reddeder; eksiksiz bir PDF/A metadata seti kurmak, hedefli bir eşdeğerlik düzeltmesinin değil SaveAsPdfA'nın işidir

Retler bilinçlidir. XMP paketi yokken yöntem EPdfError fırlatır, çünkü eksiksiz bir PDF/A kimliklendirme ve metadata seti kurmak SaveAsPdfA'nın işidir — PDFium Component ile PDF/A arşiv dosyaları üretmek yazısında ele alınıyor. Bozuk bir Info tarihi, inandırıcı görünen yanlış bir değer yazmaktansa EPdfXmpError fırlatır ve hiçbir şey kaydedilmez. İmzalı belgeler, çağıran AllowSignedDocument = True geçmedikçe reddedilir. Eşdeğerlik, ISO 19005-1'in tek kuralıdır; normalize edilmiş bir dosya otomatik olarak uyumlu değildir

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;                      // zaten tutarlı, dosyaya dokunma
    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      // bozuk Info tarihi ya da okunamayan paket
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Karşılaştırmayı kendi XMP paketinizde çalıştırmak

ComparePdfAInfoAndXmp ile SynchronizePdfAInfoToXmp, FPdfPdfa içinde belge yüklemeden bir TPdfXmpPacket üzerinde çalışan düz fonksiyonlardır; birim testlerine ve XMP'yi bir şablondan kuran boruhattına uyar. Tek tuzak Present'tir: Default(TPdfAMetadataValues) ile başlatılmış bir kaydın kümesi boştur, her alan sonra pamsNotRequired raporlar ve ne değer doldurduysanız doldurun karşılaştırma boşuna geçer

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 hangi alanların zorunlu olduğuna karar verir; yalnız değerler yok sayılır
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Boruhattınız, başka sistemlerin düzenlemeye devam ettiği belgeleri arşivliyorsa gece boyu InspectPdfAMetadata taramasını, sapan dosyalar için NormalizePdfAMetadata ile eşleştirin ve ValidatePdfA'yı, uzun süreli depoya herhangi bir şey çıkmadan önceki kapı olarak tutun. Tipli rapor, onarım yolu ve PDF/A araç setinin kalanı Delphi ve C++Builder için PDFium Component ile gelir