Technical Article

Construir Árvores de Estrutura de PDF Etiquetados no Delphi com PDFlibPas

Um PDF acessível baseia-se numa estrutura que a página visível nunca mostra: a árvore de estrutura definida na norma ISO 32000-1 §14.7. Trata-se de uma hierarquia lógica de cabeçalhos, parágrafos, tabelas e figuras, disposta sobre o conteúdo desenhado e mapeada para funções padrão através de um mapa de funções (role map). Um leitor de ecrã lê essa árvore, e não as marcas na página. Sem ela, uma fatura gerada com aspeto impecável é semanticamente vazia, porque o fluxo de conteúdo regista apenas a ordem de desenho e nada mais. O total pode ser anunciado antes dos itens de linha, o rodapé pode cortar um parágrafo ao meio e a tabela de itens pode colapsar numa sequência indiferenciada de palavras. O custo de evitar isto é extremamente vantajoso para si. Emitir a estrutura enquanto desenha exige apenas alguns minutos de código; adaptá-la posteriormente a documentos finalizados é um projeto complexo de correção. A biblioteca PDF losLab (PDFlibPas) expõe a árvore ao Delphi e ao C++Builder através de um conjunto reduzido de chamadas que envolvem cada operação de desenho na sua função lógica.

Como o conteúdo marcado se liga à árvore de estrutura

Duas camadas cooperam entre si. No fluxo de conteúdo, as operações de desenho são delimitadas em sequências de conteúdo marcado, cada uma contendo um MCID numérico inteiro. No catálogo do documento, a árvore de estrutura mapeia esses MCIDs numa hierarquia de elementos tipificados (H1, P, Table, Figure) com atributos como texto alternativo e idioma. Os tipos de elementos personalizados são permitidos, mas cada um deve ser resolvido para uma função padrão através do mapa de funções (ISO 32000-1 §14.8.4). O conteúdo sem significado semântico, como linhas delimitadoras, fundos e decorações de página repetidas, é marcado como um artefacto para que a tecnologia de apoio o ignore, em vez de o ler a meio de uma frase.

O PDFlibPas mantém ambas as camadas por trás de um único par de delimitadores. O BeginTag abre um elemento de estrutura e inicia a sequência de conteúdo marcado, as chamadas de desenho ocorrem dentro deste e o EndTag fecha ambos. O trabalho de gestão que costuma complicar a etiquetagem manual (os MCIDs, a árvore mãe e as referências de página) é realizado internamente para que não ocorram erros.

Duas opções ao nível do documento estruturam o trabalho antes de qualquer etiqueta ser aberta. O SetMarkInfo escreve a flag do catálogo que declara o documento como etiquetado, e o IsTaggedPDF lê-a de volta, sendo este o primeiro teste simples para decidir se um ficheiro recebido possui alguma estrutura que valha a pena preservar. O idioma tem dois pontos de entrada. O SetDocumentLanguage define a predefinição do documento por si só, enquanto o SetPDFUAMode define-o como parte da ativação da saída PDF/UA completa. Um ficheiro pode ser devidamente etiquetado sem alegar conformidade PDF/UA, e uma implementação faseada começa frequentemente por aí.

Etiquetar durante o desenho, não depois

O padrão de geração que funciona consiste em tratar o delimitador de etiqueta como parte da assinatura de cada chamada de desenho, e nunca como uma etapa posterior:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // top-left origin
    Lib.SetPDFUAMode('en-US');                 // bumps the save version to PDF 1.7
    Lib.SetInformation(1, 'Service Manual');   // /Title is mandatory for PDF/UA
    Lib.AddRoleMap('ManualTitle', 'H1');       // custom type -> standard role
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // page decoration: excluded from reading
    // ... draw rules and background tint ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Três chamadas nessa sequência carregam peso de conformidade. O SetPDFUAMode ativa a saída PDF/UA e eleva silenciosamente a versão do documento para PDF 1.7, o que colide com a fixação de versões. Um documento bloqueado em PDF 1.4 com LockSaveVersion recusa-se a guardar e devolve o código de erro 602 quando o modo UA está ativo, um conflito que costuma surgir quando os perfis de arquivo e os requisitos de acessibilidade são configurados por equipas diferentes. O SetInformation(1, ...) escreve o título do documento, que a norma ISO 14289 espera que os visualizadores mostrem em vez do nome do ficheiro; a sua ausência é uma das falhas de PDF/UA mais comuns na prática. O AddRoleMap regista o tipo personalizado ManualTitle como um H1, e a sua omissão faz com que os diagnósticos descritos abaixo sinalizem uma função não mapeada.

Os níveis de cabeçalho merecem uma política deliberada, e não escolhas ad-hoc feitas apenas pela aparência da página. Os utilizadores de leitores de ecrã saltam entre secções através de atalhos de cabeçalho, pelo que um modelo que passe de H1 para H3 porque o nível intermédio parecia demasiado grande no design visual quebra silenciosamente essa navegação, e nenhuma revisão visual detetará o problema. É exatamente este defeito que o diagnóstico HEADING-LEVEL-SKIP visa assinalar. Mapeie os styles visuais de cada modelo para uma estrutura de cabeçalhos fixa apenas uma vez, num único local, e o desvio nunca ocorrerá.

Tabelas que um leitor de ecrã consegue realmente navegar

