Seu preflight informa que o arquivo está em conformidade com PDF/UA. O veraPDF abre o mesmo arquivo e sinaliza uma Figure sem texto alternativo segundo a cláusula 7.3. As duas ferramentas estão certas, e a diferença entre elas resume o problema de verificar acessibilidade por meio de uma varredura de bytes. Uma análise em nível de bytes confirma que o arquivo declara estar marcado: ela encontra /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part no pacote XMP, o título do documento e o idioma. Esses marcadores de formato são necessários, mas não revelam se a figura real da página quatro tem uma descrição que um leitor de tela possa ler em voz alta. Essa resposta está na árvore de tags e, para obtê-la, é preciso percorrer a árvore
O PDFium Component é uma biblioteca PDF VCL nativa para Delphi e C++Builder, e seu método ValidatePdfUa executa as duas análises. A etapa em nível de bytes cuida dos marcadores de formato. Sobre ela há uma etapa de árvore de estrutura que carrega a árvore de tags ativa, percorre cada elemento e verifica um conjunto pequeno de regras de conteúdo de alta confiabilidade, nas quais a ausência de um atributo representa um defeito real de acessibilidade, não uma preferência de estilo. Este artigo trata dessa segunda etapa: o que ela verifica, por que a lógica das regras é uma função pura sem uma DLL por baixo e onde ela deliberadamente limita seu alcance
Por que uma varredura de bytes não vê um Alt ausente
A ISO 14289-1 (PDF/UA-1) acrescenta uma camada de requisitos à ISO 32000. Alguns desses requisitos são estruturais e ficam visíveis no arquivo bruto: o catálogo deve declarar uma árvore de estrutura, as preferências do visualizador devem definir DisplayDocTitle e as fontes devem estar incorporadas. Um analisador de tokens que remove os corpos dos streams e compara tokens de nome respeitando os limites dos delimitadores consegue verificar tudo isso, e o método ValidatePdfUaCompliance do PDFium faz exatamente essa análise para cláusulas como 7.1, 7.18 e 7.21
Mas "toda Figure tem texto alternativo" não é uma propriedade da sintaxe do arquivo. É uma propriedade da estrutura lógica, a árvore de elementos marcados que associa conteúdo a significado. A entrada Alt de uma Figure pode estar no dicionário do elemento de estrutura, ser fornecida por um trecho /ActualText ou vir de um tipo personalizado associado por um mapa de funções. Não é possível encontrá-la com segurança procurando /Alt no fluxo de bytes, pois essa sequência aparece em contextos sem relação, pode estar compactada em um stream de objetos e não informa a qual elemento de estrutura pertence. A maneira correta de responder é consultar a própria árvore de estrutura do documento, elemento por elemento, a mesma superfície avaliada pelo veraPDF e pelo PAC. Esse é o princípio das verificações de Nível 1 do PDFium: varredura de bytes para o formato, percurso da árvore para o conteúdo
Lendo a árvore de tags em tempo real
O ponto de partida é TPdf.GetStructureElements, também exposto pela propriedade StructureElements, que retorna um TPdfStructureElements: um array plano de registros TPdfStructureElement na ordem do documento. Cada registro é a projeção de um elemento de estrutura obtida pelas funções de acesso do PDFium e contém os campos realmente necessários às regras de acessibilidade:
type
TPdfStructureElement = record
Level: Integer; // depth in the tag tree
ParentIndex: Integer; // index of parent element, or -1
TypeName: WString; // standard /S name: Figure, Formula, Note...
Title: WString; // /T
AlternateText: WString; // /Alt (FPDF_StructElement_GetAltText)
ActualText: WString; // /ActualText
Expansion: WString; // /E
ID: WString; // /ID (FPDF_StructElement_GetID)
Language: WString; // /Lang
MarkedContentIDs: TPdfIntegerArray;
// ... child bookkeeping fields
end;
O campo TypeName orienta as decisões do validador. Ele vem de FPDF_StructElement_GetType, que retorna o tipo de estrutura padrão do elemento, isto é, seu nome /S, depois que o PDFium resolve o mapa de funções. AlternateText vem de FPDF_StructElement_GetAltText, ActualText de FPDF_StructElement_GetActualText e ID de FPDF_StructElement_GetID. Como o array é plano e ordenado, o validador pode analisar o documento inteiro de uma só vez, sem recursão, o que é importante para a única regra global em vez de restrita a cada elemento
O verificador é uma função pura, e isso é intencional
A lógica das regras não fica dentro do método que se comunica com a DLL. Ela é uma função pura, pública e independente:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Ela recebe um array plano de elementos e retorna um conjunto de problemas. Não chama nenhuma função do PDFium, não abre documentos nem acessa estado global. Essa separação é intencional e traz duas vantagens. A primeira é a testabilidade: você pode criar em um teste unitário um array TPdfStructureElements sintético, com uma Figure sem Alt, uma Formula cujo único texto acessível está em ActualText e duas Notes que compartilham um ID, e verificar o conjunto resultante sem sequer ter pdfium.dll disponível. A lógica das regras é validada offline; o percurso via DLL é verificado separadamente por um teste rápido com documento real, ignorado quando a biblioteca não está disponível
A segunda vantagem é a clareza de responsabilidades. TPdf.ValidatePdfUa cuida da parte complexa, carregando cada página, obtendo seus elementos e acumulando-os, e então entrega um array limpo ao verificador puro. "Obter os dados" (DLL, efeitos colaterais e ciclo de vida) e "avaliar as regras" (lógica pura e determinística) nunca se misturam. Quando uma regra precisa mudar, você altera uma função sem nenhuma operação de entrada ou saída
O que as três regras realmente verificam
A etapa da árvore de estrutura gera três valores de problema, acrescentados ao final de TPdfUaValidationIssues para manter o enum estável na ABI dos chamadores existentes: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt e pvuaiNoteMissingId. O corpo é pequeno o bastante para ser analisado por completo:
for I := 0 to High(Elements) do
begin
T := string(Elements[I].TypeName);
if T = 'Figure' then
begin
// §7.3 — a Figure needs an alternate representation:
// an Alt entry OR ActualText. Flag only when BOTH are empty.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFigureMissingAlt);
end
else if T = 'Formula' then
begin
// §7.7 — same rule as Figure: Alt OR ActualText.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFormulaMissingAlt);
end
else if T = 'Note' then
begin
// §7.9 — every Note must have a unique ID.
NoteId := string(Elements[I].ID);
if NoteId = '' then
Include(Result, pvuaiNoteMissingId)
else
for J := 0 to I - 1 do
if (string(Elements[J].TypeName) = 'Note') and
(string(Elements[J].ID) = NoteId) then
begin
Include(Result, pvuaiNoteMissingId);
Break;
end;
end;
end;
A cláusula 7.3 trata das figuras: um elemento Figure deve fornecer uma alternativa textual. A versão inicial dessa verificação considerava apenas a entrada Alt, o que a tornava mais rígida que os validadores de conformidade. O PDF/UA também aceita uma figura cujo texto acessível seja fornecido por ActualText, pois o texto substituto é uma representação alternativa válida. Por isso, a regra só sinaliza uma Figure quando Alt e ActualText estão ambos vazios. A cláusula 7.7 trata das fórmulas e, após a mesma correção, usa o teste idêntico de Alt ou ActualText. Uma amostra do corpus de conformidade que fornecia o texto acessível de uma Formula apenas por ActualText era rejeitada incorretamente até que a ramificação Formula fosse alinhada à ramificação Figure
A cláusula 7.9 tem outra natureza. Uma Note deve ter um /ID, e esse ID deve ser único em todo o documento. A ausência do ID é uma falha do próprio elemento. Já um ID duplicado representa uma relação entre dois elementos, daí a importância do array plano: para cada Note, o verificador percorre os elementos anteriores e sinaliza uma colisão com qualquer Note precedente que tenha o mesmo ID. O custo evidente é O(n²) em relação ao número de Notes, irrelevante em qualquer documento real, e a função continua sendo um único loop legível, sem índice auxiliar a sincronizar
Acumulando entre páginas para que a unicidade seja global
O PDFium expõe os elementos de estrutura por página, não por documento. Portanto, a orquestração em ValidatePdfUa precisa reuni-los antes de executar as regras. Ela percorre cada página com FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, independentemente da página aberta no componente naquele momento, e acrescenta os elementos de todas as páginas a um único array. Só então chama o verificador puro:
// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
(not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
AllElems := nil;
PageTotal := FPDF_GetPageCount(FDocument);
for I := 0 to PageTotal - 1 do
begin
Page := FPDF_LoadPage(FDocument, I);
if Page = nil then Continue;
try
PageElems := GetStructureElementsForPage(Page);
finally
FPDF_ClosePage(Page);
end;
// append PageElems into AllElems ...
end;
Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;
Esse acúmulo torna correta a verificação de unicidade da cláusula 7.9. Duas Notes em páginas diferentes podem compartilhar um ID; se a validação fosse feita página por página, a colisão nunca apareceria, pois o conjunto de elementos de cada página pareceria internamente consistente. Criar um único array para todo o documento é a única maneira de tornar a duplicidade visível. A condição inicial também merece atenção: o percurso da árvore só acontece quando a análise em nível de bytes não informa pvuaiMissingStructTreeRoot. Um documento sem tags não tem árvore a percorrer e já foi sinalizado pela ausência da raiz de estrutura, portanto o carregamento por página é totalmente ignorado. A análise profunda não tem custo nos documentos que não podem se beneficiar dela
Conservador por design: falhe em silêncio, nunca alerte sem motivo
A característica mais importante deste validador é aquilo que ele se recusa a fazer. Ele compara apenas os nomes de tipo /S padrão retornados diretamente por FPDF_StructElement_GetType: Figure, Formula e Note. Dependendo de como o PDFium resolve o tipo, um documento que define um tipo personalizado e o associa a Figure por um mapa de funções pode informar o próprio nome desse tipo. Nesse caso, o verificador não o reconhece e permanece em silêncio. Isso é um falso negativo e constitui o comportamento intencional. A regra de projeto é informar menos em vez de produzir qualquer falso positivo, pois uma ferramenta de preflight que emite alertas indevidos para arquivos em conformidade ensina os usuários a ignorá-la, e um validador ignorado é pior que nenhum validador. Imagens decorativas ficam no stream de artefatos, não na árvore de estrutura, portanto nem sequer aparecem como Figures; você não receberá uma reclamação de "Alt ausente" para um elemento de fundo corretamente marcado como artefato
Esse também é o motivo para limitar o escopo a três regras. O aninhamento de níveis de título (cláusula 7.4), o escopo de cabeçalhos de tabela (7.5) e a detecção de ciclos no mapa de funções (7.1) são requisitos legítimos do PDF/UA, mas verificá-los corretamente exige análise real de grafos e atributos. Uma implementação ingênua produz exatamente os falsos positivos que o projeto proíbe. O PDF/UA permite padrões de títulos como H1, H2, H3, H3, que uma regra simples de "aumentar sempre" rejeitaria de forma incorreta. Essas verificações ficam a cargo de ferramentas dedicadas de conformidade. O conjunto de Nível 1 abrange apenas os casos em que a ausência de um atributo é inequívoca
O limite, dito sem rodeios
Antes de integrar essa verificação a uma etapa obrigatória de release, é importante conhecer dois limites. Primeiro, o verificador só pode trabalhar com o que o PDFium consegue ler do elemento de estrutura. Alguns arquivos do corpus de conformidade aprovados pelos validadores especializados usam um mecanismo de texto alternativo que o PDFium não expõe. Assim, FPDF_StructElement_GetAltText retorna vazio, embora o arquivo esteja realmente em conformidade. O verificador puro então sinaliza "corretamente" a ausência de Alt em dados incompletos, um falso positivo originado na cobertura dos acessores da DLL, não na lógica da regra. Tornar a regra mais permissiva para absorver esses casos também impediria a detecção das falhas reais que ela deve encontrar. Por isso, esses casos são documentados como uma limitação conhecida do PDFium, sem ocultá-los com uma flexibilização artificial
Segundo, isto é um preflight, não uma certificação. O Nível 1 detecta erros de conteúdo de alta confiabilidade que uma varredura de bytes não consegue identificar estruturalmente e faz isso sem alarmes indevidos. Entretanto, a conformidade completa com PDF/UA, incluindo semântica de títulos, estrutura de tabelas e correção da ordem de leitura, ainda exige um validador completo e, em última instância, revisão humana. Use ValidatePdfUa para interromper seu pipeline de forma rápida e econômica diante de defeitos evidentes, e deixe a decisão final para o veraPDF ou o PAC. O mesmo percurso da árvore de estrutura fundamenta a criação de um leitor de PDF acessível no Delphi, no qual a árvore de tags determina a ordem de leitura e o texto falado, além de complementar o trabalho em nível de metadados na revisão de anotações PDF no Delphi
As APIs da árvore de estrutura e o validador ValidatePdfUa apresentados aqui acompanham o PDFium Component para Delphi e C++Builder (VCL) e Lazarus/FPC (LCL). A página do produto contém o link para a referência completa da API, incluindo o layout integral do registro TPdfStructureElement e o enum de problemas usado nessas verificações