Technisch artikel

PDF/A Info- en XMP-metadata-equivalentie in Delphi

PDFium Component controleert de PDF/A-equivalentie van Info-naar-XMP-metadata met TPdf.InspectPdfAMetadata en herstelt die met TPdf.NormalizePdfAMetadata. ISO 19005-1 (zoals gecorrigeerd door Cor.1) eist dat elk van de acht gemapte Info-invoeren, van Title tot ModDate, dezelfde waarde draagt als zijn XMP-eigenschap, en niet slechts bestaat; de controle leest de juiste RDF-vorm, matcht namespaces op URI en vergelijkt datums als tijdstippen

Het bugrapport dat dit gesprek meestal opent oogt onschuldig. Een documentmanagementsysteem stempelt bij elke incrementele save een nieuwe /ModDate in het Info-woordenboek, laat het XMP-pakket met rust, en zes maanden later vlagt een archiefaudit duizenden bestanden als niet-conform. Beide datums zijn er. Ze zijn alleen op de eerste bewerking uit elkaar gegaan, en een aanwezigheidscontrole merkte daar nooit iets van. Titelbewerkingen via een API die alleen Info kent, en een Author-string als Finance; Controlling die een of andere tool splitste in twee dc:creator-items, falen op dezelfde manier

Waarom verwerpt PDF/A metadata die op beide plekken bestaat?

PDF/A verwerpt het omdat ISO 19005-1 §6.7.3 een waarderegel is, geen aanwezigheidsregel: Tabel 1 mapt acht Info-sleutels op XMP-eigenschappen, en zodra een Info-sleutel aanwezig is, moet de gemapte XMP-eigenschap een equivalente waarde bevatten. De scanner op byteniveau uit PDF/A-preflightvalidatie met PDFium Component bevestigt alleen dat xmp:CreateDate en xmp:ModifyDate bestaan (pvaiMissingXmpDates). Sinds v3.72.0 draait TPdf.ValidatePdfA er bovendien de volledige waardevergelijking naast en voegt pvaiInfoXmpValueMismatch toe aan de issueset zodra er een XMP-pakket is dat het niet met Info eens is (een pakket dat niet te parsen is, telt als oneens). Een ontbrekend pakket blijft gemeld als pvaiMissingXmpMetadata, dus de twee issues tellen hetzelfde defect nooit dubbel

Welke RDF-vorm heeft elke gemapte XMP-eigenschap nodig?

Elk van de acht mappings heeft een vast XMP-type, en een correcte waarde in de verkeerde container faalt alsnog. ComparePdfAInfoAndXmp in FPdfPdfa.pas zoekt eigenschappen op bij namespace-URI, dus een pakket dat http://purl.org/dc/elements/1.1/ aan een ongebruikelijk prefix bindt, wordt precies zo gelezen als één die dc gebruikt. De vereiste vormen zijn:

  • Title → dc:title en Subject → dc:description: een rdf:Alt-taalalternatief, vergeleken uitsluitend met zijn item x-default (de taaltag wordt case-insensitief gematcht); een Alt zonder x-default telt als ontbrekend
  • Author → dc:creator: een rdf:Seq met precies één tekstitem dat de volledige Info-string bevat, zodat een puntkomma-gescheiden auteurslijst één enkele invoer blijft
  • Keywords → pdf:Keywords en Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): eenvoudige teksteigenschappen
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): eenvoudige teksteigenschappen
De acht gemapte paren die ComparePdfAInfoAndXmp controleert op PDF/A-metadata-equivalentie in Delphi: Title en Subject hebben een rdf:Alt met een x-default-item nodig, Author een rdf:Seq met één item, Keywords, Producer, Creator en de twee datums zijn gewone tekst, elk opgezocht bij XMP-namespace-URI in plaats van prefix
Zodra een Info-sleutel aanwezig is, eist ISO 19005-1 dat de gemapte XMP-eigenschap een equivalente waarde in de vereiste RDF-vorm bevat, dus een waarde in de verkeerde container faalt alsnog
<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>

Tekstwaarden worden vergeleken als exacte reeksen van Unicode-codepunten, zonder trimmen, case folding of normalisatie. Een spatie aan het eind, of een precomposed é aan de ene kant en een decomposed e plus combinerend accent aan de andere, is een echte mismatch. De Info-kant komt altijd uit de eigen decodering door PDFium van PDFDocEncoding- en UTF-16-tekst via FPDF_GetMetaText, zodat de library de stringdecodering niet opnieuw hoeft te implementeren en daar subtiel de fout in kan gaan; de XMP-kant is net zo schoon als de bytes die hem opleverden, en daarom zijn de codepage-valkuilen die XMP-metadata onder Free Pascal corrumperen ook hier van belang

Wanneer is een PDF-datum gelijk aan een XMP-datum?

Een PDF-datum en een XMP-datum zijn gelijk als ze hetzelfde tijdstip tot op de seconde beschrijven, met aan beide kanten dezelfde kennis van de tijdzone. Beide parsers accepteren wettig gereduceerde precisie, dus D:2026 en 2026 betekenen beide 1 januari 2026, 00:00:00. Dragen beide waarden een zone, dan worden ze vóór de vergelijking naar UTC omgezet: D:20260827093659+08'00' is gelijk aan 2026-08-27T01:36:59Z. Dragen geen van beide een zone, dan worden de lokale onderdelen vergeleken zoals ze staan. Heeft slechts één kant een zone, dan is de uitkomst pamsValueMismatch, want een offset verzinnen is gokken. Een fractionele seconde ongelijk aan nul, zoals .250 in XMP, dwingt ook een mismatch af, want een PDF-datum kan die niet uitdrukken en hem geruisloos wegronden zou een echte tegenstrijdigheid verhullen; .000 wordt geaccepteerd. Onparseerbare waarden worden apart gemeld als pamsInvalidInfoDate of pamsInvalidXmpDate

