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 eix-default(tag-ul de limbă este potrivit fără distincție de majuscule); un Alt fărăx-defaultcontează ca lipsă - Author →
dc:creator: unrdf:Seqcu 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(namespacehttp://ns.adobe.com/pdf/1.3/): proprietăți de text simple - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): proprietăți de text simple
<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
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ă
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