Teknisk artikel

PDF/A Info- og XMP-metadata-ækvivalens i Delphi

PDFium Component tjekker PDF/A Info-til-XMP metadata-ækvivalens med TPdf.InspectPdfAMetadata og reparerer den med TPdf.NormalizePdfAMetadata. ISO 19005-1 (som rettet af Cor.1) kræver, at hver af de otte mappede Info-entries, Title gennem ModDate, bærer samme værdi som sin XMP-egenskab, ikke blot eksisterer; tjekket læser den korrekte RDF-form, matcher namespaces efter URI og sammenligner datoer som tidspunkter

Fejlrapporten, der sædvanligvis starter denne samtale, ser harmløs ud. Et dokumentstyringssystem stempler en ny /ModDate ind i Info-dictionaryen ved hver inkrementel gemning, lader XMP-pakken være, og seks måneder senere flagger en arkivrevision tusindvis af filer som ikke-konforme. Begge datoer er der. De holdt bare op med at være enige ved den første redigering, og et tilstedeværelsestjek lagde aldrig mærke til det. Titelredigeringer foretaget gennem et Info-only API, og en Author-streng som Finance; Controlling, som et eller andet værktøj delte i to dc:creator-items, fejler på samme måde

Hvorfor afviser PDF/A metadata, der findes begge steder?

PDF/A afviser det, fordi ISO 19005-1 §6.7.3 er en værdiregel, ikke en tilstedeværelseregel: Tabel 1 mapper otte Info-nøgler til XMP-egenskaber, og så snart en Info-nøgle er til stede, skal den mappede XMP-egenskab holde en ækvivalent værdi. Den byteniveau-scanner, der er beskrevet i PDF/A preflight-validering med PDFium Component, bekræfter kun, at xmp:CreateDate og xmp:ModifyDate eksisterer (pvaiMissingXmpDates). Siden v3.72.0 kører TPdf.ValidatePdfA desuden den fulde værdisammenligning og tilføjer pvaiInfoXmpValueMismatch til issue-sættet, når en XMP-pakke eksisterer, men er uenig med Info (en pakke, der ikke kan parses, tæller som uenig). En manglende pakke forbliver rapporteret som pvaiMissingXmpMetadata, så de to issues tæller aldrig samme defekt dobbelt

Hvilken RDF-form skal hver mappede XMP-egenskab have?

Hver af de otte mappinger har en fast XMP-type, og en korrekt værdi i den forkerte container fejler stadig. ComparePdfAInfoAndXmp i FPdfPdfa.pas slår egenskaber op efter namespace-URI, så en pakke, der binder http://purl.org/dc/elements/1.1/ til et usædvanligt præfiks, læses præcis som en, der bruger dc. De krævede former er:

  • Title → dc:title og Subject → dc:description: et rdf:Alt sprog-alternativ, sammenlignet kun med sit x-default-item (sprogtagget matches case-insensitivt); en Alt uden x-default tæller som manglende
  • Author → dc:creator: en rdf:Seq med præcis ét tekstitem, der holder hele Info-strengen, så en semikolon-adskilt forfatterliste forbliver ét enkelt entry
  • Keywords → pdf:Keywords og Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): simple tekstegenskaber
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): simple tekstegenskaber
De otte mappede par, som ComparePdfAInfoAndXmp tjekker for PDF/A metadata-ækvivalens i Delphi: Title og Subject behøver en rdf:Alt med et x-default-item, Author en ét-item rdf:Seq, Keywords, Producer, Creator og de to datoer er simpel tekst, hver opslået efter XMP namespace-URI frem for præfiks
Så snart en Info-nøgle er til stede, kræver ISO 19005-1, at den mappede XMP-egenskab holder en ækvivalent værdi i den krævede RDF-form, så en værdi i den forkerte container fejler stadig
<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>

Tekstværdier sammenlignes som eksakte Unicode kodepunkt-sekvenser, uden trimming, case folding eller normalisering. Et efterstillede mellemrum, eller et precomposed é på den ene side og et decomposed e plus kombinerende accent på den anden, er en ægte mismatch. Info-siden kommer altid fra PDFiums egen dekodning af PDFDocEncoding og UTF-16-tekst gennem FPDF_GetMetaText, hvilket holder biblioteket fra selv at genimplementere strengdekodning og få det subtilt forkert; XMP-siden er kun så ren som de bytes, der producerede den, hvilket er grunden til, at codepage-fælderne, der korrumperer XMP-metadata under Free Pascal, også tæller her

Hvornår er en PDF-dato og en XMP-dato ens?

En PDF-dato og en XMP-dato er ens, når de beskriver samme tidspunkt på sekundet, med samme tidszoneviden på begge sider. Begge parsere accepterer lovlig reduceret præcision, så D:2026 og 2026 betyder begge 1. januar 2026, 00:00:00. Bærer begge værdier en zone, konverteres de til UTC før sammenligning: D:20260827093659+08'00' er lig 2026-08-27T01:36:59Z. Bærer ingen af dem en zone, sammenlignes de lokale komponenter, som de er skrevet. Har kun den ene side en zone, bliver resultatet pamsValueMismatch, for at opfinde et offset ville være et gæt. En ikke-nul brøkdel af et sekund, såsom .250 i XMP, tvinger også en mismatch frem, da en PDF-dato ikke kan udtrykke den, og stille at runde den væk ville skjule en reel uenighed; .000 accepteres. Uparsebare værdier rapporteres separat som pamsInvalidInfoDate eller pamsInvalidXmpDate

