Articol tehnic

Echivalența metadatelor Info și XMP PDF/A în Delphi

PDFium Component verifică echivalența metadatelor Info-versus-XMP PDF/A cu TPdf.InspectPdfAMetadata și o repară cu TPdf.NormalizePdfAMetadata. ISO 19005-1 (în forma corectată de Cor.1) cere fiecăreia dintre cele opt intrări Info mapate, de la Title până la ModDate, să poarte aceeași valoare ca proprietatea ei XMP, nu doar să existe; verificarea citește forma RDF corectă, potrivește namespace-urile după URI și compară datele ca instanțe

Raportul de bug care pornește de obicei discuția asta arată inofensiv. Un sistem de management al documentelor ștampilează un /ModDate nou în dicționarul Info la fiecare salvare incrementală, lasă pachetul XMP în pace, iar șase luni mai târziu un audit de arhivă marchează mii de fișiere ca neconforme. Ambele date sunt acolo. Au încetat doar să mai fie de acord la prima editare, iar o verificare de prezență nu a observat niciodată. Editările de titlu făcute printr-un API doar-Info și un șir Author precum Finance; Controlling pe care vreun instrument l-a împărțit în două elemente dc:creator eșuează la fel

De ce respinge PDF/A metadate care există în ambele locuri?

PDF/A o respinge pentru că ISO 19005-1 §6.7.3 este o regulă de valoare, nu de prezență: Tabelul 1 mapează opt chei Info către proprietăți XMP, iar odată ce o cheie Info este prezentă, proprietatea XMP mapată trebuie să țină o valoare echivalentă. Scannerul la nivel de byte descris în validarea de preflight PDF/A cu PDFium Component confirmă doar că xmp:CreateDate și xmp:ModifyDate există (pvaiMissingXmpDates). Din v3.72.0, TPdf.ValidatePdfA rulează în plus comparația completă de valori și adaugă pvaiInfoXmpValueMismatch la mulțimea de probleme când un pachet XMP există dar nu se află de acord cu Info (un pachet care nu poate fi parsat contează ca dezacord). Un pachet lipsă rămâne raportat ca pvaiMissingXmpMetadata, deci cele două probleme nu numără niciodată de două ori același defect

Ce formă RDF are nevoie fiecare proprietate XMP mapată?

Fiecare dintre cele opt mapări are un tip XMP fix, iar o valoare corectă în containerul greșit eșuează tot. ComparePdfAInfoAndXmp din FPdfPdfa.pas caută proprietățile după URI de namespace, deci un pachet care leagă http://purl.org/dc/elements/1.1/ de un prefix neobișnuit este citit exact ca unul care folosește dc. Formele cerute sunt:

  • Title → dc:title și Subject → dc:description: o alternativă de limbă rdf:Alt, comparată doar cu elementul ei x-default (tag-ul de limbă este potrivit fără distincție de majuscule); un Alt fără x-default contează ca lipsă
  • Author → dc:creator: un rdf:Seq cu exact un element de text care ține tot șirul Info, astfel încât o listă de autori separată prin punct și virgulă rămâne o singură intrare
  • Keywords → pdf:Keywords și Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): proprietăți de text simple
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): proprietăți de text simple
Cele opt perechi mapate pe care ComparePdfAInfoAndXmp le verifică pentru echivalența metadatelor PDF/A în Delphi: Title și Subject au nevoie de un rdf:Alt cu un element x-default, Author de un rdf:Seq cu un singur element, Keywords, Producer, Creator și cele două date sunt text simplu, fiecare căutat după URI de namespace XMP, nu după prefix
Odată ce o cheie Info este prezentă, ISO 19005-1 cere proprietății XMP mapate să țină o valoare echivalentă în forma RDF cerută, deci o valoare în containerul greșit eșuează tot
<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>

Valorile de text sunt comparate ca secvențe exacte de puncte de cod Unicode, fără trim, împăturire de majuscule sau normalizare. Un spațiu din coadă, sau un é precompus de o parte și un e descompus plus accent combinant de cealaltă, este un dezacord autentic. Partea Info vine întotdeauna din decodarea proprie a PDFium a textului PDFDocEncoding și UTF-16 prin FPDF_GetMetaText, ceea ce împiedică biblioteca să re-implementeze decodarea de șiruri și să o greșească subtil; partea XMP este curată doar cât byte-ii care au produs-o, motiv pentru care capcanele de codepage care corup metadatele XMP sub Free Pascal contează și aici

Când sunt egale o dată PDF și o dată XMP?

O dată PDF și o dată XMP sunt egale când descriu aceeași instanță până la secundă, cu aceeași cunoaștere de fus orar pe ambele părți. Ambele parsere acceptă precizie redusă legală, deci D:2026 și 2026 înseamnă ambele 1 ianuarie 2026, 00:00:00. Când ambele valori poartă un fus, ele sunt convertite la UTC înainte de comparație: D:20260827093659+08'00' este egal cu 2026-08-27T01:36:59Z. Când niciuna nu poartă un fus, componentele locale sunt comparate ca scrise. Când doar o parte are un fus, rezultatul este pamsValueMismatch, pentru că inventarea unui offset ar fi o ghicire. O fracțiune de secundă nenulă precum .250 în XMP forțează de asemenea un dezacord, pentru că o dată PDF nu are cum să o exprime, iar rotunjirea ei în tăcere ar ascunde un dezacord real; .000 este acceptat. Valorile neparsabile sunt raportate separat ca pamsInvalidInfoDate sau pamsInvalidXmpDate

