Você lança um conversor que marca todo arquivo como PDF/A-1b, o sistema de registros do cliente ingere tudo por um ano e depois uma auditoria passa o lote inteiro no veraPDF, com um terço dos arquivos voltando como não conformes. Nada travou, nenhuma exceção foi lançada, os arquivos abrem normalmente em qualquer visualizador da sua mesa. Eles simplesmente não eram o padrão que você estampou neles. Esse é o modo normal de falha do PDF de arquivo, e é por isso que "definimos a flag" nunca é a mesma afirmação que "ele valida"
A primeira coisa a entender sobre PDFium e PDF/A é que o mecanismo em si não tem relação com isso. O PDFium renderiza, faz parse e grava PDF, mas sua superfície pública não tem ConvertToPDFA, não tem gravador de OutputIntent, não tem API de XMP. Toda a parte de conformidade de arquivo, o pacote XMP, o OutputIntent e seu perfil ICC, as marcações do catálogo e a validação, vive no próprio PDFiumPas, em uma unit puramente Pascal de cerca de 2.000 linhas (FPdfPdfa.pas) que faz parse dos bytes salvos e os reescreve por meio de uma atualização incremental. Saber onde o trabalho acontece mostra onde os bugs se escondem, e eles não se escondem no PDFium
O que o PDF/A exige de verdade, e onde surgem as restrições
PDF/A não é um único formato. A ISO 19005 define três partes, PDF/A-1, -2 e -3, e, dentro de cada uma, níveis de conformidade que prometem coisas diferentes. O nível B, básico, garante apenas que a aparência visual possa ser reproduzida. O nível A, acessível, adiciona uma árvore de estrutura marcada e mapeamento Unicode por cima do B. O nível U, que só existe nas partes 2 e 3, fica entre os dois: texto Unicode confiável sem a árvore estrutural completa. A ISO 19005-1 não tem nível U, uma restrição que a biblioteca codifica diretamente
Algumas regras do formato são as que realmente pegam na prática. Criptografia é proibida de forma absoluta (ISO 19005-1 §6.1.3 e sucessoras): um arquivo PDF/A não pode carregar um dicionário /Encrypt. O documento precisa declarar uma condição de saída por meio de um OutputIntent cujo destino seja um perfil ICC válido (§6.2.3.2). A própria declaração de conformidade precisa aparecer como metadados XMP sob o esquema de identificação do PDF/A. O nível A ainda exige §6.8 de estrutura lógica, a árvore de tags que torna o documento legível por máquina. Se você falhar em qualquer um desses pontos, o verificador de conformidade rejeita o arquivo, mesmo que ele renderize perfeitamente
A chamada que gera um arquivo para arquivamento
O PDFiumPas expõe todo o pipeline por trás de TPdf.SaveAsPdfA. A sobrecarga simples recebe uma conformidade de destino e, por padrão, usa PDF/A-1b, que é a escolha certa para o caso comum de "quero tornar isso renderizável para sempre"
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// Default conformance is pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// file now carries XMP, sRGB OutputIntent, and catalog markers
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;
Por baixo dos panos, isso é um movimento em duas etapas. SaveAsPdfA primeiro pede ao PDFium para serializar o documento com FPDF_SaveAsCopy, depois entrega esse fluxo de bytes a InjectPdfAMarkers, que acrescenta os metadados XMP, o OutputIntent sRGB com o perfil ICC incorporado e um catálogo reescrito como atualização incremental. A origem é lida da posição zero e o destino é escrito da posição zero; a árvore de objetos original é deixada intacta e as marcações entram depois do %%EOF existente. Se você precisa dos bytes em vez de um arquivo, SaveAsPdfAToStream recebe um TStream e as mesmas opções
Escolhendo o nível de conformidade no registro de opções
Para mirar uma parte e um nível específicos, passe um registro TPdfASaveOptions. O campo Conformance recebe um valor TPdfAConformance. O enum cobre todas as combinações válidas e nada além disso: pac1b, pac1a para a parte 1; pac2b, pac2u, pac2a para a parte 2; pac3b, pac3u, pac3a para a parte 3, além de pacUnknown e pacNone para o lado da validação. Não existe pac1u, porque esse nível não existe no padrão
var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('report.pdf');
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: reliable Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;
Grande parte do registro pode ficar vazia. Deixe Title, Author, Subject, Keywords, Creator e Producer em branco e SaveAsPdfA preenche tudo a partir do dicionário Info do documento via FPDF_GetMetaText. Deixe CreationDate e ModDate em branco e ele usa a hora UTC atual para ambas as datas XMP. Deixe DocumentId e InstanceId em branco e a biblioteca os pré-popula a partir de FPDF_GetFileIdentifier, recorrendo a um ID determinístico derivado dos bytes de origem. O único campo que talvez você queira sobrescrever de propósito é IccProfileData: vazio significa o perfil sRGB IEC61966-2.1 empacotado, mas um fluxo CMYK ou em tons de cinza deve fornecer o seu próprio
Por que o nível A pesa mais, e quando essa escolha é justificável
Há aqui uma sutileza que pega quem espera que uma flag seja garantia. Você pode pedir pac1a para um documento que não tem árvore de tags, mas PDF/A-1a exige estrutura lógica §6.8, e a biblioteca não consegue fabricar uma árvore de estrutura a partir de um PDF sem marcação. Em vez de emitir um arquivo que afirma nível A e falha nele, SaveAsPdfA verifica se existe uma estrutura marcada real (/StructTreeRoot mais /MarkInfo com /Marked true) e, se ela estiver ausente, rebaixa a declaração: pac1a vira pac1b, pac2a vira pac2b, e assim por diante nas três partes. Os auxiliares internos são PdfAIsLevelA e PdfADowngradeToLevelB
O raciocínio vale ser dito com todas as letras: um arquivo que declara honestamente o nível que atende é mais útil do que um que mente sobre um nível que não atende. O nível U é tratado de outro jeito. Detectar cobertura Unicode genuína exigiria um teste ingênuo de "tem /ToUnicode?", que rebaixaria demais documentos legítimos, já que WinAnsi e codificações semelhantes são exceções, então o lado de gravação emite a declaração U conforme o chamador a informou e deixa a discrepância para ser sinalizada na validação. Se você precisa de um arquivo de arquivo com nível A garantido, marque o documento antes de converter; o conversor não inventa estrutura que não existe
A armadilha do ICC que só um validador real revela
Essa é a falha que ensinou a lição mais dura, porque o próprio verificador da biblioteca passou nela, mas o veraPDF, o validador de referência da ISO 19005, não passou. PDF/A exige que o perfil de destino do OutputIntent seja um fluxo ICCBased válido, e §6.2.3.2 obriga o verificador a validar esse fluxo como um espaço de cor. Um fluxo ICCBased precisa declarar /N, o número de componentes de cor. Uma versão inicial do injetor gravava o dicionário do fluxo ICC apenas com /Length e sem /N, e o veraPDF rejeitava o resultado com "The N entry (value null)... is missing"
O que tornou isso traiçoeiro foi que a rejeição só aparecia em PDF/A-1b e -1a. Os modelos de conformidade das partes 2 e 3 não executavam essa verificação específica no perfil de destino, então a mesma estrutura injetada validava em pac2b, pac3b e pac2u, mas falhava em pac1b por nada além do valor pdfaid:part. Nenhum teste unitário conseguiria ver isso, porque o ValidatePdfACompliance da biblioteca só verificava se a chave /DestOutputProfile existia, não o que havia dentro do dicionário do fluxo. Os testes internos continuavam verdes, a validação real de arquivo falhava
A correção é IccComponentCount, que lê a assinatura do espaço de cor de dados no offset 16 do cabeçalho ICC e a mapeia para uma contagem de componentes: GRAY vale 1, RGB , Lab e XYZ valem 3, CMYK vale 4, com um perfil desconhecido caindo para 3. Essa contagem vai para o dicionário do fluxo como /N. Ela é calculada, não fixada em 3, para que um chamador que forneça um perfil CMYK ou em tons de cinza por meio de IccProfileData continue recebendo o valor correto. A lição maior é metodológica: o verificador interno da biblioteca e um validador autoritativo têm pontos cegos, e a saída PDF/A precisa ser testada de ponta a ponta contra uma implementação de referência como o veraPDF, não confiada a auto-verificações. A mesma disciplina de atualização incremental por trás de arquivos limpos é coberta em validar objetos comprimidos e fluxos xref, o que importa porque os PDFs modernos que o injetor consome muitas vezes são construídos sobre fluxos de cross-reference
Criptografia, fluxos xref e outras bordas
Como a ISO 19005 proíbe criptografia, o caminho de gravação a remove antes de escrever. SaveAsPdfA aplica FPDF_REMOVE_SECURITY ao serializar, então uma origem criptografada, carregada com a senha, é descriptografada na entrada do arquivo de arquivo. Em um documento não criptografado isso não faz nada e não muda nada. A consequência é a mesma restrição que o HotPDF aplica pelo outro lado: um único arquivo não pode ser ao mesmo tempo criptografado e PDF/A. Quando um fluxo precisa dos dois, a resposta são dois artefatos, uma cópia criptografada para distribuição e uma cópia limpa separada para o arquivo de arquivo
Outra borda só aparece quando machuca: documentos PDF 1.5+ que usam um fluxo de cross-reference puro e não carregam a palavra-chave trailer. O injetor lê o trailer para localizar o /Info de origem e anexar a atualização incremental, e precisa aceitar a forma de xref-stream, senão um documento assim seria copiado com as marcações silenciosamente descartadas. A ISO 32000-1 §7.5.6 permite explicitamente que uma atualização incremental clássica de trailer siga um documento xref-stream, com /Prev apontando para o deslocamento do xref-stream, exatamente a estrutura que o injetor emite. O próprio FPDF_SaveAsCopy do PDFium sempre escreve um trailer clássico, então no pipeline normal o injetor nunca encontra uma origem puramente xref-stream, mas o caminho de leitura lida com isso para documentos que chegam de fora
Valide antes de confiar na declaração
A biblioteca traz um verificador em nível de bytes, TPdf.ValidatePdfA, que retorna um TPdfAValidationResult. O campo Conformance informa o nível detectado e Issues é um conjunto de valores TPdfAValidationIssue; o método auxiliar IsCompliant só é verdadeiro quando um nível real foi detectado e o conjunto de issues está vazio. Rode isso como uma primeira barreira rápida em lote
var
Pdf: TPdf;
Res: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice_archive.pdf');
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
finally
Pdf.Free;
end;
end;
Seja honesto sobre o que isso compra. O verificador em nível de bytes captura problemas estruturais, como um OutputIntent ausente, uma ação proibida, um /Encrypt presente ou transparência onde a parte 1 a proíbe, com alta confiança, e a detecção de incorporação de fontes usa uma heurística de contagem que deliberadamente reporta só um sinal de alta confiança, em vez de tentar acompanhar cobertura por glifo. O que ele não faz é análise de operadores do fluxo de conteúdo, que exigiria um parser completo e está fora do escopo por design. Para um gate de release, combine o verificador interno com o veraPDF: o verificador é instantâneo e roda em qualquer lugar sem DLL, o veraPDF é autoritativo. Encaixar essa combinação em uma execução em lote é o tema do CLI de relatório de preflight em lote, que é onde essa validação pertence em um fluxo real de arquivo de arquivo
As APIs SaveAsPdfA, InjectPdfAMarkers e ValidatePdfA mostradas aqui fazem parte do PDFium Component para Delphi, C++Builder e Lazarus/FPC. A página do produto traz a referência completa da API, incluindo o enum de conformidade integral e o registro de opções por trás destes exemplos