Artigo Técnico

Marcação Automática de Estrutura de PDF Acessível em Delphi

O PDFlibPas consegue marcar um documento enquanto este é desenhado. Ligue SetAutoTagMode e as chamadas comuns de DrawText passam a parágrafos, o texto desenhado logo depois de RegisterHeading passa a um título desse nível, os cabeçalhos e rodapés correntes passam a artefactos que um leitor ignora, as imagens passam a figuras, e DrawTableRows transporta a tabela, as suas linhas e as suas células para a árvore de estrutura

A alternativa — e até há pouco tempo a única opção — era envolver cada chamada de desenho em BeginTag e EndTag à mão. Isso funciona, e para documentos com estrutura invulgar continua a ser a ferramenta certa. Para o relatório, a fatura ou o extrato comuns, significa que a acessibilidade da saída depende de ninguém se esquecer de um par, em todos os caminhos de código que desenham alguma coisa

O que os bits do modo cobrem

SetAutoTagMode recebe uma máscara de bits e devolve o modo anteriormente em vigor. AUTOTAG_TEXT (1) marca o texto como parágrafo, ou como título quando um é devido. AUTOTAG_FURNITURE (2) marca cabeçalhos, rodapés e números de página correntes como artefactos. AUTOTAG_FIGURE (4) transforma uma imagem desenhada numa figura, ou num artefacto quando foi declarada decorativa. AUTOTAG_TABLE (8) transporta as tabelas desenhadas para a árvore de estrutura. AUTOTAG_DEFAULT é 15, ou seja, os quatro

Ligar o modo também marca o documento como marcado, e esse passo é menos cosmético do que parece. Um leitor considera um documento como não marcado a menos que o catálogo diga o contrário (ISO 32000-1 §14.7.1), pelo que um ficheiro que transporta uma árvore de estrutura completa sem qualquer declaração /MarkInfo é anunciado pelas tecnologias de apoio como não tendo estrutura nenhuma. A árvore está lá; nada a lê

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutoTagMode(AUTOTAG_DEFAULT);   // text + furniture + figures + tables
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.RegisterHeading(1, 'Annual service report');
    Lib.DrawText(72, 96, 'Annual service report');   // becomes H1
    Lib.SetTextSize(11);
    Lib.DrawText(72, 130, 'Every unit installed before 2024 was inspected.');
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Como é que um título sabe a que texto pertence?

RegisterHeading nomeia o nível para o próximo texto desenhado, e fica à espera de texto. Se for desenhada uma imagem entretanto, a imagem passa a figura e o título fica pendente para o texto que se seguir. Esse comportamento é deliberado: a alternativa, em que a imagem assumiria o nível do título, produzia documentos em que um traço decorativo por baixo de um título era anunciado como o título

A mesma regra de "consumido por um item" rege as figuras. RegisterFigure fornece a descrição que a próxima imagem transporta, e RegisterDecoration declara a próxima imagem como um traço, margem ou fundo que não transporta significado. Ambos são consumidos por uma imagem, pelo que uma imagem posterior nunca herda uma descrição destinada a uma anterior — que é como o texto alternativo acaba associado à imagem errada em código marcado à mão

A descrição importa mais do que qualquer outra cadeia isolada num documento acessível. Um leitor não vidente recebe a descrição em vez da imagem, e isso é tudo o que recebe. "Gráfico" não é uma descrição; "Receita trimestral por região, com a região este mais alta no Q3" é

Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0);      // becomes a tagged Figure

Lib.RegisterDecoration;                       // meaningless rule
Lib.AddImageFromFile('divider.png', 0);       // drawn inside a layout artifact

Tabelas, cabeçalhos e onde vive a decisão de repetição

Com o bit de tabela ligado, DrawTableRows transporta a tabela, as suas linhas e as suas células para a árvore de estrutura, pelo que um leitor consegue dizer em que coluna se encontra um valor em vez de ler a tabela inteira como uma sequência de texto sem relação. SetTableHeaderRowCount nomeia quantas linhas iniciais são cabeçalhos; essas linhas são escritas como células de cabeçalho com um âmbito de coluna, o que é o que permite a um leitor anunciar a legenda do valor em que o utilizador está

As linhas de cabeçalho nomeadas desta forma ficam onde estão. Repeti-las no topo de cada página é uma decisão de composição, e continua a sê-lo: DrawTaggedTableRows recebe um argumento RepeatHeaderRows exatamente para esse fim. Manter os dois separados evita que a árvore de estrutura adquira uma segunda cópia do cabeçalho por cada quebra de página, que é o que uma repetição automática produziria

var
  TableID: Integer;
begin
  TableID := Lib.CreateTable(40, 3);
  Lib.SetTableHeaderRowCount(TableID, 1);       // row 1 is the header band
  Lib.SetTableCellContent(TableID, 1, 1, 'Part');
  Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
  Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
  // ... fill the data rows ...
  // Draw rows 1..40 into a 600pt band, repeating one header row per page
  Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;

Misturar marcação automática com marcação manual

A marcação automática afasta-se dentro de uma etiqueta aberta à mão. Parte de um documento pode ser descrita pelo seu código e o resto deixada à biblioteca, sem que os dois se aninhem um no outro — que é o arranjo que a maioria dos documentos reais quer. A capa e o bloco de assinatura têm estrutura que só o programador percebe; as duzentas páginas de texto do corpo no meio não

Duas regras de segurança mantêm a saída limpa. Nada é marcado dentro de um artefacto, porque o conteúdo marcado como artefacto não pode transportar qualquer elemento de estrutura. E texto vazio não abre qualquer elemento, pelo que um DrawText disperso com uma cadeia vazia não consegue produzir um elemento de estrutura que um leitor anunciaria como estando em branco. Ambos são do tipo de defeito que os documentos marcados à mão acumulam silenciosamente e que um validador reporta em massa meses depois

O que a marcação automática ainda não decide por si

A ordem de leitura para além da ordem de desenho, papéis semânticos que não sejam parágrafo, título, figura ou tabela, e declarações de idioma. A marcação automática atribui estrutura pela ordem em que o conteúdo é desenhado — se o código de composição desenhar a barra lateral antes do corpo, essa é a ordem que a árvore regista. Para documentos em que a ordem visual e a ordem de leitura diferem genuinamente, a API de marcação manual continua a ser a ferramenta certa, e o guia de PDF marcado e estrutura de acessibilidade cobre papéis, âmbitos e vínculos de cabeçalho em detalhe

Quando o documento estiver concluído, valide em vez de assumir: as notas sobre preflight PDF/A e PDF/UA mostram como obter um veredito sobre a estrutura que produziu, e o guia de exportação de relatórios orientada por dados aborda onde estas chamadas se encaixam num motor de relatórios que gera a sua composição a partir de dados

O PDFlibPas é uma biblioteca PDF nativa em Pascal para Delphi, C++Builder e Lazarus sem qualquer runtime PDF externo, pelo que a saída acessível é produzida pelo mesmo código que desenha o documento — veja a página do produto PDFlibPas para a lista completa de API e plataformas