Cum decide PDFium Component că o dată Info PDF și o dată XMP sunt egale în Delphi: două fusuri se convertesc la UTC și compară instanțele, două valori fără fus se compară ca scrise, un singur fus singuratic este un pamsValueMismatch, o fracțiune de secundă nenulă nu poate fi exprimată, iar valorile neparsabile sunt raportate separat
Egalitatea înseamnă aceeași instanță până la secundă cu aceeași cunoaștere de fus orar pe ambele părți, deci inventarea unui offset sau rotunjirea unei fracțiuni de secundă ar ascunde un dezacord real

Prezența are propria regulă. TPdfAMetadataValues.Present este o mulțime umplută parcurgând dicționarul /Info al trailer-ului activ, și ține „cheie lipsă" separat de „cheie prezentă cu șir gol". O cheie lipsă produce pamsNotRequired și nu cere nimic de la XMP; /Title () este prezent, deci pachetul XMP trebuie să poarte și el un titlu x-default gol

Cum inspectezi metadatele Info și XMP înainte de salvare?

TPdf.InspectPdfAMetadata întoarce un TPdfAMetadataReport cu câte un TPdfAMetadataComparison per câmp, fiecare ținând valoarea Info, valoarea XMP și un TPdfAMetadataState, astfel încât un eșec poate fi explicat fără inginerie inversă pe un singur flag de validare. MismatchFields rezumă mulțimea care eșuează, HasXmpPacket vă spune dacă a fost găsit un pachet, iar XmpParseError poartă mesajul parserului când pachetul există dar nu poate fi citit

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;

Ce schimbă NormalizePdfAMetadata și ce refuză?

TPdf.NormalizePdfAMetadata tratează dicționarul Info ca sursă de adevăr și rescrie doar proprietățile XMP al căror câmp a aterizat în MismatchFields; tot restul din pachet supraviețuiește. Title și Subject sunt scrise în elementul x-default în timp ce celelalte alternative de limbă rămân intacte, Author devine un rdf:Seq cu un singur element, namespace-urile necunoscute și proprietățile fără legătură sunt păstrate, iar proprietățile XMP pentru chei Info absente sunt lăsate neatinse. O dată Info cu fus este scrisă ca o dată XMP canonică UTC cu sufix Z; una fără fus își păstrează componentele locale. Overload-ul pe fișier salvează printr-un fișier temporar și o înlocuire atomică, iar actualizarea XMP în sine este adăugată ca actualizare incrementală

Ce rescrie NormalizePdfAMetadata când repară metadatele PDF/A în Delphi cu PDFium Component: Info este sursa de adevăr, doar intrările din MismatchFields sunt scrise înapoi ca text Alt x-default, un Seq cu un singur element sau o dată UTC canonică, în timp ce namespace-urile necunoscute, proprietățile fără legătură și proprietățile pentru chei absente supraviețuiesc neatinse
Reparația refuză un pachet XMP lipsă, o dată Info malformată și documentele semnate, pentru că construirea unui set complet de metadate PDF/A este treaba lui SaveAsPdfA, nu a unei reparări țintite de echivalență

Refuzurile sunt deliberate. Fără pachet XMP, metoda ridică EPdfError, pentru că construirea unui set complet de identificare și metadate PDF/A este treaba lui SaveAsPdfA, acoperită în crearea fișierelor de arhivă PDF/A cu PDFium Component. O dată Info malformată ridică EPdfXmpError în loc să scrie o valoare greșită cu aspect plauzibil, și nu se salvează nimic. Documentele semnate sunt respinse dacă apelantul nu pasa AllowSignedDocument = True. Echivalența este și ea o regulă a ISO 19005-1, deci un fișier normalizat nu este automat unul conform

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;                      // deja consistent, lasă fișierul în pace
    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      // dată Info malformată sau pachet necitibil
        raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
    end;
  finally
    Pdf.Free;
  end;
end;

Rularea comparației pe propriul pachet XMP

ComparePdfAInfoAndXmp și SynchronizePdfAInfoToXmp sunt funcții simple în FPdfPdfa care lucrează pe un TPdfXmpPacket fără niciun document încărcat, ceea ce li se potrivește testelor unitare și pipeline-urilor care asamblează XMP dintr-un șablon. Capcana cea una este Present: un record inițializat cu Default(TPdfAMetadataValues) are o mulțime goală, fiecare câmp raportează apoi pamsNotRequired, iar comparația trece vacuu indiferent ce valori ați umplut

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 decide care câmpuri sunt obligatorii; valorile singure sunt ignorate
  Info.Present := [pamfTitle, pamfAuthor, pamfModDate];

  Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
  try
    Changed := SynchronizePdfAInfoToXmp(Info, Packet);
    // xmp:ModifyDate este acum 2026-08-27T01:36:59Z, dc:creator un rdf:Seq cu un singur element
    if Changed <> [] then
      TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
  finally
    Packet.Free;
  end;
end;

Dacă pipeline-ul dvs. arhivează documente pe care alte sisteme continuă să le editeze, cuplați o trecere nocturnă InspectPdfAMetadata cu NormalizePdfAMetadata pentru fișierele care alunecă, și păstrați ValidatePdfA ca poartă înainte ca orice să plece spre stocare pe termen lung. Raportul tipizat, calea de reparare și restul uneltelor PDF/A se livrează în PDFium Component pentru Delphi și C++Builder