Um portal de receção de arquivos rejeitou um lote de ficheiros "PDF/A-2b" que abriam sem problemas em todos os visualizadores disponíveis. O fornecedor garantiu que estavam em conformidade. Mas não estavam: cada um continha uma ação de JavaScript oculta no catálogo, o tipo de elemento que uma inspeção visual casual nunca deteta, mas que um validador PDF/A completo como o veraPDF identifica num instante. O problema era que ninguém queria integrar uma cadeia de ferramentas Java num serviço de processamento em lote do Delphi apenas para responder a uma pergunta de sim ou não por ficheiro. Esse é o espaço que a função ValidatePdfACompliance do PDFium Component preenche, e vale a pena compreender como alcança um veredicto sem analisar exaustivamente um fluxo de conteúdo
Por que razão o próprio PDFium não consegue responder a isto
A primeira coisa sobre a qual devemos ser francos: a biblioteca pdfium.dll incluída não possui qualquer capacidade PDF/A. Não existe nenhuma ConvertToPDFA, nenhum escritor de OutputIntent nem nenhuma API XMP exposta publicamente. Toda a componente PDF/A nesta biblioteca, tanto para escrita como para validação, reside em Pascal puro no ficheiro FPdfPdfa.pas e funciona através de análise ao nível dos bytes e atualizações incrementais. Por isso, ao chamar o validador, não está a solicitar nada ao motor de renderização do Chromium. Está a executar um analisador de tokens em Pascal sobre os bytes estruturais do ficheiro
A API pública é deliberadamente reduzida. Uma única função lê um fluxo a partir da posição 0 e devolve um registo (record):
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
A propriedade IsCompliant codifica a regra essencial para a validação: um ficheiro só é aprovado se for detetado um nível de conformidade real e a lista de problemas estiver vazia. Uma análise que seja bem-sucedida mas não encontre a marca de identificação pdfaid resulta em pacNone, o que não constitui uma aprovação. Esta é a mesma perspetiva que o CLI de relatórios preflight em lote apresenta externamente: uma lista de conclusões vazia num ficheiro não reconhecido não significa que o documento esteja saudável
Remover corpos de fluxo antes de qualquer verificação de tokens
Eis o detalhe de implementação mais importante, e aquele que é mais fácil de errar caso escreva o seu próprio analisador. O detetor identifica violações procurando por tokens de nomes delimitados, tais como /JavaScript, /LZWDecode, /BM. Se verificar os bytes brutos do ficheiro, os corpos dos fluxos binários incorporados, imagens comprimidas, perfis ICC ou ficheiros de tipos de letra conterão aleatoriamente sequências de bytes semelhantes a esses tokens. Acabará por reportar que /AA ou /3D foram encontrados apenas porque três bytes num JPEG calharam a soletrar essa sequência. Isto é uma fábrica de falsos positivos
A solução reside na função PdfStructureBytes: esta percorre o ficheiro e substitui por espaços os bytes situados entre cada palavra-chave stream e endstream, mantendo intacta a estrutura do dicionário. A verificação só é executada após este processo. Todas as validações de tokens de nomes no validador operam sobre esta cópia limpa. Se tiver de reter apenas uma ideia deste artigo, que seja esta. A mesma disciplina é aplicada no validador de PDF/UA, que mantém a sua própria cópia desta rotina, dado que ambas as normas evoluem de forma independente
Os 29 problemas (issues) e o seu significado
O tipo TPdfAValidationIssue constitui um contrato documentado. Os ordinais estão congelados porque os testes do DUnitX, as demonstrações e a camada de relatórios dependem deles; deste modo, as novas conclusões são sempre adicionadas ao fim. A partir da versão v1.63.0, existem 29 elementos, que se dividem em algumas famílias:
- Metadados e identidade:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Cor e saída:
pvaiMissingOutputIntent,pvaiMissingIccProfileepvaiMixedDeviceColorSpacesquando surgem simultaneamente DeviceRGB e DeviceCMYK (6.2.3.3) - Proibições estritas para todas as partes:
pvaiEncryptionPresent(um dicionário/Encrypté estritamente proibido),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Tipos de letra:
pvaiFontNotEmbeddede o mais rigorosopvaiUnembeddedFont, além depvaiUnicodeMappingMissingpara uma declaração de Nível U sem/ToUnicode - Etiquetagem (Tagging):
pvaiLevelAStructureMissingquando uma declaração de conformidade=A não possui uma estrutura etiquetada
Os seis novos membros, adicionados nos ordinais 24 a 29, cobrem os casos subtis com os quais os verificadores se deparam frequentemente: pvaiTrappedTrue (um /Trapped /True no dicionário Info, um falso positivo comum dado que o valor deve ser False ou Unknown), pvaiForbiddenActionSubtype (Sound ou Movie utilizados como ação, e não apenas como anotação), pvaiTransparentColorSpace (um modo de mistura não-Normal ou um /CA//ca diferente de 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont e pvaiMixedDeviceColorSpaces
Filtragem inteligente por partes: A-1 é rigoroso, A-2 e A-3 são mais flexíveis
O PDF/A não se rege por um único livro de regras. Três elementos que o PDF/A-1 proíbe são explicitamente permitidos a partir do PDF/A-2: transparência (um grupo /Transparency ou uma /SMask ativa, 6.4), conteúdo opcional (/OCProperties, 6.1.13) e ficheiros incorporados (/EmbeddedFiles ou /EF, 6.1.11). Um validador simplista que assinale estes três elementos em todos os ficheiros rejeitará em massa documentos PDF/A-2 perfeitamente válidos
Assim, o validador lê o número da parte a partir da marcação pdfaid usando a função PdfAPartOf e filtra essas verificações limitando-as a PartNo = 1. As validações de modo de mistura e transparência de anotação para os novos problemas de transparência são de igual modo exclusivas da 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;
Vale a pena mencionar uma configuração padrão conservadora: quando não existe qualquer marcação pdfaid, a parte é tratada como sendo a 1, a mais restritiva. O raciocínio é que um ficheiro não identificado deve ser sujeito às regras mais rigorosas, em vez de ser aprovado sem critério. JavaScript, ações proibidas, LZW, XFA, NeedAppearances, anotações proibidas e tipos de letra não incorporados continuam proibidos em todas as partes, pelo que estas verificações nunca são filtradas
Expandir fluxos de objetos para detetar elementos ocultos
O PDF 1.5 introduziu o fluxo de referências cruzadas (cross-reference stream) e o fluxo de objetos (/Type /ObjStm), que criam um ponto cego para um analisador de bytes rudimentar. Um catálogo, um OutputIntent, um dicionário de ações — qualquer elemento que não seja em si um fluxo — pode ser comprimido via Flate dentro de um ObjStm. Se analisar a estrutura bruta, não verá nada disso, reportando um ficheiro limpo que na verdade não o é
A função PdfExpandObjectStreams elimina essa limitação. Antes de qualquer verificação ser executada, o validador faz Data := PdfExpandObjectStreams(Data). A rotina localiza todos os ObjStm, lê os seus cabeçalhos /N e /First para obter os números e desvios dos objetos contidos, descomprime o corpo com PdfInflate (a zlib da RTL, System.ZLib no Delphi e zstream no FPC) e anexa cada objeto contido como um bloco N 0 obj ... endobj comum ao final de uma cópia dos bytes. Las verificações de tokens existentes encontram então esses objetos sem qualquer alteração na sua lógica
Duas restrições tornam esta abordagem robusta em vez de frágil. Os objetos de fluxo (stream), os metadados, os perfis ICC e os programas de tipos de letra não podem residir num fluxo de objetos — apenas dicionários que não sejam fluxos o podem fazer. Desta forma, a expansão lida apenas com dicionários, e os objetos anexados não contêm a palavra-chave stream que perturbaria o processo de limpeza do corpo. E como o conteúdo anexado é colocado após %%EOF, a pesquisa inversa a partir de startxref continua a localizar o trailer original. O próprio trailer do fluxo de referências cruzadas já tinha sido tratado anteriormente, na v1.49.3, lendo diretamente Root, Size e ID do dicionário em texto simples do fluxo xref, um tema explorado no artigo complementar sobre a validação de fluxos de objetos e referências cruzadas; o trabalho com o fluxo de objetos apenas necessitou de adicionar o passo de descompressão, dispensando a descodificação de entradas xref de tipo 2 ou a reversão de preditores PNG
Os limites realistas de um validador ao nível dos bytes
Esta é uma ferramenta de preflight, e não um validador certificado, pelo que os limites são reais. A incorporação de tipos de letra baseia-se numa heurística de contagem, e conseguir a sua exatidão exigiu uma correção que importa conhecer. A validação original utilizava PdfCountName('/FontDescriptor'), mas cada tipo de letra contribui com dois tokens /FontDescriptor — uma referência a partir do dicionário de tipos de letra e um /Type no próprio objeto descritor —, pelo que a contagem era de 2N para N programas incorporados, resultando num teste sempre verdadeiro. A solução foi a função PdfCountDescriptorRefs, que conta apenas o formato de referência /FontDescriptor N G R, um por tipo de letra, e assinala pvaiUnembeddedFont apenas quando os programas incorporados são efetivamente 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 corrigida, esta verificação é genérica: um documento misto onde cada descritor por acaso contenha algum FontFile pode ainda deixar passar um tipo de letra individual não conforme. A expansão de fluxos de objetos também tem um efeito secundário conhecido: expõe os recursos padrão Standard-14 contidos no /DR de um AcroForm, tais como /Helv, e a heurística reporta-os devidamente como não incorporados, embora o veraPDF os ignore por nunca serem realmente utilizados para renderização. As verificações ao nível dos operadores nos fluxos de conteúdo (6.2.10) estão totalmente fora de âmbito, pois exigiriam uma análise completa de conteúdos em vez de um varrimento de bytes. Encare o validador como um primeiro filtro rápido e sem dependências que deteta as violações que a inserção de marcadores não consegue resolver, e reserve um validador completo para a certificação final
Este é o lado da verificação neste cenário. O lado complementar da escrita — onde SaveAsPdfA insere o XMP, o OutputIntent e o perfil ICC sRGB, e despromove de forma honesta um pedido de Nível A que não possua uma estrutura etiquetada — baseia-se no mesmo mecanismo ao nível dos bytes. Ambas as vertentes são fornecidas no PDFium Component para Delphi, um pacote VCL único construído sobre uma implementação PDF/A em Pascal puro, sem necessidade de instalar qualquer runtime externo