Odborný článok

Ekvivalencia PDF/A Info a XMP metadát v Delphi

PDFium Component kontroluje ekvivalenciu metadát PDF/A Info na XMP cez TPdf.InspectPdfAMetadata a opravuje ju cez TPdf.NormalizePdfAMetadata. ISO 19005-1 (v znení opravy Cor.1) vyžaduje, aby každý z ôsmich mapovaných Info záznamov, od Title po ModDate, niesol tú istú hodnotu ako jeho XMP vlastnosť, nie len aby existoval; kontrola číta správny RDF tvar, páruje namespace podľa URI a porovnáva dátumy ako okamihy

Bug report, ktorý túto konverzáciu obyčajne otvára, pôsobí neškodne. Document management systém pečiatkuje nové /ModDate do Info slovníka pri každej inkrementálnej uložke, XMP packet necháva pokoji a o pol roka audit archívu označí tisíce súborov ako nekonformné. Obe dátumy tam sú. Len prestali sedieť pri prvej úprave a presence kontrola si to nikdy nevšimla. Rovnako zlyhajú úpravy Title cez API iba-Info a reťazec Author ako Finance; Controlling, ktorý nejaký nástroj rozdelil na dve položky dc:creator

Prečo PDF/A odmieta metadáta, ktoré existujú na oboch miestach?

PDF/A ich odmieta, pretože ISO 19005-1 §6.7.3 je pravidlo o hodnote, nie o prítomnosti: Tabuľka 1 mapuje ôsem Info kľúčov na XMP vlastnosti a hneď ako Info kľúč existuje, mapovaná XMP vlastnosť musí držať ekvivalentnú hodnotu. Bajtovo-úrovňový skener popísaný v PDF/A preflight validácii s PDFium Component len potvrdí, že xmp:CreateDate a xmp:ModifyDate existujú (pvaiMissingXmpDates). Od v3.72.0 navyše TPdf.ValidatePdfA púšťa úplné porovnanie hodnôt a pridáva pvaiInfoXmpValueMismatch do sady issue, keď XMP packet existuje, ale nesúhlasí s Info (packet, ktorý sa nedá sparsovať, počíta ako nesúhlasiaci). Chýbajúci packet zostáva hlásený ako pvaiMissingXmpMetadata, takže tie dva issue nikdy nepočítajú dvakrát ten istý defekt

Aký RDF tvar potrebuje každá mapovaná XMP vlastnosť?

Každé z ôsmich mapovaní má pevný XMP typ a správna hodnota v nesprávnom kontajneri stále zlyhá. ComparePdfAInfoAndXmp v FPdfPdfa.pas vyhľadáva vlastnosti podľa namespace URI, takže packet, ktorý viaže http://purl.org/dc/elements/1.1/ na nezvyčajný prefix, číta sa presne tak ako jeden používajúci dc. Vyžadované tvary sú:

  • Title → dc:title a Subject → dc:description: jazyková alternativa rdf:Alt, porovnávaná výhradne proti svojej položke x-default (language tag sa páruje case-insensitive); Alt bez x-default počíta ako chýbajúci
  • Author → dc:creator: rdf:Seq s presne jednou textovou položkou držiacou celý Info reťazec, takže zoznam autorov oddelený bodkočiarkami zostáva jediným záznamom
  • Keywords → pdf:Keywords a Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): jednoduché textové vlastnosti
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): jednoduché textové vlastnosti
Osem mapovaných párov, ktoré kontroluje ComparePdfAInfoAndXmp kvôli ekvivalencii metadát PDF/A v Delphi: Title a Subject potrebujú rdf:Alt s položkou x-default, Author jedno-položkový rdf:Seq, Keywords, Producer, Creator a obe dátumy sú jednoduchý text, každé vyhľadané podľa XMP namespace URI, nie podľa prefixu
Hneď ako Info kľúč existuje, ISO 19005-1 vyžaduje, aby mapovaná XMP vlastnosť držala ekvivalentnú hodnotu v požadovanom RDF tvare, takže hodnota v nesprávnom kontajneri stále zlyhá
<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>

Textové hodnoty sa porovnávajú ako presné postupnosti Unicode code pointov, bez trimovania, foldovania veľkosti písmen či normalizácie. Medzera na konci, alebo predkomponované é na jednej strane a dekomponované e plus kombinujúci akcent na druhej, je reálny mismatch. Strana Info vždy pochádza z vlastného dekódovania textu PDFDocEncoding a UTF-16 v PDFium cez FPDF_GetMetaText, čo bráni knižnici reimplementovať dekódovanie reťazcov a pokaziť to jemne; strana XMP je len taká čistá, ako bajty, ktoré ju vytvorili, a preto tu záleží aj na codepage pascoch, ktoré korumpujú XMP metadáta pod Free Pascalom

Kedy sú PDF dátum a XMP dátum rovnaké?

PDF dátum a XMP dátum sú rovnaké, keď popisujú ten istý okamih na sekundu presne, s tou istou znalosťou časového pásma na oboch stranách. Oba parsery akceptujú legálne zníženú presnosť, takže D:2026 aj 2026 znamenajú 1. január 2026, 00:00:00. Keď obe hodnoty nesú pásmo, konvertujú sa na UTC pred porovnaním: D:20260827093659+08'00' sa rovná 2026-08-27T01:36:59Z. Keď ani jedna pásmo nesie, porovnávajú sa lokálne komponenty tak, ako sú napísané. Keď len jedna strana má pásmo, výsledkom je pamsValueMismatch, pretože vymyslieť offset by bolo hádanie. Nenulová zlomková sekunda ako .250 v XMP tiež vnúti mismatch, keďže PDF dátum ju nedokáže vyjadriť a potiché zaokrúhľenie by schovalo reálny nesúhlas; .000 sa akceptuje. Neparsovateľné hodnoty sa hlásia oddelene ako pamsInvalidInfoDate alebo pamsInvalidXmpDate