Hoe PDFium Component in Delphi beslist dat een PDF Info-datum en een XMP-datum gelijk zijn: twee zones worden naar UTC omgezet en vergelijken tijdstippen, twee waarden zonder zone worden vergeleken zoals ze staan, één zone alleen is een pamsValueMismatch, een fractionele seconde ongelijk aan nul is niet uit te drukken, en onparseerbare waarden worden apart gemeld
Gelijkheid betekent hetzelfde tijdstip tot op de seconde met aan beide kanten dezelfde kennis van de tijdzone, dus een offset verzinnen of een fractionele seconde wegronden zou een echte tegenstrijdigheid verhullen

Aanwezigheid heeft zijn eigen regel. TPdfAMetadataValues.Present is een set die wordt gevuld door het woordenboek /Info van de actieve trailer af te lopen, en hij houdt "sleutel afwezig" gescheiden van "sleutel aanwezig met een lege string". Een afwezige sleutel levert pamsNotRequired op en eist niets van XMP; /Title () is aanwezig, dus het XMP-pakket moet ook een lege titel x-default meedragen

Hoe inspecteert u Info- en XMP-metadata vóór het opslaan?

TPdf.InspectPdfAMetadata geeft een TPdfAMetadataReport terug met één TPdfAMetadataComparison per veld, elk met de Info-waarde, de XMP-waarde en een TPdfAMetadataState, zodat een faling uit te leggen is zonder één validatievlag te moeten reverse-engineeren. MismatchFields vat de falende set samen, HasXmpPacket vertelt of er een pakket is gevonden, en XmpParseError draagt de parsermelding wanneer het pakket bestaat maar niet leesbaar is

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;

Wat verandert NormalizePdfAMetadata, en wat weigert het?

TPdf.NormalizePdfAMetadata behandelt het Info-woordenboek als de bron van waarheid en herschrijft alleen de XMP-eigenschappen waarvan het veld in MismatchFields terechtkwam; al het andere in het pakket overleeft het. Title en Subject worden in het item x-default geschreven terwijl andere taalalternatieven intact blijven, Author wordt een rdf:Seq met één item, onbekende namespaces en ongerelateerde eigenschappen blijven behouden, en XMP-eigenschappen voor afwezige Info-sleutels worden onaangeroerd gelaten. Een Info-datum met zone wordt geschreven als een canonieke UTC-XMP-datum met suffix Z; één zonder zone houdt zijn lokale onderdelen. De bestandsoverload slaat op via een tijdelijk bestand en een atomaire vervanging, en de XMP-update zelf wordt toegevoegd als incrementele update

Wat NormalizePdfAMetadata herschrijft bij het herstellen van PDF/A-metadata in Delphi met PDFium Component: Info is de bron van waarheid, alleen MismatchFields-invoeren worden teruggeschreven als x-default Alt-tekst, een Seq met één item of een canonieke UTC-datum, terwijl onbekende namespaces, ongerelateerde eigenschappen en eigenschappen van afwezige sleutels onaangeroerd overleven
De reparatie weigert een ontbrekend XMP-pakket, een misvormde Info-datum en ondertekende documenten, want een volledige PDF/A-metadataset bouwen is het werk van SaveAsPdfA, niet van een gerichte equivalentiefix

De weigeringen zijn bewust. Zonder XMP-pakket gooit de methode EPdfError, want het bouwen van een volledige PDF/A-identificatie en metadataset is het werk van SaveAsPdfA, behandeld in PDF/A-archiefbestanden maken met PDFium Component. Een misvormde Info-datum gooit EPdfXmpError in plaats van een plausibel ogende foute waarde te schrijven, en er wordt niets opgeslagen. Ondertekende documenten worden geweigerd tenzij de aanroeper AllowSignedDocument = True doorgeeft. Equivalentie is ook slechts één regel van ISO 19005-1, dus een genormaliseerd bestand is niet automatisch conformant

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;                      // al consistent, laat het bestand met rust
    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      // misvormde Info-datum of onleesbaar pakket
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

De vergelijking draaien op uw eigen XMP-pakket

ComparePdfAInfoAndXmp en SynchronizePdfAInfoToXmp zijn gewone functies in FPdfPdfa die op een TPdfXmpPacket werken zonder geladen document, wat handig is voor unittests en pipelines die XMP uit een sjabloon opbouwen. De enige valkuil is Present: een record geïnitialiseerd met Default(TPdfAMetadataValues) heeft een lege set, elk veld meldt dan pamsNotRequired, en de vergelijking slaagt vacuüm, ongeacht welke waarden u invulde

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 bepaalt welke velden verplicht zijn; waarden alleen worden genegeerd
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Archivert uw pipeline documenten die andere systemen blijven bewerken, dan koppelt u een nachtelijke InspectPdfAMetadata-ronde aan NormalizePdfAMetadata voor de bestanden die afdwalen, en houdt u ValidatePdfA als poort voordat er iets naar langetermijnopslag vertrekt. Het getypeerde rapport, het reparatiepad en de rest van de PDF/A-tooling zitten in de PDFium Component voor Delphi en C++Builder