Artículo técnico

Equivalencia de metadatos Info y XMP de PDF/A en Delphi

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:title y Subject → dc:description: una alternativa de idioma rdf:Alt, comparada contra su item x-default únicamente (el tag de idioma se empareja sin distinguir mayúsculas); un Alt sin x-default cuenta como ausente
  • Author → dc:creator: un rdf:Seq con 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:Keywords y Producer → pdf:Producer (namespace http://ns.adobe.com/pdf/1.3/): propiedades de texto simples
  • Creator → xmp:CreatorTool, CreationDate → xmp:CreateDate, ModDate → xmp:ModifyDate (namespace http://ns.adobe.com/xap/1.0/): propiedades de texto simples
Los ocho pares mapeados que ComparePdfAInfoAndXmp verifica para la equivalencia de metadatos PDF/A en Delphi: Title y Subject necesitan un rdf:Alt con un item x-default, Author un rdf:Seq de un item, Keywords, Producer, Creator y las dos fechas son texto simple, cada uno buscado por URI de namespace XMP y no por prefijo
Una vez que una clave Info está presente, ISO 19005-1 exige que la propiedad XMP mapeada cargue un valor equivalente en la forma RDF requerida, así que un valor en el contenedor equivocado igual falla
<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

Cómo decide PDFium Component que una fecha Info PDF y una fecha XMP son iguales en Delphi: dos zonas se convierten a UTC y comparan instantes, dos valores sin zona se comparan como están escritos, una zona sola es un pamsValueMismatch, un segundo fraccionario distinto de cero no se puede expresar, y los valores no parseables se reportan por separado
Igualdad significa el mismo instante al segundo con el mismo conocimiento de zona horaria en ambos lados, así que inventar un offset o redondear un segundo fraccionario escondería un desacuerdo real

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

Qué reescribe NormalizePdfAMetadata al reparar metadatos PDF/A en Delphi con PDFium Component: Info es la fuente de verdad, solo las entradas de MismatchFields se reescriben como texto Alt x-default, un Seq de un item o una fecha UTC canónica, mientras namespaces desconocidos, propiedades no relacionadas y propiedades de claves ausentes sobreviven intactos
La reparación se niega ante un packet XMP ausente, una fecha Info malformada y documentos firmados, porque construir un set completo de metadatos PDF/A es trabajo de SaveAsPdfA, no de un fix de equivalencia quirúrgico

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