Ako rozhoduje PDFium Component, že PDF Info dátum a XMP dátum sú rovnaké v Delphi: dve pásma konvertujú na UTC a porovnávajú okamihy, dve hodnoty bez pásma sa porovnávajú tak, ako sú napísané, samotné jedno pásmo je pamsValueMismatch, nenulová zlomková sekunda sa nedá vyjadriť a neparsovateľné hodnoty sa hlásia oddelene
Rovnosť znamená ten istý okamih na sekundu presne s tou istou znalosťou časového pásma na oboch stranách, takže vymyslený offset alebo zaokrúhlenie zlomkovej sekundy by schovalo reálny nesúhlas

Prítomnosť má vlastné pravidlo. TPdfAMetadataValues.Present je sada plnená prechádzaním slovníka /Info aktívneho traileru a drží „kľúč chýba“ odlišne od „kľúč existuje s prázdnym reťazcom“. Chýbajúci kľúč dáva pamsNotRequired a nič nepožaduje po XMP; /Title () existuje, takže XMP packet musí niesť aj prázdny titulok x-default

Ako prezriete Info a XMP metadáta pred uložením?

TPdf.InspectPdfAMetadata vracia TPdfAMetadataReport s jednou TPdfAMetadataComparison na pole, každá drží Info hodnotu, XMP hodnotu a TPdfAMetadataState, takže zlyhanie sa dá vysvetliť bez reverzného inžinierstva jediného validačného flagu. MismatchFields zhrnie zlyhávajúcu sadu, HasXmpPacket povedia, či sa packet našiel, a XmpParseError vozí hlásenie parsera, keď packet existuje, ale nedá sa čítať

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;

Čo mení NormalizePdfAMetadata a čo odmieta?

TPdf.NormalizePdfAMetadata berie Info slovník ako zdroj pravdy a prepisuje len XMP vlastnosti, ktorých pole pristalo v MismatchFields; všetko ostatné v packete prežije. Title a Subject sa zapisujú do položky x-default, kým ostatné jazykové alternatívy zostávajú nedotknuté, Author sa stane jedno-položkovým rdf:Seq, neznáme namespace a nesúvisiace vlastnosti sa zachovajú a XMP vlastnosti pre chýbajúce Info kľúče sa nechajú pokoji. Dátum Info s pásmom sa zapisuje ako kanonický UTC XMP dátum s príponou Z; ten bez pásma si drží lokálne komponenty. File overload ukladá cez dočasný súbor a atomickú náhradu a samotná XMP aktualizácia sa pripája ako inkrementálna aktualizácia

Čo prepisuje NormalizePdfAMetadata pri oprave metadát PDF/A v Delphi s PDFium Component: Info je zdroj pravdy, späť sa zapisujú len záznamy MismatchFields ako x-default Alt text, jedno-položkový Seq alebo kanonický UTC dátum, kým neznáme namespace, nesúvisiace vlastnosti a vlastnosti chýbajúcich kľúčov prežívajú nedotknuté
Oprava odmieta chýbajúci XMP packet, zdeformovaný Info dátum a podpísané dokumenty, pretože stavba kompletnej sady metadát PDF/A je práca SaveAsPdfA, nie cielená oprava ekvivalencie

Odmietnutia sú zámerné. Bez XMP packetu metóda vyhodí EPdfError, pretože stavba kompletnej PDF/A identifikácie a sady metadát je práca SaveAsPdfA, pokrytej v vytváraní PDF/A archivačných súborov s PDFium Component. Zdeformovaný Info dátum vyhodí EPdfXmpError namiesto zápisu vierohodne vyzerajúcej zlej hodnoty a nič sa neuloží. Podpísané dokumenty sa odmietajú, pokiaľ volajúci nepošle AllowSignedDocument = True. Ekvivalencia je jedna z pravidiel ISO 19005-1 tiež, takže normalizovaný súbor nie je automaticky konformný

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;                      // už konzistentné, nechajte súbor pokoji
    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      // zdeformovaný Info dátum alebo nečitateľný packet
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Pustenie porovnania na vlastnom XMP packete

ComparePdfAInfoAndXmp a SynchronizePdfAInfoToXmp sú holé funkcie v FPdfPdfa, ktoré pracujú na TPdfXmpPacket bez načítaného dokumentu, čo sedí unit testom a pipeline skladajúcim XMP zo šablóny. Jedna pasca je Present: record inicializovaný s Default(TPdfAMetadataValues) má prázdnu sadu, každé pole potom hlási pamsNotRequired a porovnanie prejde prázdne, nech ste vyplnili akékoľvek hodnoty

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 rozhoduje, ktoré polia sú povinné; samotné hodnoty sa ignorujú
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

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

Ak váš pipeline archivuje dokumenty, ktoré iné systémy neustále upravujú, spárujte nočný prechod InspectPdfAMetadata s NormalizePdfAMetadata pre súbory, ktoré odchýlia, a nechajte ValidatePdfA ako bránu skôr, než čokoľvek odíde do dlhodobého úložiska. Typovaný report, opravná cesta a zvyšok PDF/A náradia vozí PDFium Component for Delphi and C++Builder