PDFium Component verifica la equivalencia de metadatos Info-XMP de PDF/A con TPdf.InspectPdfAMetadata y la repara con TPdf.NormalizePdfAMetadata. ISO 19005-1 (corregida por Cor.1) exige que cada una de las ocho entradas Info mapeadas, de Title a ModDate, lleve el mismo valor que su propiedad XMP, y no solo que exista; el chequeo lee la forma RDF correcta, empareja namespaces por URI y compara fechas como instantes
El reporte de defecto que suele arrancar esta conversación parece inofensivo. Un sistema de gestión documental estampa un nuevo /ModDate en el diccionario Info en cada guardado incremental, deja el packet XMP quieto, y seis meses después una auditoría de archivo marca miles de archivos como no conformes. Ambas fechas están. Dejaron de coincidir en la primera edición, y un chequeo de presencia nunca lo notó. Las ediciones de Title hechas por una API solo-Info, y un string Author como Finance; Controlling que alguna herramienta partió en dos items dc:creator, fallan igual
¿Por qué PDF/A rechaza metadatos que existen en ambos lugares?
PDF/A lo rechaza porque ISO 19005-1 §6.7.3 es una regla de valor, no de presencia: la Tabla 1 mapea ocho claves Info a propiedades XMP, y una vez que una clave Info está presente, la propiedad XMP mapeada debe cargar un valor equivalente. El escáner a nivel de bytes descrito en validación preflight PDF/A con PDFium Component solo confirma que xmp:CreateDate y xmp:ModifyDate existen (pvaiMissingXmpDates). Desde la v3.72.0, TPdf.ValidatePdfA además corre la comparación de valores completa y agrega pvaiInfoXmpValueMismatch al set de issues cuando existe un packet XMP pero discrepa con Info (un packet que no se puede parsear cuenta como discrepancia). Un packet ausente sigue reportado como pvaiMissingXmpMetadata, así que los dos issues jamás cuentan dos veces el mismo defecto
¿Qué forma RDF necesita cada propiedad XMP mapeada?
Cada uno de los ocho mapeos tiene un tipo XMP fijo, y un valor correcto en el contenedor equivocado igual falla. ComparePdfAInfoAndXmp en FPdfPdfa.pas busca las propiedades por URI de namespace, así que un packet que liga http://purl.org/dc/elements/1.1/ a un prefijo inusual se lee exactamente igual que uno que usa dc. Las formas requeridas son:
- Title →
dc:titley Subject →dc:description: una alternativa de idiomardf:Alt, comparada contra su itemx-defaultúnicamente (el tag de idioma se empareja sin distinguir mayúsculas); un Alt sinx-defaultcuenta como ausente - Author →
dc:creator: unrdf:Seqcon exactamente un item de texto que carga el string Info completo, de modo que una lista de autores separada por punto y coma queda como una única entrada - Keywords →
pdf:Keywordsy Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): propiedades de texto simples - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): propiedades de texto 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>
Los valores de texto se comparan como secuencias exactas de code points Unicode, sin recorte, sin plegado de mayúsculas y sin normalización. Un espacio al final, o una é precompuesta de un lado y una e descompuesta más acento combinante del otro, es un desacuerdo genuino. El lado Info siempre viene de la decodificación propia de PDFium de texto PDFDocEncoding y UTF-16 a través de FPDF_GetMetaText, lo que evita que la librería re-implemente la decodificación de strings y se equivoque de forma sutil; el lado XMP está tan limpio como los bytes que lo produjeron, por eso las trampas de codepage que corrompen metadatos XMP bajo Free Pascal también importan acá
¿Cuándo son iguales una fecha PDF y una fecha XMP?
Una fecha PDF y una fecha XMP son iguales cuando describen el mismo instante al segundo, con el mismo conocimiento de zona horaria de ambos lados. Ambos parsers aceptan precisión reducida legal, así que D:2026 y 2026 significan ambos 1 de enero de 2026, 00:00:00. Cuando ambos valores cargan zona, se convierten a UTC antes de comparar: D:20260827093659+08'00' es igual a 2026-08-27T01:36:59Z. Cuando ninguno carga zona, los componentes locales se comparan tal como están escritos. Cuando solo un lado tiene zona, el resultado es pamsValueMismatch, porque inventar un offset sería adivinar. Un segundo fraccionario distinto de cero como .250 en XMP también fuerza un desacuerdo, ya que una fecha PDF no tiene forma de expresarlo y redondearlo en silencio escondería un desacuerdo real; .000 se acepta. Los valores no parseables se reportan por separado como pamsInvalidInfoDate o pamsInvalidXmpDate
La presencia tiene su propia regla. TPdfAMetadataValues.Present es un set que se llena recorriendo el diccionario /Info del trailer activo, y mantiene separado «clave ausente» de «clave presente con string vacío». Una clave ausente da pamsNotRequired y no exige nada al XMP; /Title () está presente, así que el packet XMP debe cargar también un título x-default vacío
¿Cómo inspecciona los metadatos Info y XMP antes de guardar?
TPdf.InspectPdfAMetadata devuelve un TPdfAMetadataReport con un TPdfAMetadataComparison por campo, cada uno cargando el valor Info, el valor XMP y un TPdfAMetadataState, así que un fallo se puede explicar sin hacer ingeniería inversa de una sola bandera de validación. MismatchFields resume el conjunto que falla, HasXmpPacket le dice si se encontró un packet, y XmpParseError carga el mensaje del parser cuando el packet existe pero no se puede leer
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;
¿Qué cambia NormalizePdfAMetadata y qué se niega a hacer?
TPdf.NormalizePdfAMetadata trata el diccionario Info como la fuente de verdad y reescribe solo las propiedades XMP cuyo campo cayó en MismatchFields; todo lo demás del packet sobrevive. Title y Subject se escriben en el item x-default mientras otras alternativas de idioma quedan intactas, Author se convierte en un rdf:Seq de un item, los namespaces desconocidos y las propiedades no relacionadas se preservan, y las propiedades XMP de claves Info ausentes se dejan quietas. Una fecha Info con zona se escribe como fecha XMP canónica UTC con sufijo Z; una sin zona conserva sus componentes locales. La sobrecarga de archivo guarda a través de un archivo temporal y un reemplazo atómico, y la actualización XMP en sí se agrega como actualización incremental
Las negaciones son deliberadas. Sin packet XMP el método lanza EPdfError, porque construir una identificación PDF/A completa y un set de metadatos es trabajo de SaveAsPdfA, cubierto en creación de archivos PDF/A para archivado con PDFium Component. Una fecha Info malformada lanza EPdfXmpError en lugar de escribir un valor incorrecto de apariencia plausible, y no se guarda nada. Los documentos firmados se rechazan salvo que quien llame pase AllowSignedDocument = True. La equivalencia es además una sola regla de ISO 19005-1, así que un archivo normalizado no es automáticamente 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; // ya consistente, deje el archivo quieto
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 // fecha Info malformada o packet ilegible
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Correr la comparación sobre su propio packet XMP
ComparePdfAInfoAndXmp y SynchronizePdfAInfoToXmp son funciones comunes de FPdfPdfa que trabajan sobre un TPdfXmpPacket sin documento cargado, lo que les viene bien a las pruebas unitarias y a los pipelines que arman XMP desde una plantilla. La única trampa es Present: un record inicializado con Default(TPdfAMetadataValues) tiene un set vacío, cada campo entonces reporta pamsNotRequired, y la comparación pasa de forma vacua sin importar qué valores usted haya llenado
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 qué campos son obligatorios; los valores solos se ignoran
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate ahora es 2026-08-27T01:36:59Z, dc:creator un rdf:Seq de un item
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Si su pipeline archiva documentos que otros sistemas siguen editando, combine un barrido nocturno de InspectPdfAMetadata con NormalizePdfAMetadata para los archivos que se desvían, y mantenga ValidatePdfA como el gate antes de que algo salga hacia almacenamiento de largo plazo. El reporte tipado, el camino de reparación y el resto de la herramienta PDF/A vienen en el PDFium Component para Delphi y C++Builder