Technical Article

Achatando Hiperlinks Rich-Text XFA em Links PDF no Delphi

A arquitetura XFA (XML Forms Architecture) está obsoleta. A ISO 32000-1 a mantém na seção §12.7 com a nota de que foi removida no PDF 2.0, e os visualizadores modernos estão desativando seus motores XFA um a um. Nada disso esvaziou os arquivos. Formulários de admissão governamentais, propostas de seguros e extratos bancários foram criados como XFA por quase duas décadas, e esses arquivos continuam chegando em caixas de entrada e fluxos de documentos atualmente. Quando o visualizador que costumava renderizá-los deixa de fazê-lo, o formulário se transforma em uma página em branco com um aviso do tipo "abra em um leitor diferente". A correção definitiva é achatar (flatten) o XFA em conteúdo PDF estático que qualquer leitor possa desenhar.

A parte difícil desse achatamento não são os campos. Caixas de texto e caixas de seleção se mapeiam para os widgets do AcroForm de maneira bastante direta. O problema real está no rich text que o XFA armazena dentro de um elemento gráfico (draw), em um bloco <exData contentType="text/html">. Esse bloco é um subconjunto de HTML com estilos embutidos (inline) e, frequentemente, links (âncoras). Exibi-lo na página significa reproduzir tanto o texto estilizado quanto os hiperlinks ativos, e os hiperlinks são o ponto onde a maioria das implementações desiste silenciosamente.

Como o rich text XFA realmente se apresenta

Um corpo exData é uma pequena fração de XHTML. Um parágrafo é um <p>; um trecho de caracteres estilizado é um <span> com seu próprio CSS embutido para peso, inclinação, cor e tamanho; e um hiperlink é uma <a href="..."> envolvendo seu texto visível. Uma única linha pode conter vários spans seguidos, cada um com estilos diferentes, e um deles pode ser uma âncora. O estilo não é um adorno que possa ser descartado. Uma cláusula renderizada em vermelho e negrito porque representa um aviso legal deve permanecer em vermelho e negrito após o achatamento, sob pena de o documento achatado descaracterizar o original.

Portanto, o motor de achatamento não pode tratar o bloco como uma única string. Il precisa percorrer a estrutura interna, determinar o estilo resultante de cada trecho sobrepondo o CSS do span à fonte básica do elemento de desenho e distribuir os trechos um após o outro na linha. O HotPDF modela cada um desses fragmentos posicionados como um registro interno do tipo TXFARichRun. O registro carrega o texto do trecho, seu estilo resultante, sua caixa medida (box) e, para uma âncora, o endereço Href para o qual ela aponta.

Posicionando os trechos da esquerda para a direita

O posicionamento é o momento em que o rich text deixa de ser um problema de análise e se torna um desafio de diagramação de texto. Os trechos compartilham uma linha, então cada um começa onde o anterior terminou. Não há marcação que registre essas posições; elas precisam ser medidas. A rotina interna LayoutRichText do motor mede cada trecho com as mesmas métricas de fonte que serão usadas posteriormente para pintá-lo e, em seguida, define o deslocamento horizontal do trecho como a soma contínua das larguras de todos os trechos anteriores. O primeiro trecho começa na origem da caixa de desenho, o segundo começa na largura do primeiro, o terceiro na largura combinada dos dois primeiros e assim por diante ao longo da linha.

É por isso que o alinhamento da fonte de medição é tão crítico. A etapa de layout mede os avanços; uma etapa de renderização separada desenha os glifos. Se essas duas etapas divergirem quanto à fonte, as caixas que o layout calculou não corresponderão aos glifos que o renderizador pinta. O HotPDF os mantém em sincronia mapeando o estilo resultante de cada trecho em uma especificação de fonte, por meio do assistente interno RunStyleToFontSpec, que corresponde aos padrões do renderizador de Arial a 10 pontos. O avanço medido e o texto desenhado passam a coincidir, e a caixa calculada do trecho de fato cobre os caracteres que o leitor vê na tela.

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

De um trecho de âncora para uma anotação de Link do PDF

Um hiperlink em um PDF finalizado não faz parte do conteúdo da página. Trata-se de um objeto separado, uma anotação de Link (Link annotation), descrita na ISO 32000-1 §12.5.6.5. A anotação possui uma /Rect que define o retângulo clicável na página e uma ação que é disparada quando o retângulo é clicado. Para um link externo, a ação é uma ação de URI: /S /URI com o endereço de destino como sua string /URI. O texto visível embaixo é o conteúdo comum da página; a anotação é a área ativa invisível sobreposta a ele.