Hvordan PDFium Component beslutter, at en PDF Info-dato og en XMP-dato er ens i Delphi: to zoner konverteres til UTC og sammenligner tidspunkter, to zone-løse værdier sammenlignes som skrevet, én zone alene er en pamsValueMismatch, en ikke-nul brøkdel af et sekund kan ikke udtrykkes, og uparsebare værdier rapporteres separat
Lighed betyder samme tidspunkt på sekundet med samme tidszoneviden på begge sider, så at opfinde et offset eller runde en brøkdel af et sekund væk ville skjule en reel uenighed

Tilstedeværelse har sin egen regel. TPdfAMetadataValues.Present er et sæt fyldt ved at gennemløbe den aktive trailers /Info-dictionary, og det holder "nøgle fraværende" adskilt fra "nøgle til stede med en tom streng". En fraværende nøgle giver pamsNotRequired og kræver intet af XMP; /Title () er til stede, så XMP-pakken skal bære en tom x-default-titel også

Hvordan inspicerer man Info- og XMP-metadata før gemning?

TPdf.InspectPdfAMetadata returnerer en TPdfAMetadataReport med én TPdfAMetadataComparison pr. felt, hver med Info-værdien, XMP-værdien og en TPdfAMetadataState, så en fejl kan forklares uden at reverse-engineere et enkelt valideringsflag. MismatchFields opsummerer det fejlande sæt, HasXmpPacket fortæller dig, om en pakke blev fundet, og XmpParseError bærer parser-beskeden, når pakken eksisterer, men ikke kan læses

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;

Hvad ændrer NormalizePdfAMetadata, og hvad nægter den?

TPdf.NormalizePdfAMetadata behandler Info-dictionaryen som sandhedskilden og omskriver kun de XMP-egenskaber, hvis felt havnede i MismatchFields; alt andet i pakken overlever. Title og Subject skrives ind i x-default-itemet, mens andre sprog-alternativer forbliver intakte, Author bliver til en ét-item rdf:Seq, ukendte namespaces og urelaterede egenskaber bevares, og XMP-egenskaber for fraværende Info-nøgler efterlades urørt. En Info-dato med zone skrives som en kanonisk UTC XMP-dato med Z-suffiks; en zone-løs beholder sine lokale komponenter. Fil-overloaden gemmer gennem en midlertidig fil og en atomær erstatning, og selve XMP-opdateringen tilføjes som en inkrementel opdatering

Hvad NormalizePdfAMetadata omskriver ved reparation af PDF/A metadata i Delphi med PDFium Component: Info er sandhedskilden, kun MismatchFields-entries skrives tilbage som x-default Alt-tekst, en ét-item Seq eller en kanonisk UTC-dato, mens ukendte namespaces, urelaterede egenskaber og fraværende-nøgle-egenskaber overlever urørt
Reparationen nægter en manglende XMP-pakke, en misdannet Info-dato og signerede dokumenter, for at bygge et komplet PDF/A metadata-sæt er en opgave for SaveAsPdfA, ikke for en målrettet ækvivalensfix

Afvisningerne er bevidste. Uden en XMP-pakke rejser metoden EPdfError, for at bygge et komplet PDF/A-identifikations- og metadata-sæt er SaveAsPdfA's opgave, dækket i at skabe PDF/A arkivfiler med PDFium Component. En misdannet Info-dato rejser EPdfXmpError i stedet for at skrive en plausibel forkert værdi, og intet gemmes. Signerede dokumenter afvises, medmindre kalderen giver AllowSignedDocument = True med. Ækvivalens er også én regel i ISO 19005-1, så en normaliseret fil er ikke automatisk en konform en

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;                      // allerede konsistent, rør ikke filen
    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      // misdannet Info-dato eller ulæselig pakke
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

At køre sammenligningen på din egen XMP-pakke

ComparePdfAInfoAndXmp og SynchronizePdfAInfoToXmp er almindelige funktioner i FPdfPdfa, der virker på en TPdfXmpPacket uden dokument indlæst, hvilket passer til unit-teste og pipelines, der samler XMP fra en skabelon. Den ene fælde er Present: en record initialiseret med Default(TPdfAMetadataValues) har et tomt sæt, hvert felt rapporterer så pamsNotRequired, og sammenligningen består vakuant, uanset hvilke værdier du fyldte ind

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 beslutter, hvilke felter er obligatoriske; værdier alene ignoreres
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Arkiverer din pipeline dokumenter, som andre systemer vedbliver med at redigere, så par en natlig InspectPdfAMetadata-omgang med NormalizePdfAMetadata for de filer, der driver, og behold ValidatePdfA som gaten, før noget går til langtidsopbevaring. Den typede rapport, reparationsstien og resten af PDF/A-værktøjet leveres i PDFium Component for Delphi and C++Builder