Teknisk artikel

PDF/A Info- och XMP-metadataekvivalens i Delphi

PDFium Component kontrollerar PDF/A Info-till-XMP-metadataekvivalens med TPdf.InspectPdfAMetadata och lagar den med TPdf.NormalizePdfAMetadata. ISO 19005-1 (som korrigerats av Cor.1) kräver att vart och ett av de åtta mappade Info-fälten, Title till ModDate, bär samma värde som sin XMP-egenskap, inte bara att det finns; kontrollen läser den korrekta RDF-formen, matchar namespaces efter URI och jämför datum som tidpunkter

Felrapporten som brukar inleda det här samtalet ser ofarlig ut. Ett dokumenthanteringssystem stämplar en ny /ModDate i Info-ordboken vid varje inkrementell sparning, lämnar XMP-paketet orört, och ett halvår senare flaggar en arkivgranskning tusentals filer som inkonforma. Båda datumen finns där. De slutade bara instämma vid den första redigeringen, och en närvarokontroll märkte aldrig det. Titelredigeringar gjorda via ett API som bara når Info, och en Author-sträng som Finance; Controlling som något verktyg delade i två dc:creator-poster, faller på samma sätt

Varför avvisar PDF/A metadata som finns på båda ställena?

PDF/A avvisar den för att ISO 19005-1 §6.7.3 är en värdesregel, inte en närvaroregel: Tabell 1 mappar åtta Info-nycklar till XMP-egenskaper, och när en Info-nyckel väl finns måste den mappade XMP-egenskapen hålla ett ekvivalent värde. Den bytenivåskanner som beskrivs i PDF/A-preflightvalidering med PDFium Component bekräftar bara att xmp:CreateDate och xmp:ModifyDate finns (pvaiMissingXmpDates). Sedan v3.72.0 kör TPdf.ValidatePdfA dessutom hela värdesjämförelsen och lägger till pvaiInfoXmpValueMismatch i problemuppsättningen när ett XMP-paket finns men motsäger Info (ett paket som inte går att parsa räknas som motsägande). Ett saknat paket fortsätter rapporteras som pvaiMissingXmpMetadata, så de två problemen räknar aldrig dubbelt för samma defekt

Vilken RDF-form kräver varje mappad XMP-egenskap?

Vart och ett av de åtta mappningarna har en fast XMP-typ, och ett korrekt värde i fel behållare faller fortfarande. ComparePdfAInfoAndXmp i FPdfPdfa.pas slår upp egenskaper efter namespace-URI, så ett paket som binder http://purl.org/dc/elements/1.1/ till ett ovanligt prefix läses exakt som ett som använder dc. De krävda formerna är:

  • Title → dc:title och Subject → dc:description: ett rdf:Alt-språkalternativ, jämfört bara mot sin x-default-post (språktaggen matchas skiftlägesokänsligt); en Alt utan x-default räknas som saknad
  • Author → dc:creator: en rdf:Seq med exakt ett textobjekt som håller hela Info-strängen, så en semikolonseparerad författarlista förblir en enda post
  • Keywords → pdf:Keywords och Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): enkla textegenskaper
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): enkla textegenskaper
De åtta mappade paren som ComparePdfAInfoAndXmp kontrollerar för PDF/A-metadataekvivalens i Delphi: Title och Subject behöver en rdf:Alt med en x-default-post, Author en Seq med en post, Keywords, Producer, Creator och de två datumen är enkel text, vart och ett uppslaget efter XMP-namespace-URI i stället för prefix
När en Info-nyckel väl finns kräver ISO 19005-1 att den mappade XMP-egenskapen håller ett ekvivalent värde i den krävda RDF-formen, så ett värde i fel behållare faller fortfarande
<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>

Textvärden jämförs som exakta Unicode-kodpunktssekvenser, utan trimning, skiftlägesomvandling eller normalisering. Ett avslutande mellanslag, eller ett prekomponerat é på ena sidan och ett dekomponerat e plus kombinerande accent på den andra, är en äkta mismatch. Info-sidan kommer alltid från PDFiums egen avkodning av PDFDocEncoding och UTF-16-text genom FPDF_GetMetaText, vilket hindrar biblioteket från att omimplementera strängavkodning och få den subtilt fel; XMP-sidan är bara så ren som de byte som producerade den, vilket är varför kodsidfallorna som korrumperar XMP-metadata under Free Pascal spelar roll här också

När är ett PDF-datum och ett XMP-datum lika?

Ett PDF-datum och ett XMP-datum är lika när de beskriver samma tidpunkt med sekundnoggrannhet, med samma tidszonskunskap på båda sidor. Båda parserna accepterar laglig reducerad precision, så D:2026 och 2026 betyder båda 1 januari 2026, 00:00:00. När båda värdena bär en zon konverteras de till UTC före jämförelsen: D:20260827093659+08'00' är lika med 2026-08-27T01:36:59Z. När ingen av dem bär en zon jämförs de lokala komponenterna som skrivna. När bara ena sidan har en zon blir resultatet pamsValueMismatch, för att hitta på en offset vore en gissning. En icke-noll bråkdels sekund som .250 i XMP tvingar också fram en mismatch, eftersom ett PDF-datum inte har något sätt att uttrycka den och att tyst avrunda bort den skulle dölja en verklig oenighet; .000 accepteras. Värden som inte går att parsa rapporteras separat som pamsInvalidInfoDate eller pamsInvalidXmpDate

