Você lança um conversor que marca todo ficheiro como PDF/A-1b, o sistema de registos do cliente ingere tudo por um ano e depois uma auditoria passa o lote inteiro no veraPDF, com um terço dos ficheiros voltando como não conformes. Nada travou, nenhuma exceção foi lançada, os ficheiros abrem normalmente em qualquer visualizador da sua mesa. Eles simplesmente não eram o predefinido que você estampou neles. Esse é o modo normal de falha do PDF de ficheiro, 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 ficheiro, 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 guardados e os reescreve através 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 realmente exige, e onde ele aperta
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 maneira absoluta (ISO 19005-1 §6.1.3 e sucessoras): um ficheiro PDF/A não pode carregar um dicionário /Encrypt. O documento necessita declarar uma condição de saída através de um OutputIntent cujo destino seja um perfil ICC válido (§6.2.3.2). A própria declaração de conformidade necessita 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 ficheiro, mesmo que ele renderize perfeitamente
A única chamada que produz um ficheiro de ficheiro
O PDFiumPas expõe todo o pipeline por trás de TPdf.SaveAsPdfA. A sobrecarga simples recebe uma conformidade de destino e, por predefiniçã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ê necessita dos bytes em vez de um ficheiro, SaveAsPdfAToStream recebe um TStream e as mesmas opções
Escolhendo a conformidade com o registo de opções
Para mirar uma parte e um nível específicos, passe um registo 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 predefinido
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 registo 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 degrada, e por que essa é a escolha honesta
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 ficheiro 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 ficheiro 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 aplicação chamadora a informou e deixa a discrepância para ser sinalizada na validação. Se você necessita de um ficheiro de ficheiro 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 verificador real detecta
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 necessita 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 ficheiro 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 aplicação chamadora que forneça um perfil CMYK ou em tons de cinza através 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 necessita 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 ficheiros 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 palavra-passe, é descriptografada na entrada do ficheiro de ficheiro. 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 ficheiro não pode ser ao mesmo tempo criptografado e PDF/A. Quando um fluxo necessita dos dois, a resposta são dois artefatos, uma cópia criptografada para distribuição e uma cópia limpa separada para o ficheiro de ficheiro
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 necessita 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
Verifique 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 deteçã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 ficheiro de ficheiro
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 registo de opções por trás destes exemplos