O PDFium Component checa a equivalência de metadados Info-XMP em PDF/A com o TPdf.InspectPdfAMetadata e a repara com o TPdf.NormalizePdfAMetadata. A ISO 19005-1 (conforme corrigida pela Cor.1) exige que cada uma das oito entradas Info mapeadas, de Title a ModDate, carregue o mesmo valor que sua propriedade XMP, não que meramente exista; a checagem lê a forma RDF correta, casa namespaces por URI e compara datas como instantes
O relatório de bug que costuma abrir essa conversa parece inofensivo. Um sistema de gestão documental carimba um novo /ModDate no dicionário Info a cada save incremental, deixa o pacote XMP quieto, e seis meses depois uma auditoria de arquivo marca milhares de arquivos como não conformes. As duas datas estão lá. Elas simplesmente pararam de concordar na primeira edição, e uma checagem de presença nunca notou. Edições de Title feitas por uma API só de Info, e uma string Author como Finance; Controlling que alguma ferramenta separou em dois itens dc:creator, falham do mesmo jeito
Por que o PDF/A rejeita metadados que existem nos dois lugares?
O PDF/A rejeita porque a ISO 19005-1 §6.7.3 é uma regra de valor, não de presença: a Tabela 1 mapeia oito chaves Info para propriedades XMP, e uma vez que uma chave Info está presente, a propriedade XMP mapeada precisa conter um valor equivalente. O scanner de nível de byte descrito em validação preflight PDF/A com PDFium Component só confirma que xmp:CreateDate e xmp:ModifyDate existem (pvaiMissingXmpDates). Desde a v3.72.0, o TPdf.ValidatePdfA roda adicionalmente a comparação completa de valores e adiciona pvaiInfoXmpValueMismatch ao conjunto de problemas quando um pacote XMP existe mas discorda do Info (um pacote que não pode ser analisado conta como discordando). Um pacote ausente continua reportado como pvaiMissingXmpMetadata, então os dois problemas nunca contam em dobro o mesmo defeito
Que forma RDF cada propriedade XMP mapeada precisa?
Cada um dos oito mapeamentos tem um tipo XMP fixo, e um valor correto no contêiner errado mesmo assim falha. A ComparePdfAInfoAndXmp em FPdfPdfa.pas busca propriedades por URI de namespace, então um pacote que liga http://purl.org/dc/elements/1.1/ a um prefixo incomum é lido exatamente como um que usa dc. As formas exigidas são:
- Title →
dc:titlee Subject →dc:description: uma alternativa de idiomardf:Alt, comparada apenas contra o itemx-defaultdela (a tag de idioma é casada sem diferenciar maiúsculas); um Alt semx-defaultconta como ausente - Author →
dc:creator: umrdf:Seqcom exatamente um item de texto guardando a string Info inteira, então uma lista de autores separada por ponto e vírgula continua uma única entrada - Keywords →
pdf:Keywordse Producer →pdf:Producer(namespacehttp://ns.adobe.com/pdf/1.3/): propriedades de texto simples - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(namespacehttp://ns.adobe.com/xap/1.0/): propriedades 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>
Valores de texto são comparados como sequências exatas de code points Unicode, sem trim, fold de maiúsculas ou normalização. Um espaço no fim, ou um é pré-composto de um lado e um e decomposto mais acento combinante do outro, é uma divergência genuína. O lado Info sempre vem da decodificação própria do PDFium de texto PDFDocEncoding e UTF-16 por meio do FPDF_GetMetaText, o que evita que a biblioteca reimplemente decodificação de strings e erre sutilmente; o lado XMP é limpo tanto quanto os bytes que o produziram, e é por isso que as armadilhas de codepage que corrompem metadados XMP sob Free Pascal importam aqui também
Quando uma data PDF e uma data XMP são iguais?
Uma data PDF e uma data XMP são iguais quando descrevem o mesmo instante até o segundo, com o mesmo conhecimento de fuso dos dois lados. Os dois parsers aceitam precisão reduzida legal, então D:2026 e 2026 significam ambos 1º de janeiro de 2026, 00:00:00. Quando os dois valores carregam um fuso, eles são convertidos para UTC antes da comparação: D:20260827093659+08'00' equivale a 2026-08-27T01:36:59Z. Quando nenhum carrega fuso, os componentes locais são comparados como escritos. Quando só um lado tem fuso, o resultado é pamsValueMismatch, porque inventar um offset seria um chute. Um segundo fracionário não zero como .250 no XMP também força divergência, já que uma data PDF não tem como expressá-lo e arredondá-lo para fora em silêncio esconderia uma discordância real; .000 é aceito. Valores não analisáveis são reportados separadamente como pamsInvalidInfoDate ou pamsInvalidXmpDate
Presença tem regra própria. O TPdfAMetadataValues.Present é um set preenchido percorrendo o dicionário /Info do trailer ativo, e ele mantém "chave ausente" separado de "chave presente com string vazia". Uma chave ausente rende pamsNotRequired e não exige nada do XMP; /Title () está presente, então o pacote XMP precisa carregar também um título x-default vazio
Como inspecionar metadados Info e XMP antes de salvar?
O TPdf.InspectPdfAMetadata devolve um TPdfAMetadataReport com um TPdfAMetadataComparison por campo, cada um guardando o valor Info, o valor XMP e um TPdfAMetadataState, então uma falha pode ser explicada sem fazer engenharia reversa de uma flag única de validação. O MismatchFields resume o conjunto que falha, o HasXmpPacket diz se um pacote foi encontrado, e o XmpParseError carrega a mensagem do parser quando o pacote existe mas não pode ser lido
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;
O que o NormalizePdfAMetadata muda, e o que ele recusa?
O TPdf.NormalizePdfAMetadata trata o dicionário Info como a fonte da verdade e reescreve apenas as propriedades XMP cujo campo caiu em MismatchFields; todo o resto do pacote sobrevive. Title e Subject são gravados no item x-default enquanto outras alternativas de idioma ficam intactas, Author vira um rdf:Seq de um item, namespaces desconhecidos e propriedades não relacionadas são preservados, e propriedades XMP de chaves Info ausentes não são tocadas. Uma data Info com fuso é gravada como data XMP UTC canônica com sufixo Z; uma sem fuso mantém os componentes locais dela. A sobrecarga de arquivo salva através de um arquivo temporário e uma substituição atômica, e a atualização XMP em si é anexada como uma atualização incremental
As recusas são deliberadas. Sem pacote XMP o método dispara EPdfError, porque montar um conjunto completo de identificação e metadados PDF/A é trabalho do SaveAsPdfA, coberto em criar arquivos de arquivamento PDF/A com PDFium Component. Uma data Info malformada dispara EPdfXmpError em vez de gravar um valor errado de aparência plausível, e nada é salvo. Documentos assinados são rejeitados a menos que quem chame passe AllowSignedDocument = True. Equivalência é uma regra da ISO 19005-1 também, então um arquivo normalizado não é automaticamente um 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; // já consistente, deixa o arquivo em paz
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 malformada ou pacote ilegível
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
Rodando a comparação no seu próprio pacote XMP
A ComparePdfAInfoAndXmp e a SynchronizePdfAInfoToXmp são funções simples em FPdfPdfa que trabalham num TPdfXmpPacket sem documento carregado, o que serve para testes de unidade e pipelines que montam XMP a partir de um template. A única armadilha é o Present: um record inicializado com Default(TPdfAMetadataValues) tem um set vazio, todo campo então reporta pamsNotRequired, e a comparação passa de graça não importa quais valores você tenha preenchido
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 quais campos são obrigatórios; só valores são ignorados
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate agora é 2026-08-27T01:36:59Z, dc:creator um rdf:Seq de um item
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
Se o seu pipeline arquiva documentos que outros sistemas seguem editando, pareie uma varredura noturna de InspectPdfAMetadata com o NormalizePdfAMetadata para os arquivos que desviarem, e mantenha o ValidatePdfA como o gate antes que qualquer coisa saia para armazenamento de longo prazo. O relatório tipado, o caminho de reparo e o resto do ferramental PDF/A vêm no PDFium Component para Delphi e C++Builder