Hur PDFium Component avgör att ett PDF Info-datum och ett XMP-datum är lika i Delphi: två zoner konverteras till UTC och jämför tidpunkter, två zonlösa värden jämförs som skrivna, en ensam zon är en pamsValueMismatch, en icke-noll bråkdels sekund kan inte uttryckas, och oparsbara värden rapporteras separat
Likahet betyder samma tidpunkt med sekundnoggrannhet och samma tidszonskunskap på båda sidor, så att hitta på en offset eller avrunda bort en bråkdels sekund skulle dölja en verklig oenighet

Närvaro har sin egen regel. TPdfAMetadataValues.Present är en mängd som fylls genom att gå igenom den aktiva trailerns /Info-ordbok, och den håller "nyckel saknas" isär från "nyckel finns med tom sträng". En saknad nyckel ger pamsNotRequired och kräver ingenting av XMP; /Title () finns, så XMP-paketet måste bära en tom x-default-titel också

Hur inspekterar du Info- och XMP-metadata innan sparning?

TPdf.InspectPdfAMetadata returnerar en TPdfAMetadataReport med en TPdfAMetadataComparison per fält, vart och ett med Info-värdet, XMP-värdet och en TPdfAMetadataState, så ett misslyckande kan förklaras utan att baklängeskonstruera en enda valideringsflagga. MismatchFields sammanfattar den fallande uppsättningen, HasXmpPacket talar om huruvida ett paket hittades, och XmpParseError bär parsermeddelandet när paketet finns men inte kan läsas

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;

Vad ändrar NormalizePdfAMetadata, och vad vägrar det?

TPdf.NormalizePdfAMetadata behandlar Info-ordboken som sanningskällan och skriver bara om de XMP-egenskaper vars fält hamnade i MismatchFields; allt annat i paketet överlever. Title och Subject skrivs in i x-default-posten medan andra språkalternativ lämnas intakta, Author blir en rdf:Seq med en post, okända namespaces och orelaterade egenskaper bevaras, och XMP-egenskaper för saknade Info-nycklar lämnas orörda. Ett Info-datum med zon skrivs som ett kanoniskt UTC XMP-datum med suffixet Z; ett zonlöst behåller sina lokala komponenter. Filöverlagringen sparar via en tillfällig fil och en atomär ersättning, och XMP-uppdateringen själv läggs till som en inkrementell uppdatering

Vad NormalizePdfAMetadata skriver om när PDF/A-metadata lagas i Delphi med PDFium Component: Info är sanningskällan, bara MismatchFields-poster skrivs tillbaka som x-default Alt-text, en Seq med en post eller ett kanoniskt UTC-datum, medan okända namespaces, orelaterade egenskaper och egenskaper för saknade nycklar överlever orörda
Reparationen vägrar ett saknat XMP-paket, ett felformat Info-datum och undertecknade dokument, för att bygga en komplett PDF/A-metadatamängd är SaveAsPdfA:s jobb, inte en riktad ekvivalensfix

Vägrarna är medvetna. Utan XMP-paket kastar metoden EPdfError, för att bygga en komplett PDF/A-identifierings- och metadatamängd är SaveAsPdfA:s jobb, som tas upp i att skapa PDF/A-arkivfiler med PDFium Component. Ett felformat Info-datum kastar EPdfXmpError i stället för att skriva ett felaktigt värde som ser trovärdigt ut, och ingenting sparas. Undertecknade dokument avvisas om anroparen inte skickar AllowSignedDocument = True. Ekvivalens är för övrigt en av ISO 19005-1:s regler, så en normaliserad fil är inte automatiskt en konform sådan

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;                      // redan konsistent, lämna filen i fred
    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      // felformat Info-datum eller oläsligt paket
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Köra jämförelsen på ditt eget XMP-paket

ComparePdfAInfoAndXmp och SynchronizePdfAInfoToXmp är vanliga funktioner i FPdfPdfa som arbetar på ett TPdfXmpPacket utan något laddat dokument, vilket passar enhetstester och pipelines som bygger XMP från en mall. Den enda fällan är Present: en post initierad med Default(TPdfAMetadataValues) har en tom mängd, varje fält rapporterar då pamsNotRequired, och jämförelsen går igenom vakant oavsett vilka värden du fyllt i

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 avgör vilka fält som är obligatoriska; värden ensamma ignoreras
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Om din pipeline arkiverar dokument som andra system fortsätter redigera, para ihop en nattlig InspectPdfAMetadata-svepning med NormalizePdfAMetadata för filerna som driver, och behåll ValidatePdfA som grinden innan något lämnar för långtidsförvaring. Den typade rapporten, reparationsvägen och resten av PDF/A-verktygen levereras i PDFium Component for Delphi and C++Builder