PDFium Component verifica l'equivalenza Info-XMP dei metadati PDF/A con TPdf.InspectPdfAMetadata e la ripara con TPdf.NormalizePdfAMetadata. ISO 19005-1 (come corretta da Cor.1) richiede che ciascuna delle otto voci Info mappate, da Title a ModDate, porti lo stesso valore della sua proprietà XMP, non semplicemente che esista; il controllo legge la forma RDF corretta, abbina i namespace per URI e confronta le date come istanti
Il bug report che di solito apre questa conversazione sembra innocuo. Un sistema di gestione documentale timbra un nuovo /ModDate nel dizionario Info a ogni salvataggio incrementale, lascia in pace il pacchetto XMP, e sei mesi dopo un audit d'archivio segnala migliaia di file come non conformi. Entrambe le date ci sono. Hanno solo smesso di concordare alla prima modifica, e un controllo di presenza non se n'è mai accorto. Modifiche al Title fatte tramite una API solo-Info, e una stringa Author come Finance; Controlling che qualche strumento ha spezzato in due voci dc:creator, falliscono allo stesso modo
Perché PDF/A respinge metadati che esistono in entrambi i posti?
PDF/A la respinge perché ISO 19005-1 §6.7.3 è una regola sul valore, non sulla presenza: la Tabella 1 mappa otto chiavi Info su proprietà XMP, e una volta che una chiave Info è presente, la proprietà XMP mappata deve contenere un valore equivalente. Lo scanner a livello di byte descritto nella validazione preflight PDF/A con PDFium Component conferma solo che xmp:CreateDate e xmp:ModifyDate esistano (pvaiMissingXmpDates). Dalla v3.72.0, TPdf.ValidatePdfA esegue inoltre il confronto completo dei valori e aggiunge pvaiInfoXmpValueMismatch all'insieme dei problemi quando un pacchetto XMP esiste ma è in disaccordo con Info (un pacchetto non parsabile conta come in disaccordo). Un pacchetto assente resta segnalato come pvaiMissingXmpMetadata, così i due problemi non contano mai due volte lo stesso difetto
Quale forma RDF richiede ciascuna proprietà XMP mappata?
Ciascuna delle otto mappature ha un tipo XMP fisso, e un valore corretto nel contenitore sbagliato fallisce comunque. ComparePdfAInfoAndXmp in FPdfPdfa.pas cerca le proprietà per URI di namespace, così un pacchetto che lega http://purl.org/dc/elements/1.1/ a un prefisso insolito viene letto esattamente come uno che usa dc. Le forme richieste sono:
- Title →
dc:titlee Subject →dc:description: un'alternativa di linguardf:Alt, confrontata solo con la sua vocex-default(il tag di lingua è confrontato senza distinguere maiuscole); un Alt senzax-defaultconta come assente - Author →
dc:creator: unrdf:Seqcon esattamente una voce di testo che contiene l'intera stringa Info, così un elenco di autori separati da punto e virgola resta una voce unica - Keywords →
pdf:Keywordse Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): proprietà di testo semplici - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): proprietà di testo semplici
<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>
I valori di testo vengono confrontati come sequenze esatte di code point Unicode, senza trimming, folding di maiuscole o normalizzazione. Uno spazio in coda, oppure una é precomposta da un lato e una e decomposta con accento combinato dall'altro, è un disaccordo autentico. Il lato Info viene sempre dalla decodifica di PDFium stessa del testo PDFDocEncoding e UTF-16 tramite FPDF_GetMetaText, il che evita alla libreria di re-implementare la decodifica delle stringhe e sbagliarla in modo sottile; il lato XMP è pulito solo quanto i byte che lo hanno prodotto, ed è per questo che le trappole di codepage che corrompono i metadati XMP sotto Free Pascal contano anche qui
Quando una data PDF e una data XMP sono uguali?
Una data PDF e una data XMP sono uguali quando descrivono lo stesso istante al secondo, con la stessa conoscenza del fuso orario su entrambi i lati. Entrambi i parser accettano la precisione ridotta lecita, quindi D:2026 e 2026 significano entrambi 1 gennaio 2026, 00:00:00. Quando entrambi i valori portano un fuso, vengono convertiti in UTC prima del confronto: D:20260827093659+08'00' equivale a 2026-08-27T01:36:59Z. Quando nessuno dei due porta un fuso, le componenti locali vengono confrontate così come scritte. Quando solo un lato ha un fuso, il risultato è pamsValueMismatch, perché inventarsi un offset sarebbe un'interpretazione. Un secondo frazionario non nullo come .250 in XMP forza anch'esso un disaccordo, visto che una data PDF non ha modo di esprimerlo e arrotondarlo via in silenzio nasconderebbe un disaccordo reale; .000 è accettato. I valori non parsabili vengono segnalati separatamente come pamsInvalidInfoDate o pamsInvalidXmpDate
La presenza ha la sua regola. TPdfAMetadataValues.Present è un insieme riempito percorrendo il dizionario /Info del trailer attivo, e tiene separato "chiave assente" da "chiave presente con stringa vuota". Una chiave assente dà pamsNotRequired e non pretende nulla da XMP; /Title () è presente, quindi il pacchetto XMP deve portare anche un titolo x-default vuoto
Come si ispezionano i metadati Info e XMP prima del salvataggio?
TPdf.InspectPdfAMetadata restituisce un TPdfAMetadataReport con un TPdfAMetadataComparison per campo, ciascuno con il valore Info, il valore XMP e uno TPdfAMetadataState, così un fallimento si può spiegare senza fare reverse engineering di un singolo flag di validazione. MismatchFields riassume l'insieme dei campi che falliscono, HasXmpPacket dice se un pacchetto è stato trovato, e XmpParseError porta il messaggio del parser quando il pacchetto esiste ma non si può leggere
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;
Che cosa cambia NormalizePdfAMetadata, e che cosa rifiuta?
TPdf.NormalizePdfAMetadata tratta il dizionario Info come fonte di verità e riscrive solo le proprietà XMP il cui campo è finito in MismatchFields; tutto il resto del pacchetto sopravvive. Title e Subject vengono scritti nella voce x-default mentre le altre alternative di lingua restano intatte, Author diventa un rdf:Seq a una voce, i namespace sconosciuti e le proprietà non correlate vengono preservati, e le proprietà XMP per chiavi Info assenti restano intoccate. Una data Info con fuso viene scritta come data XMP UTC canonica con suffisso Z; una senza fuso conserva le sue componenti locali. L'overload su file salva attraverso un file temporaneo e una sostituzione atomica, e l'aggiornamento XMP in sé viene accodato come aggiornamento incrementale
I rifiuti sono deliberati. Senza pacchetto XMP il metodo solleva EPdfError, perché costruire un insieme completo di identificazione e metadati PDF/A è compito di SaveAsPdfA, trattato in creare file PDF/A di archiviazione con PDFium Component. Una data Info malformata solleva EPdfXmpError invece di scrivere un valore sbagliato dall'aspetto plausibile, e nulla viene salvato. I documenti firmati vengono respinti a meno che il chiamante non passi AllowSignedDocument = True. Anche l'equivalenza è una regola di ISO 19005-1, quindi un file normalizzato non è automaticamente uno conforme
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; // già coerente, lasciate stare il file
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 // data Info malformata o pacchetto illeggibile
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Eseguire il confronto sul vostro pacchetto XMP
ComparePdfAInfoAndXmp e SynchronizePdfAInfoToXmp sono funzioni semplici in FPdfPdfa che lavorano su un TPdfXmpPacket senza alcun documento caricato, il che sta bene ai test unitari e alle pipeline che assemblano XMP da un template. L'unica trappola è Present: un record inizializzato con Default(TPdfAMetadataValues) ha un insieme vuoto, ogni campo allora riporta pamsNotRequired, e il confronto passa vacuo qualunque valore abbiate riempito
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 quali campi sono obbligatori; i valori da soli vengono ignorati
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate ora è 2026-08-27T01:36:59Z, dc:creator un rdf:Seq a una voce
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Se la vostra pipeline archivia documenti che altri sistemi continuano a modificare, abbinate una passata notturna di InspectPdfAMetadata a NormalizePdfAMetadata per i file che derivano, e tenete ValidatePdfA come cancello prima che qualcosa parta per l'archiviazione a lungo termine. Il report tipizzato, il percorso di riparazione e il resto degli strumenti PDF/A viaggiano nel PDFium Component per Delphi e C++Builder