PDFium Component vérifie l'équivalence entre Info et métadonnées XMP PDF/A avec TPdf.InspectPdfAMetadata et la répare avec TPdf.NormalizePdfAMetadata. L'ISO 19005-1 (telle que corrigée par Cor.1) exige que chacune des huit entrées Info mappées, de Title à ModDate, porte la même valeur que sa propriété XMP, et pas simplement qu'elle existe ; le contrôle lit la forme RDF correcte, apparie les namespaces par URI et compare les dates comme des instants
Le rapport de bug qui ouvre d'ordinaire cette conversation a l'air inoffensif. Un système de gestion documentaire estampille un nouveau /ModDate dans le dictionnaire Info à chaque sauvegarde incrémentale, laisse le paquet XMP tranquille, et six mois plus tard un audit d'archives signale des milliers de fichiers comme non conformes. Les deux dates sont là. Elles ont simplement cessé d'être d'accord à la première édition, et un contrôle de présence ne l'a jamais remarqué. Des éditions de Title passées par une API Info seule, et une chaîne Author comme Finance; Controlling que certains outils scindent en deux éléments dc:creator, échouent de la même façon
Pourquoi PDF/A rejette-t-il des métadonnées présentes aux deux endroits ?
PDF/A les rejette parce que l'ISO 19005-1 §6.7.3 est une règle de valeur, pas une règle de présence : le Tableau 1 mappe huit clés Info vers des propriétés XMP, et dès qu'une clé Info est présente, la propriété XMP mappée doit porter une valeur équivalente. Le scanner au niveau octet décrit dans la validation preflight PDF/A avec PDFium Component ne confirme que xmp:CreateDate et xmp:ModifyDate existent (pvaiMissingXmpDates). Depuis la v3.72.0, TPdf.ValidatePdfA exécute en plus la comparaison complète des valeurs et ajoute pvaiInfoXmpValueMismatch à l'ensemble de problèmes quand un paquet XMP existe mais diverge d'Info (un paquet impossible à parser compte comme divergent). Un paquet absent reste signalé comme pvaiMissingXmpMetadata, si bien que les deux problèmes ne comptent jamais deux fois le même défaut
Quelle forme RDF chaque propriété XMP mappée exige-t-elle ?
Chacun des huit mappages a un type XMP fixe, et une valeur correcte dans le mauvais conteneur échoue quand même. ComparePdfAInfoAndXmp dans FPdfPdfa.pas recherche les propriétés par URI de namespace, si bien qu'un paquet qui lie http://purl.org/dc/elements/1.1/ à un préfixe inhabituel se lit exactement comme un paquet utilisant dc. Les formes exigées sont :
- Title →
dc:titleet Subject →dc:description: une alternative de languerdf:Alt, comparée uniquement à son élémentx-default(le tag de langue est apparié sans tenir compte de la casse) ; un Alt sansx-defaultcompte comme absent - Author →
dc:creator: unrdf:Seqavec exactement un élément texte portant toute la chaîne Info, si bien qu'une liste d'auteurs séparés par des points-virgules reste une entrée unique - Keywords →
pdf:Keywordset Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/) : propriétés texte simples - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/) : propriétés texte simples
<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>
Les valeurs texte sont comparées comme des séquences exactes de points de code Unicode, sans retrait d'espaces, sans mise en casse ni normalisation. Un espace final, ou un é précomposé d'un côté et un e décomposé plus accent combinatoire de l'autre, est une vraie divergence. Le côté Info vient toujours du décodage maison de PDFium du texte PDFDocEncoding et UTF-16 via FPDF_GetMetaText, ce qui évite à la bibliothèque de réimplémenter le décodage de chaînes et de se tromper subtilement ; le côté XMP n'est propre que comme les octets qui l'ont produit, et c'est pourquoi les pièges de pages de code qui corrompent les métadonnées XMP sous Free Pascal comptent ici aussi
Quand une date PDF et une date XMP sont-elles égales ?
Une date PDF et une date XMP sont égales quand elles décrivent le même instant à la seconde près, avec la même connaissance de fuseau horaire des deux côtés. Les deux analyseurs acceptent une précision réduite légale, donc D:2026 et 2026 signifient tous deux le 1er janvier 2026, 00:00:00. Quand les deux valeurs portent un fuseau, elles sont converties en UTC avant comparaison : D:20260827093659+08'00' égale 2026-08-27T01:36:59Z. Quand aucune ne porte de fuseau, les composantes locales sont comparées telles qu'écrites. Quand un seul côté a un fuseau, le résultat est pamsValueMismatch, parce qu'inventer un décalage serait une devinette. Une fraction de seconde non nulle comme .250 dans XMP force elle aussi une divergence, puisqu'une date PDF ne peut pas l'exprimer et que l'arrondir en silence cacherait un vrai désaccord ; .000 est accepté. Les valeurs impossibles à parser sont signalées séparément comme pamsInvalidInfoDate ou pamsInvalidXmpDate
La présence a sa propre règle. TPdfAMetadataValues.Present est un ensemble rempli en parcourant le dictionnaire /Info du trailer actif, et il distingue une clé absente d'une clé présente avec une chaîne vide. Une clé absente donne pamsNotRequired et n'exige rien de XMP ; /Title () est présent, donc le paquet XMP doit porter aussi un titre x-default vide
Comment inspecter les métadonnées Info et XMP avant sauvegarde ?
TPdf.InspectPdfAMetadata renvoie un TPdfAMetadataReport avec un TPdfAMetadataComparison par champ, chacun portant la valeur Info, la valeur XMP et un TPdfAMetadataState, si bien qu'un échec peut être expliqué sans faire de l'ingénierie inverse sur un drapeau de validation unique. MismatchFields résume l'ensemble en échec, HasXmpPacket dit si un paquet a été trouvé, et XmpParseError transporte le message de l'analyseur quand le paquet existe mais ne peut pas être lu
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;
Que change NormalizePdfAMetadata, et à quoi refuse-t-il de toucher ?
TPdf.NormalizePdfAMetadata traite le dictionnaire Info comme source de vérité et ne réécrit que les propriétés XMP dont le champ a atterri dans MismatchFields ; tout le reste du paquet survit. Title et Subject sont écrits dans l'élément x-default tandis que les autres alternatives de langue restent intactes, Author devient un rdf:Seq à un élément, les namespaces inconnus et les propriétés sans rapport sont préservés, et les propriétés XMP des clés Info absentes ne sont pas touchées. Une date Info avec fuseau est écrite comme date XMP UTC canonique avec suffixe Z ; une date sans fuseau garde ses composantes locales. La surcharge fichier sauvegarde via un fichier temporaire et un remplacement atomique, et la mise à jour XMP elle-même est ajoutée comme mise à jour incrémentale
Les refus sont délibérés. Sans paquet XMP, la méthode lève EPdfError, parce que construire un jeu complet d'identification et de métadonnées PDF/A est le travail de SaveAsPdfA, couvert dans la création de fichiers d'archivage PDF/A avec PDFium Component. Une date Info mal formée lève EPdfXmpError au lieu d'écrire une valeur fausse d'apparence plausible, et rien n'est sauvegardé. Les documents signés sont rejetés sauf si l'appelant passe AllowSignedDocument = True. L'équivalence est aussi une règle de l'ISO 19005-1, si bien qu'un fichier normalisé n'est pas automatiquement un fichier 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; // déjà cohérent, laisser le fichier tranquille
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 // date Info mal formée ou paquet illisible
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Exécuter la comparaison sur votre propre paquet XMP
ComparePdfAInfoAndXmp et SynchronizePdfAInfoToXmp sont des fonctions ordinaires de FPdfPdfa qui travaillent sur un TPdfXmpPacket sans document chargé, ce qui convient aux tests unitaires et aux pipelines qui assemblent du XMP depuis un gabarit. Le seul piège est Present : un enregistrement initialisé avec Default(TPdfAMetadataValues) a un ensemble vide, chaque champ rapporte alors pamsNotRequired, et la comparaison passe de façon creuse quelles que soient les valeurs que vous avez remplies
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 décide quels champs sont obligatoires ; les valeurs seules sont ignorées
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate vaut désormais 2026-08-27T01:36:59Z, dc:creator un rdf:Seq à un élément
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Si votre pipeline archive des documents que d'autres systèmes continuent d'éditer, associez un balayage nocturne InspectPdfAMetadata à NormalizePdfAMetadata pour les fichiers qui dérivent, et gardez ValidatePdfA comme verrou avant que quoi que ce soit ne parte vers le stockage à long terme. Le rapport typé, le chemin de réparation et le reste de l'outillage PDF/A sont livrés dans le PDFium Component pour Delphi et C++Builder