Um portal de ingestão de arquivos rejeitou um lote de arquivos "PDF/A-2b" que abriam perfeitamente em todos os visualizadores na mesa de trabalho. O fornecedor jurou que eles estavam em conformidade. Não estavam: cada um deles continha uma ação JavaScript enterrada no catálogo, o tipo de coisa que uma olhada casual nunca percebe, mas que um validador PDF/A completo como o veraPDF sinaliza num piscar de olhos. O problema é que ninguém queria acoplar uma cadeia de ferramentas Java a um serviço em lote do Delphi apenas para responder a uma pergunta de sim ou não por arquivo. Esse é o espaço que a função ValidatePdfACompliance do Componente PDFium preenche, e vale a pena entender como ela chega a um veredito sem precisar analisar totalmente um fluxo de conteúdo
Por que o próprio PDFium não pode responder a isso
A primeira coisa a ser honesta: a biblioteca pdfium.dll empacotada não possui nenhuma capacidade de PDF/A. Não existe ConvertToPDFA, nenhum gravador de OutputIntent, nenhuma API XMP na interface pública. Cada parte do PDF/A nesta biblioteca, tanto o lado de escrita quanto o de verificação, reside em Pascal puro em FPdfPdfa.pas e funciona por meio de análise em nível de byte combinada com atualização incremental. Portanto, quando você chama o validador, não está perguntando nada ao renderizador do Chromium. Você está executando um leitor de tokens em Pascal sobre os bytes estruturais do arquivo
A API pública é intencionalmente pequena. Uma função lê um fluxo a partir da posição 0 e retorna um registro:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
O IsCompliant codifica a regra importante em um portal de validação: um arquivo é aprovado somente quando um nível real de conformidade é detectado e o conjunto de problemas está vazio. Uma análise que é bem-sucedida, mas não encontra nenhum marcador pdfaid, resolve para pacNone, o que explicitamente não é uma aprovação. Esse é o mesmo ponto que a CLI de relatório preflight em lote apresenta externamente: uma lista de descobertas vazia em um arquivo não reconhecido não significa saúde perfeita
Removendo corpos de fluxo antes de qualquer varredura de token
Aqui está o detalhe de implementação mais importante, e aquele que é mais fácil de errar caso você escreva o seu próprio analisador. O detector encontra violações procurando por tokens de nomes delimitados, coisas como /JavaScript, /LZWDecode, /BM. Se você escanear os bytes brutos do arquivo, os corpos de fluxo binários incorporados, imagens compactadas, perfis ICC, programas de fonte, conterão aleatoriamente sequências de bytes semelhantes a esses tokens. Você reportará /AA ou /3D como "encontrados" porque três bytes dentro de um JPEG por acaso formaram essa palavra. Isso seria uma fábrica de falsos positivos
A solução é o PdfStructureBytes: ele percorre o arquivo e substitui por espaços os bytes entre cada palavra-chave stream e endstream, mantendo intacta a estrutura do dicionário. Somente após isso a varredura é executada. Cada verificação de token de nome no validador opera nesta cópia limpa. Se você guardar uma única ideia deste artigo, guarde esta. A mesma disciplina é espelhada no validador PDF/UA, que mantém sua própria cópia da rotina porque os dois padrões evoluem de forma independente
Os 29 problemas e o que cada um significa
O TPdfAValidationIssue is a documented contract. The ordinals are frozen because DUnitX tests, the demos, and the report layer all depend on them, so new findings are only ever appended to the end. As of v1.63.0 there are 29 members. They fall into a few families:
- Metadados e identidade:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Cor e saída:
pvaiMissingOutputIntent,pvaiMissingIccProfile, epvaiMixedDeviceColorSpacesquando ambos DeviceRGB e DeviceCMYK aparecem (6.2.3.3) - Proibições estritas para cada parte:
pvaiEncryptionPresent(um dicionário/Encrypté proibido terminantemente),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fontes:
pvaiFontNotEmbeddede o mais estritopvaiUnembeddedFont, além depvaiUnicodeMappingMissingpara uma declaração Level U sem/ToUnicode - Etiquetagem (tagging):
pvaiLevelAStructureMissingquando uma declaração de conformidade=A não possui estrutura etiquetada
Os seis membros mais recentes, adicionados nos ordinais de 24 a 29, cobrem os casos sutis nos quais os revisores realmente tropeçam: pvaiTrappedTrue (um /Trapped /True no dicionário Info, um "falso amigo" já que o valor deve ser False ou Unknown), pvaiForbiddenActionSubtype (Sound ou Movie usados como ação, e não apenas uma anotação), pvaiTransparentColorSpace (um modo de mesclagem não Normal ou um /CA//ca não igual a 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont e pvaiMixedDeviceColorSpaces
Restrições cientes da parte: A-1 é estrito, A-2 e A-3 relaxam
PDF/A não é um livro de regras único. Três coisas que o PDF/A-1 proíbe são explicitamente permitidas a partir do PDF/A-2 em diante: transparência (um grupo /Transparency ou um /SMask ativo, 6.4), conteúdo opcional (/OCProperties, 6.1.13) e arquivos incorporados (/EmbeddedFiles ou /EF, 6.1.11). Um validador ingênuo que sinalize todos os três para cada arquivo rejeitará em massa documentos PDF/A-2 perfeitamente válidos
Portanto, o validador lê o número da parte a partir do marcador pdfaid por meio de PdfAPartOf e restringe essas verificações a PartNo = 1. As verificações de modo de mesclagem e alfa de anotação para os novos problemas de transparência são igualmente apenas para a parte 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Um padrão conservador merece menção: quando não há nenhum marcador pdfaid, a parte é tratada como 1, a mais estrita. O raciocínio é que um arquivo não identificado deve ser mantido sob as regras mais rígidas em vez de ser aprovado sem análise. JavaScript, ações proibidas, LZW, XFA, NeedAppearances, anotações proibidas e fontes não incorporadas permanecem proibidos para cada parte, de modo que essas verificações nunca ficam retidas pelas restrições de versão
Expandindo fluxos de objetos para que nada se esconda
O PDF 1.5 introduziu o fluxo de referência cruzada e o fluxo de objetos (/Type /ObjStm), e eles criam um ponto cego para um analisador de bytes ingênuo. Um catálogo, um OutputIntent, um dicionário de ações, qualquer coisa que não seja em si um fluxo, pode ser compactado em Flate dentro de um ObjStm. Escaneie a estrutura bruta e você não verá nada disso, relatando então um arquivo limpo que na verdade é tudo menos limpo
A função PdfExpandObjectStreams fecha essa lacuna. Antes de qualquer verificação rodar, o validador executa Data := PdfExpandObjectStreams(Data). A rotina localiza cada ObjStm, lê seu cabeçalho /N e /First para obter os números e deslocamentos dos objetos contidos, infla o corpo com PdfInflate (a biblioteca zlib do RTL, System.ZLib no Delphi e zstream no FPC) e anexa cada objeto contido como um N 0 obj ... endobj comum ao final de uma cópia dos bytes. As verificação de token existentes encontram esses objetos sem qualquer alteração lógica
Duas restrições tornam isso limpo em vez de frágil. Objetos de fluxo, os metadados, perfil ICC e programas de fonte não podem residir em um fluxo de objetos, apenas dicionários que não são fluxos podem, portanto a expansão lida apenas com dicionários e os objetos anexados não carregam nenhuma palavra-chave stream para perturbar a etapa de remoção do corpo. E porque o conteúdo anexado cai depois de %%EOF, a busca reversa a partir de startxref ainda encontra o trailer original. O trailer do próprio fluxo de referência cruzada já havia sido tratado anteriormente, na versão 1.49.3, lendo Root, Size e ID diretamente do dicionário plaintext da xref-stream, um tópico explorado no artigo complementar sobre validação de objetos e fluxos de referência cruzada; o trabalho com o fluxo de objetos apenas teve de adicionar a etapa de inflar, sem a necessidade de decodificar entradas xref do tipo 2 ou desdobrar um preditor PNG
Os limites honestos de um verificador em nível de byte
Esta é uma ferramenta preflight, não um validador certificado, e os limites são reais. A incorporação de fontes é uma heurística de contagem, e acertar isso exigiu uma correção que vale a pena conhecer. A verificação original usava PdfCountName('/FontDescriptor'), mas cada fonte contribui com dois tokens /FontDescriptor, uma referência do dicionário de fonte e um /Type no próprio objeto descritor, de modo que a contagem era de 2N contra N programas incorporados e o teste era sempre verdadeiro. A correção é o PdfCountDescriptorRefs, que conta apenas o formato de referência /FontDescriptor N G R, uma por fonte, e gera pvaiUnembeddedFont somente quando os programas incorporados são genuinamente em menor número:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Mesmo corrigido, é impreciso: um documento misto onde cada descritor por acaso tem algum FontFile ainda pode deixar passar uma fonte individual não conforme. Expandir fluxos de objetos também tem um efeito colateral conhecido, expõe os recursos padrão 14 que um AcroForm /DR carrega, como o /Helv, e a heurística diligentemente relata-os como não incorporados, embora o veraPDF permita a passagem deles porque nunca são usados de fato para renderizar. Verificações no nível do operador de fluxo de conteúdo (6.2.10) estão totalmente fora do escopo, pois exigiriam uma análise completa do conteúdo e não apenas uma varredura de bytes. Trate o validador como um primeiro portal rápido e livre de dependências que captura as violações que a injeção de marcadores não pode corrigir, reservando um validador completo para a certificação final
Esta é a metade de verificação da história. O lado complementar de escrita, onde o método SaveAsPdfA injeta os metadados XMP, OutputIntent e o perfil ICC sRGB, e honestamente rebaixa um pedido de Nível A que não possui estrutura etiquetada, é construído sobre o mesmo mecanismo de nível de byte. Ambas as metades são fornecidas no Componente PDFium para Delphi, um pacote VCL único sobre uma implementação PDF/A pura em Pascal, sem necessidade de instalação de qualquer runtime externo