Technical Article

Formulários PDF Interativos em Delphi: Ações e JavaScript

Um campo de formulário PDF por si só é apenas uma caixa que contém um valor. O que faz um formulário comportar-se como uma pequena aplicação é a ação associada a ele: um clique que oculta uma secção, obtém valores guardados de um ficheiro, salta para a última página ou executa um script que soma uma coluna. Nada disto reside no campo. Reside num dicionário de ações, e a norma ISO 32000-1 organiza toda a família na secção 12.6. Este artigo aborda as ações que um programa Delphi utiliza com mais frequência e mostra como o PDFlibPas liga cada uma a um campo ou hiperligação.

O modelo mental a reter é que um campo e uma ação são objetos separados unidos por uma referência. Uma anotação de controlo (widget) ou uma anotação de hiperligação transporta uma ação na sua entrada /A. A ação identifica o campo sobre o qual atua pelo título, não pelo índice, por isso o título que atribui a um campo é o identificador que qualquer ação posterior utiliza para o encontrar. Uma vez esclarecida esta divisão, a API deixa de parecer um conjunto desordenado de chamadas e passa a assemelhar-se a um padrão único aplicado a quatro tipos de verbos.

Ações nomeadas: navegação sem número de página

As ações mais simples não contêm quaisquer parâmetros. A norma ISO 32000-1, secção 12.6.4.11, Tabela 194, define ações nomeadas: o visualizador interpreta um nome simbólico em tempo de execução em vez de seguir um destino guardado. São universalmente suportados quatro nomes, e são exatamente aqueles que um leitor espera de uma barra de ferramentas: NextPage, PrevPage, FirstPage e LastPage. Como o destino é relativo à página que o visualizador está a mostrar no momento, um botão Seguinte criado desta forma funciona em todas as páginas sem que tenha de calcular um destino.

No PDFlibPas, uma ação nomeada é associada a um retângulo ativo na página atual. O quarto e o quinto argumentos inteiros selecionam o verbo e a aparência.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // jump to last page

Não há nenhum destino para manter sincronizado, o que é o principal objetivo. Uma ação nomeada sobrevive à inserção e eliminação de páginas porque nunca nomeia uma página em primeiro lugar. Confronte isto com uma hiperligação direta de redirecionamento, que guarda um índice de página de destino que teria de renumerar no momento em que o documento crescesse.

A ação Hide e a armadilha da matriz

A ação Hide, ISO 32000-1, secção 12.6.4.10, Tabela 196, alterna a visibilidade de um ou mais campos. É a forma mais limpa de construir comportamentos de mostrar e ocultar sem recorrer a scripts, sendo ideal para uma ligação de Mostrar detalhes ou para dois painéis mutuamente exclusivos onde a revelação de um oculta o outro. A ação transporta um destino na sua entrada /T e um booleano /H que decide a direção: ocultar quando verdadeiro (true), mostrar quando falso (false).

A subtileza reside inteiramente na forma como esse destino é codificado, e é o tipo de detalhe que produz um formulário que funciona na sua máquina e falha na do cliente. Quando a ação nomeia um único campo, /T é escrito como uma única cadeia de texto. Quando nomeia vários, /T é escrito como uma matriz (array) de cadeias de texto. Os visualizadores mais antigos não tratam uma matriz de um único elemento da mesma forma que tratam uma cadeia simples, pelo que a codificação tem de ramificar com base na contagem: um único nome deve ser emitido como uma cadeia de texto, e não como uma matriz de comprimento um, para que a maior variedade de leitores o possa honrar. O PDFlibPas toma essa decisão por si. Só precisa de passar os nomes dos campos separados por vírgulas, pontos e vírgulas ou quebras de linha, e o escritor emite uma única cadeia de texto para um nome e uma matriz para dois ou mais.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Como a ação não faz referência a nenhum recurso externo, permanece compatível com PDF/A. Os nomes que passa são títulos de campos totalmente qualificados, razão pela qual um campo descendente dentro de um grupo tem de ser endereçado através do seu caminho completo separado por pontos e não apenas pelo seu nome simples.

ImportData: pré-preenchimento a partir de FDF

Os formulários interativos muitas vezes necessitam de ser pré-preenchidos. A norma ISO 32000-1, secção 12.6.4.8, Tabela 198, define a ação de importação de dados como um mecanismo que preenche o AcroForm a partir de um ficheiro de Formato de Dados de Formulários (FDF) no disco. Esta é a ação por trás de controlos como Recarregar dados de exemplo ou Repor predefinições, onde um ficheiro FDF é fornecido juntamente com o PDF e contém os valores canónicos dos campos. A chamada reflete as outras, recebendo o retângulo ativo, o caminho para o FDF e uma máscara de bits de aparência: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). O ficheiro não necessita de existir quando o PDF é criado, mas deve estar presente quando o utilizador clica, e quaisquer barras invertidas no caminho são reescritas na forma de barra canónica do PDF.