As linhas de grelha desenhadas não significam nada fora do ecrã. O que os leitores de ecrã navegam são as relações estruturais: quais as células que são cabeçalhos, o que cada cabeçalho governa e como as células de dados se ligam aos cabeçalhos em esquemas irregulares. As chamadas de atributos de elementos de estrutura tratam de todos estes três pontos:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // valid only while this TH is open
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header spans the value and unit columns
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // explicit binding for irregular tables
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

A regra de ordenação é estrita e aplicada em silêncio. Cada chamada SetStructElem* aplica-se à etiqueta que está aberta nesse momento, entre o seu BeginTag e o seu EndTag, e devolve 0 sem gerar qualquer exceção quando nenhuma etiqueta está aberta ou quando o atributo não se aplica à etiqueta atual. Uma chamada mal colocada simplesmente desaparece. Envolver os valores de retorno em asserções durante o desenvolvimento deteta o problema enquanto este ainda é visível; caso contrário, a falta de escopo (scope) só aparecerá quando uma auditoria de acessibilidade correr um leitor de ecrã real pela tabela. Os IDs de elemento passados através do BeginTagEx2 alimentam a árvore de IDs (ISO 32000-1 §14.7.4), sendo isso que torna a ligação de SetStructElemHeaders resolúvel em primeiro lugar.

A mesma família de atributos cobre o resto do suporte utilizado pelas tecnologias de apoio. O SetStructElemListNumbering declara como os itens de lista são etiquetados, para que o leitor de ecrã anuncie a posição na lista em vez de ler os glifos de marcadores. O SetStructElemBBox regista a caixa delimitadora (bounding box) de figuras e tabelas, que as vistas de refluxo (reflow) usam para posicionar o conteúdo. O SetStructElemActualText fornece texto de substituição para trechos cujos glifos não se mapeiam para caracteres legíveis, como uma letra capitular desenhada com arte vetorial. Cada um segue a mesma regra: liga-se à etiqueta aberta, ou desaparece.

Artefactos, idioma e validação de diagnósticos antes de guardar

Elementos repetidos da página, como cabeçalhos, marcas de dobra, marcas de água e tonalidades de fundo, devem estar dentro dos blocos BeginArtifact e EndArtifact para nunca entrarem no fluxo de leitura. O idioma é herdável. A predefinição do documento provém do argumento de SetPDFUAMode, e um trecho noutro idioma substitui-o por elemento através de BeginTagEx ou SetStructElemLang. É isso que mantém uma citação em francês pronunciável dentro de um manual em inglês.

Antes de guardar, o GetPDFUADiagnostics executa as verificações estruturais da biblioteca sobre o documento em memória e devolve os resultados sob a forma de texto, onde uma string vazia significa que nada foi detetado. Os códigos indicam diretamente os erros clássicos de criação: FIGURE-NO-ALT para uma imagem sem texto alternativo, HEADING-LEVEL-SKIP para um H3 após um H1, e ROLEMAP-UNMAPPED para um tipo personalizado que nunca foi registado. Integrar isto no processo de compilação (gerar o conjunto de documentos, falhar a etapa em caso de diagnósticos não vazios) torna as regressões de acessibilidade em falhas semelhantes a erros de compilação, em vez de descobertas de auditoria meses mais tarde. O veredicto de conformidade total ainda pertence ao preflight do ficheiro guardado, abordado em preflight PDF/A e PDF/UA no Delphi, uma vez que algumas normalizações só são aplicadas durante a serialização.

A navegação de anotações tem o seu próprio controlo. O PDF/UA espera que a navegação por teclado de campos de formulário e hiperligações siga a ordem da estrutura, e o SetTabOrderMode escreve a entrada de ordem de tabulação ao nível da página que os leitores respeitam, estando o GetTabOrderMode disponível para auditar ficheiros recebidos. É o tipo de requisito ao qual ninguém presta atenção até que um utilizador que navega apenas por teclado reporte o problema, custando uma chamada por documento para ser resolvido corretamente.

As árvores de estrutura não sobrevivem a todas as mesclagens

Os documentos etiquetados só permanecem etiquetados se cada etapa posterior de processamento preservar a árvore, e a margem de risco no PDFlibPas reside na família de funções de lista de mesclagem. O MergeFileListFast abdica da preservação da árvore de estrutura a favor da velocidade. Esse é o compromisso certo para lotes de imagens digitalizadas e o errado para relatórios etiquetados, pois o resultado abre normalmente, renderiza de forma idêntica e perdeu silenciosamente a sua camada de acessibilidade. Utilize a função padrão MergeFileList ou a variante estrita sempre que qualquer entrada esteja etiquetada, e torne o IsTaggedPDF parte das asserções pós-montagem para que um lote unificado não possa ser enviado sem que alguém se aperceba. Os fluxos de montagem para grandes conjuntos de documentos envolvem mais compromissos deste tipo, explorados em mesclagem, divisão e acesso direto a PDFs grandes.

O ciclo de verificação encerra-se fora da biblioteca: abra o resultado no Acrobat, inspecione o painel de etiquetas e leia pelo menos um documento por família de modelos com um leitor de ecrã real. Os diagnósticos detetam erros estruturais; apenas o ouvido humano consegue perceber se uma ordem de leitura é tecnicamente válida mas praticamente confusa. As versões de avaliação e a referência completa da API de etiquetagem estão disponíveis na página do produto losLab PDF Library for Delphi.