Artigo Técnico

Validação da árvore de estrutura PDF/UA no Delphi com PDFium

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