Vale a pena agradecer a existência de restrições de conformidade. Uma ação de importação de dados aponta para um ficheiro externo, pelo que não é permitida em PDF/A. Quando o documento está no modo PDF/A, a chamada devolve zero e não adiciona nada, em vez de produzir um ficheiro que falharia na validação. Se o seu fluxo de trabalho visa uma saída para arquivo, o pré-preenchimento tem de ocorrer no momento da geração, escrevendo diretamente os valores dos campos, e não adiando-os para um clique.

JavaScript: pacotes globais e scripts por ação

Para lógicas que vão além de mostrar, ocultar e importar, a família de ações estende-se ao JavaScript ao nível do documento. Existem dois locais distintos onde um script pode residir, e a diferença é importante. Um pacote de JavaScript ao nível do documento é guardado uma vez para todo o ficheiro e é executado quando o documento abre, o que o torna o local ideal para definições de funções e estados partilhados. Um script por ação está associado a uma única hiperligação ou campo e é executado apenas quando esse objeto é ativado, sendo o local ideal para a linha única que chama uma função que o pacote já definiu.

O PDFlibPas expõe ambos. O método AddGlobalJavaScript guarda um pacote nomeado ao nível do documento; reutilizar um nome substitui o que quer que estivesse guardado sob o mesmo. O método AddLinkToJavaScript associa um script a um retângulo ativo para que um clique o execute.

// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Manter a função no pacote global e a chamada na hiperligação não é uma preferência de estilo. Evita a duplicação do mesmo corpo em cada controlo que necessite dele, e significa que um visualizador com scripts desativados simplesmente não faz nada ao clicar, em vez de falhar com um bloco inline malformado. Também mantém pequenas as entradas de cada ação, o que facilita a leitura do ficheiro quando o inspecionar mais tarde.

Campos, campos descendentes e congelar o resultado

As ações necessitam de campos para atuar, por isso ajuda ver como um campo é criado. O método NewFormField cria um campo na página atual e devolve o seu índice; o tipo inteiro seleciona o tipo de campo, onde 1 é Texto, 2 é Botão de pressão, 3 é Caixa de seleção, 4 é Botão de opção, 5 é Escolha, 6 é Assinatura e 7 é um Pai que possui descendentes mas não desenha nada por si próprio. O título que passa não pode conter um ponto, porque o ponto é o separador nos nomes totalmente qualificados que as ações usam para endereçar os campos descendentes.

Os grupos de botões de opção e os formulários hierárquicos são construídos atribuindo campos descendentes a um campo pai. O método NewChildFormField adiciona um descendente sob um pai nomeado e, para os casos de botões de opção e escolha, o AddFormFieldSub adiciona as opções individuais e devolve um índice temporário que utiliza para posicionar cada uma. Quando a fase interativa termina e deseja congelar um campo para que a sua aparência atual se torne conteúdo permanente da página, o método FlattenFormField desenha o campo na página e remove-o do formulário. Após a simplificação (flatten), os índices dos campos seguintes recuam uma posição, o que é o único detalhe a reter se simplificar vários campos num ciclo (loop).

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

A chamada de simplificação (flatten) está comentada de propósito. Se a omitir, o documento é enviado como um formulário ativo cujas ações são acionadas no leitor. Se a ativar, o campo é convertido em marcas estáticas, que é o que deseja quando o formulário foi preenchido e o resultado deve circular como um registo fixo. O mesmo campo, o mesmo código, dois documentos muito diferentes dependendo se o congela ou não.

Escolher o verbo correto

As quatro ações dividem-se claramente pelo que afetam. Uma ação nomeada move a área de visualização (viewport) e não necessita de campo. A ação Hide altera a visibilidade e necessita de títulos de campos, com a codificação de cadeia-versus-matriz a ser gerida por si. Uma ação de importação de dados acede a um ficheiro no disco e, por isso, está fora do âmbito do PDF/A. Uma ação JavaScript executa lógica arbitrária e é melhor dividida entre um pacote global de funções e pequenas chamadas por ação. Utilize a mais simples que cumpra o objetivo: uma ação Hide é mais portátil do que um script que define uma propriedade oculta, e uma ação nomeada é mais duradoura do que um destino de página guardado porque não há nenhum número para manter.

A partir daqui, dois tópicos relacionados completam o quadro. Se o formulário faz parte de um documento acessível, a árvore de estrutura percorrida pelos leitores de ecrã é abordada no nosso artigo sobre PDF etiquetado e estrutura de acessibilidade. Quando o formulário preenchido tem de ser bloqueado e assinado, o fluxo de trabalho é descrito no guia prático do workbench de conformidade e assinatura. Todos os três baseiam-se no mesmo motor, que é fornecido como a Biblioteca PDF para Delphi, juntamente com as APIs de criação, formulário e assinatura abordadas noutros locais deste blogue.