O processo de achatamento segue exatamente esse modelo. Quando um trecho carrega um Href, o HotPDF primeiro desenha o texto estilizado e, em seguida, cria uma anotação de Link sobre a caixa do trecho. O ponto de entrada público para essa anotação é o método de página AddURILink, que cria o objeto /Type /Annot /Subtype /Link com uma ação /URI e retorna o dicionário da anotação. Seu retângulo é a caixa medida do trecho, traduzida das coordenadas locais do elemento de desenho para as coordenadas da página. O resultado é um link que cobre precisamente o texto da âncora e nenhuma outra área.

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Por que a área de clique (hit box) deve vir das larguras medidas

É tentador imaginar a localização do link pesquisando na página seu texto visível e desenhando o retângulo ao redor do que for encontrado. Isso não funciona, e o motivo é fundamental para a forma como o texto achatado é armazenado. Os trechos estilizados são desenhados com fontes de subconjuntos incorporadas (subset fonts). Uma fonte de subconjunto renomeia os glifos que mantém, de modo que o fluxo de conteúdo da página carrega códigos CID hexadecimais, e não os códigos de caracteres originais. Os bytes na página não representam as letras lidas por uma pessoa e não podem ser pesquisados como texto. Uma busca pelo título da âncora não encontra nada, porque esse título não existe como texto literal em nenhum lugar do fluxo.

A única referência confiável para o retângulo é a geometria que a etapa de layout já produziu. O deslocamento e a largura medida de cada trecho foram calculados enquanto a linha era processada, antes de qualquer glifo ser renomeado, e eles descrevem onde o texto de fato aparecerá. O HotPDF, portanto, define o retângulo do link diretamente a partir da caixa posicionada do trecho, e não a partir de qualquer busca de texto. Como a medição utilizou a fonte de renderização, a caixa está correta independentemente da incorporação em subconjunto. A geometria sobrevive à codificação; o texto não. Esse é o argumento principal para o posicionamento por largura medida, e explica por que um achatador que tenta reconstruir links por meio de busca de texto gera áreas de clique que se deslocam ou desaparecem.

Controlando o achatamento a partir do seu código

Para um PDF que já contém um pacote XFA, o ponto de entrada é FlattenLoadedXFA. Carregue o documento, chame o método e salve o resultado. O parâmetro Editable decide o que acontece com os campos de formulário: passe True para mantê-los como widgets interativos do AcroForm, ou False para marcar cada widget como somente leitura, para que o resultado seja um registro estático. Os blocos de desenho de rich text, com seus trechos estilizados e anotações de link, são criados de qualquer maneira. A função retorna a contagem de widgets emitidos.

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Sempre leia XFAFlattenWarnings após a chamada. A lista é limpa no início de cada achatamento e acumula uma linha para cada elemento que o motor recusou renderizar: um tipo de campo não suportado, uma imagem de desenho que não pôde ser decodificada, um bloco exData sem trechos válidos. Nenhum desses casos gera uma exceção, de modo que uma lista de avisos vazia é a prova de que tudo foi mapeado, e uma lista com avisos informa exatamente quais elementos originais devem ser inspecionados. Quando você tem o XFA bruto como bytes XDP, em vez de um PDF carregado, o método correspondente ApplyXFAAsAcroForm recebe esses bytes diretamente e compartilha do mesmo fluxo de código e comportamento de avisos. O método complementar AddXFAPacket atua na direção oposta, incorporando um pacote XFA em um documento que você está criando.

Confirming the result in a reader

Abra o arquivo achatado no Acrobat ou em qualquer visualizador atual e verifique duas coisas. Primeiro, se o rich text foi renderizado com seu estilo intacto: os trechos em negrito estão em negrito, os trechos coloridos carregam sua cor e as faixas de texto se posicionam na ordem correta na linha, sem se sobrepor ou sair dos limites da caixa. Segundo, se os hiperlinks estão ativos. Posicione o ponteiro sobre uma âncora e a barra de status deve exibir o endereço de destino; clique nela e a ação de URI deve abri-la. Use o inspetor de anotações do visualizador para confirmar se cada link é uma anotação /Link real cuja /Rect abraça o texto da âncora, posicionada sobre conteúdo que agora consiste em glifos desenhados estáticos, e não em XFA renderizado pelo formulário. Essa combinação - texto estático estilizado mais anotações de Link reais nos retângulos corretos - é o que faz o documento achatado durar muito mais que os motores XFA dos quais ele não precisa mais.

O achatamento dos campos em si - caixas de texto, caixas de seleção e listas de opções que envolvem esse rich text - é abordado em nosso passo a passo sobre achatamento de formulários XFA em widgets do AcroForm. Para a visão mais ampla de criação e posicionamento manual de anotações de Link, além daquelas geradas pelo processo de achatamento, consulte como trabalhar com anotações em PDF no HotPDF. Ambos se baseiam no mesmo modelo de anotações e formulários fornecido com o HotPDF Component para Delphi e C++Builder.