Um campo de formulário PDF por si só é apenas uma caixa que contém um valor. O que faz um formulário se comportar como um pequeno aplicativo é a ação anexada a ele: um clique que oculta uma seção, recupera valores salvos de um arquivo, pula para a última página ou executa um script que soma uma coluna. Nada disso reside no campo. Ele reside em um dicionário de ações, e a ISO 32000-1 organiza toda a família na §12.6. Este artigo aborda as ações que um programa Delphi utiliza com mais frequência e mostra como o PDFlibPas conecta cada uma delas a um campo ou link.
O modelo mental válido é que um campo e uma ação são objetos separados unidos por uma referência. Uma anotação de widget ou uma anotação de link carrega uma ação em sua entrada /A. A ação identifica o campo no qual opera por título, não por índice, de modo que o título que você atribui a um campo é o identificador que toda ação posterior utiliza para localizá-lo. Uma vez clara essa separação, a API deixa de parecer um conjunto aleatório de chamadas e passa a ser vista como um único padrão aplicado a quatro tipos de verbos.
Ações nomeadas: navegação sem um número de página
As ações mais simples não carregam nenhum parâmetro. A ISO 32000-1 §12.6.4.11, Tabela 194, define as ações nomeadas: o visualizador interpreta um nome simbólico em tempo de execução em vez de seguir um destino armazenado. Quatro nomes são suportados universalmente, e são exatamente os que um leitor espera de uma barra de ferramentas: NextPage, PrevPage, FirstPage e LastPage. Como o destino é relativo a qualquer página que o visualizador esteja exibindo no momento, um botão Avançar criado dessa maneira funciona em todas as páginas sem que você precise calcular um destino.
No PDFlibPas, uma ação nomeada é anexada 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á destino para manter sincronizado, o que é a grande vantagem. Uma ação nomeada sobrevive à inserção e exclusão de páginas porque nunca especifica uma página em primeiro lugar. Compare isso com um link go-to explícito, que armazena um índice de página de destino que você precisa renumerar no momento em que o documento cresce.
A ação Hide e a pegadinha do array
A ação Hide, ISO 32000-1 §12.6.4.10, Tabela 196, alterna a visibilidade de um ou mais campos. É a maneira mais limpa de criar comportamento de exibir e ocultar sem scripts, sendo perfeita para um link do tipo "Mostrar detalhes" ou para dois painéis mutuamente exclusivos onde exibir um oculta o outro. A ação carrega um destino em sua entrada /T e um booleano /H que decide a direção: ocultar quando verdadeiro (true), exibir quando falso (false).
A sutileza está inteiramente em 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 é gravado como uma única string de texto. Quando nomeia vários, /T é gravado como um array de strings de texto. Visualizadores mais antigos não tratam um array de um elemento da mesma forma que tratam uma string simples, portanto a codificação precisa se ramificar com base na contagem: um único nome deve ser emitido como uma string, não como um array de tamanho um, para que a maior variedade de leitores o respeite. O PDFlibPas faz essa escolha para você. Você passa os nomes dos campos separados por vírgulas, pontos e vírgulas ou quebras de linha, e o gravador emite uma única string para um nome e um array 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, ela permanece compatível com PDF/A. Os nomes que você passa são títulos de campos totalmente qualificados, e é por isso que um campo filho dentro de um grupo precisa ser acessado por meio de seu caminho completo separado por pontos, e não apenas por seu nome final.
ImportData: preenchimento prévio a partir de FDF
Enquanto a ação Hide reorganiza o que já está na página, a ação de importação de dados traz valores de fora dela. A ISO 32000-1 §12.6.4.8, Tabela 198, a define como uma ação que preenche o AcroForm a partir de um arquivo FDF (Forms Data Format) no disco. Esta é a ação por trás de um controle "Recarregar dados de exemplo" ou "Redefinir para os padrões", onde um arquivo FDF acompanha o PDF e contém os valores canônicos dos campos. A chamada espelha 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 arquivo não precisa existir quando o PDF é gerado, mas deve estar presente quando o usuário clica, e quaisquer barras invertidas no caminho são reescritas para a barra convencional do PDF automaticamente.
Uma restrição deve ser dita claramente porque costuma causar surpresa. Uma ação de importação de dados aponta para um arquivo externo, portanto não é permitida no formato PDF/A. Quando o documento está no modo PDF/A, a chamada retorna zero e não adiciona nada, em vez de produzir um arquivo que falhe na validação. Se o seu fluxo de trabalho visa uma saída de arquivamento, o preenchimento prévio deve ocorrer no momento da geração, gravando os valores dos campos diretamente, e não adiando essa operação para um clique.
JavaScript: pacotes globais e scripts por ação
Para lógicas que vão além de exibir, ocultar e importar, a família de ações recorre ao JavaScript no nível do documento. Existem dois locais distintos onde um script pode residir, e a diferença é importante. Um pacote JavaScript no nível do documento é armazenado uma vez para todo o arquivo e é executado quando o documento é aberto, sendo o local ideal para definições de funções e estado compartilhado. Um script por ação é anexado a um link ou campo específico e é executado apenas quando esse objeto é ativado, sendo ideal para a linha única que chama uma função que o pacote já definiu.
O PDFlibPas expõe ambos. AddGlobalJavaScript armazena um pacote nomeado no nível do documento; reutilizar um nome substitui o que estava armazenado nele. AddLinkToJavaScript anexa 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 no link não é uma preferência de estilo. Isso evita a duplicação do mesmo corpo em cada controle que precisa dele e significa que um visualizador com scripts desabilitados simplesmente não fará nada ao clicar, em vez de falhar devido a um bloco embutido malformado. Também mantém as entradas por ação pequenas, o que preserva a legibilidade do arquivo quando você o inspecionar mais tarde.
Campos, campos filhos e congelamento do resultado
As ações precisam de campos sobre os quais atuar, por isso ajuda entender como um campo é criado. NewFormField cria um campo na página atual e retorna seu índice; o tipo inteiro seleciona o tipo, onde 1 é Texto, 2 é Botão de pressão, 3 é Caixa de seleção, 4 é Botão de rádio, 5 é Seleção, 6 é Assinatura e 7 é um Pai que possui filhos, mas não desenha nada em si. O título passado não pode conter um ponto, pois o ponto é o separador nos nomes totalmente qualificados que as ações usam para endereçar os filhos.
Grupos de rádio e formulários hierárquicos são criados atribuindo filhos a um campo pai. NewChildFormField adiciona um filho sob um pai nomeado e, para os casos de rádio e seleção, AddFormFieldSub adiciona as opções individuais e retorna um índice temporário que você usa para posicionar cada uma delas. Quando a fase interativa termina e você deseja congelar um campo para que sua aparência atual se torne conteúdo permanente da página, FlattenFormField desenha o campo na página e o remove do formulário. Após um achatamento (flatten), os índices dos campos subsequentes são deslocados para baixo em um, o que é o principal detalhe a ser lembrado se você achatar vários campos em um 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 achatamento está comentada de propósito. Deixe-a desativada e o documento será enviado como um formulário interativo cujas ações são disparadas no leitor. Ative-a e o campo será renderizado como elementos estáticos, o que é ideal quando o formulário foi concluído e o resultado deve ser transmitido como um registro fixo. O mesmo campo, o mesmo código, dois documentos muito diferentes dependendo de você congelá-lo ou não.
Escolhendo o verbo correto
As quatro ações se dividem claramente pelo que alteram. Uma ação nomeada move a área de visualização e não precisa de campo. Uma ação Hide altera a visibilidade e precisa de títulos de campos, com a codificação de string contra array resolvida de forma automática. Uma ação de importação de dados acessa um arquivo em disco e, portanto, é proibida em PDF/A. Uma ação JavaScript executa uma 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 realizar o trabalho: uma ação Hide é mais portátil do que um script que define um sinalizador oculto, e uma ação nomeada é mais durável do que um destino de página armazenado porque não há número a ser mantido.
A partir daqui, dois tópicos relacionados completam o cenário. Se o formulário faz parte de um documento acessível, a árvore de estrutura que os leitores de tela percorrem é explicada em nosso artigo sobre PDF etiquetado e estrutura de acessibilidade. Quando o formulário preenchido precisa ser bloqueado e assinado, o fluxo de trabalho é descrito no passo a passo sobre o painel de conformidade e assinatura. Todos os três baseiam-se no mesmo mecanismo, que é fornecido como a biblioteca PDF para Delphi juntamente com as APIs de criação, formulário e assinatura abordadas em outras partes deste blog.