Artigo Técnico

Formulários PDF de cálculo automático no Delphi com o HotPDF

Um PDF de fatura ou despesa onde os itens de linha se somam sozinhos e a data se formata automaticamente precisa de três coisas anexadas ao seu AcroForm, e o HotPDF adiciona todas as três a um arquivo carregado no Delphi: ApplyLoadedFieldCalculation para totais AFSimple_Calculate, ApplyLoadedFieldValidation para os assistentes de validação e formato AF do Acrobat, e EnsureLoadedFieldAppearanceStream para gerar o valor que um visualizador realmente desenha

O HotPDF é um componente PDF nativo de VCL para Delphi e C++Builder, e esses três métodos operam em um documento que você abriu com o LoadFromFile em vez de um que você esteja construindo do zero. Cada um recebe um índice de campo baseado em zero na matriz AcroForm /Fields — a mesma ordem que o GetLoadedFormFieldNames retorna — e grava diretamente no dicionário do campo, de modo que todo o trabalho consiste em carregar, indexar, anexar e salvar com SaveLoadedDocument. A construção do próprio conjunto de campos é uma tarefa separada, abordada em adicionando campos AcroForm a um PDF carregado no Delphi, enquanto esta página trata de fornecer aritmética e formatação a um conjunto de campos existente

Por que um formulário PDF precisa de scripts para somar seus próprios campos?

Porque o PDF não possui uma camada de planilha. Um campo de texto armazena uma string, não uma fórmula, e nada no arquivo recalcula um total quando um item de linha é alterado, a menos que um script esteja anexado para fazer isso. O mecanismo é o dicionário de ações adicionais do campo, /AA (ISO 32000-1 §12.7.5), que mapeia eventos de disparo para ações: um gatilho de pressionamento de tecla (K) conforme os caracteres chegam, um gatilho de formatação para exibição, um gatilho de validação (V) na confirmação e um gatilho de cálculo (C) que dispara sempre que qualquer campo no formulário é alterado. Cada ação é uma ação JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), e the script que ela executa é o que realmente realiza a soma ou a reformatação. O Acrobat fornece uma biblioteca de auxiliares prontos — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx e os demais —, de modo que o script anexado geralmente é uma chamada de linha única para essa biblioteca em vez de um código sob medida

Um detalhe decide se um formulário de várias etapas fornece a resposta correta: a ordem de cálculo. O Acrobat executa cada campo que carrega uma ação de cálculo e os executa na sequência que a matriz AcroForm /CO lista, não na ordem das páginas. O ApplyLoadedFieldCalculation anexa a ação de cálculo ao campo que você nomeia; quando um campo calculado alimenta outro — um subtotal que uma linha de imposto lê, e depois um total geral que lê ambos —, você deve confirmar que a ordem de /CO lista cada entrada antes do campo que a consome, caso contrário o total geral lerá um subtotal desatualizado e ficará uma edição atrasado

Como fazer um formulário PDF calcular totais automaticamente?

O ApplyLoadedFieldCalculation recebe o índice do campo de destino, uma operação e os nomes dos campos de origem que ele deve agregar. O HotPDF grava AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) na ação de cálculo do campo de destino, e a enumeração THPDFFieldCalcOp escolhe a função: fccSum, fccAverage, fccProduct, fccMinimum e fccMaximum mapeiam para Acrobat SUM, AVG, PRD, MIN e MAX. Os nomes de origem são nomes de campos AcroForm, as mesmas strings que o GetLoadedFormFieldNames retorna, de modo que um total sobre três itens de linha e uma taxa de envio é uma única chamada

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

A chamada EnsureLoadedFieldAppearanceStream no total não é cosmética. O cálculo é executado dentro do visualizador, por isso, até que um leitor compatível com scripts abra o arquivo, o campo de total não possui conteúdo renderizado próprio. Gerar um fluxo de aparência fornece a ele um valor para mostrar nesse meio tempo, que é o que um trabalho de impressão ou um gerador de miniaturas usará quando não executar o script

Formatando e validando campos com a suíte de auxiliares AF

O ApplyLoadedFieldValidation anexa os scripts de formato, pressionamento de tecla e validação em uma única chamada, e o tipo de THPDFFieldValidationKind que você passa decide qual auxiliar do Acrobat ele conecta. Passe fvkCurrency e o HotPDF grava AFNumber_Format(2,0,0,0,"$ ",true) como o script de formato com o correspondente AFNumber_Keystroke; fvkNumber usa "." para o argumento separador, fvkPercentage emite AFPercent_Format e fvkDate emite AFDate_FormatEx("dd/mm/yyyy") a menos que você passe sua própria máscara no argumento DateFormat. Os formatos especiais — fvkZipCode, fvkSSN e fvkPhone — mapeiam para AFSpecial_Format com o código que o Acrobat reserva para cada um, e fvkArbitraryMask recebe uma string de máscara por meio do argumento Mask para qualquer coisa que essas predefinições não cubram. Para os tipos numéricos, o HotPDF também anexa um script AFRange_Validate para que entradas fora do intervalo sejam recusadas no momento da confirmação

// Formate a data da fatura como ISO, the valor como moeda e aplique uma máscara a um ID fiscal
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Os auxiliares AF cobrem as formas comuns para que você nunca precise escrever JavaScript manualmente para eles. Quando um campo precisar de uma lógica que nenhuma predefinição expresse — um checksum, uma regra entre campos, uma regex personalizada —, anexe o script bruto você mesmo com os métodos de ação em nível de campo descritos em construindo campos e ações AcroForm com o HotPDF, que recebem uma string JavaScript diretamente. Ambos os caminhos gravam no mesmo dicionário /AA, de modo que um formato de moeda predefinido em um campo e uma validação personalizada em outro coexistem no mesmo documento

Por que você deve regenerar o fluxo de aparência?

Porque a string de valor e os pixels que um visualizador desenha são duas coisas diferentes no PDF, e alterar um não altera o outro. Um campo armazena seu valor em /V, mas o que um leitor renderiza vem do fluxo de aparência do campo, /AP /N. Edite /V através do grafo de objetos e um visualizador estrito ainda mostrará a aparência antiga até que algo a regenere. O dicionário de formulário carrega uma flag /NeedAppearances que solicita ao visualizador que reconstrua todas as aparências ao abrir, e o Acrobat a respeita — mas renderizadores incorporados em navegadores, leitores móveis, servidores de impressão e geradores de miniaturas frequentemente a ignoram e desenham qualquer fluxo que já esteja presente, ou nada. O EnsureLoadedFieldAppearanceStream fecha essa lacuna construindo o próprio Form XObject /AP /N, a partir de /DA, /V e /Rect do campo, para que o valor renderizado viaje dentro do arquivo em vez de depender do visualizador para redesenhá-lo

// Gere uma aparência para cada campo antes de salvar, para que os valores sejam renderizados em todos os lugares
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Onde os scripts AF param de funcionar

Cada script AF* depende do visualizador ter um motor JavaScript, e a maioria dos visualizadores não tem. O Acrobat e alguns produtos desktop executam esses auxiliares; os visualizadores de PDF integrados no Chrome, Edge, Firefox e nas principais plataformas móveis, junto com rasterizadores e fluxos de impressão no servidor, não executam nenhum JavaScript de formulário. Nesses leitores, o cálculo nunca dispara e o formato nunca é aplicado — o campo simplesmente exibe o fluxo de aparência com o qual foi salvo pela última vez, que é exatamente o motivo pelo qual a geração do /AP /N é importante. Trate a camada AF* como uma conveniência para o subconjunto de usuários em um leitor compatível com scripts, nunca como uma garantia. Qualquer total que precise estar correto no servidor — um valor de fatura contra o qual você fará a cobrança, um valor de imposto a ser arquivado — deve ser recalculado do seu lado após o retorno do formulário, porque você não pode presumir que o leitor que o preencheu executou uma única linha de seu script

Mais um caso merece ser mencionado: formulários que chegaram como XFA. Um formulário XFA dinâmico carrega seu próprio motor de cálculo em uma carga útil XML sobre a qual os auxiliares AF* não sabem nada, portanto, anexar AFSimple_Calculate a um campo XFA não realiza nada. Achate o formulário XFA para um AcroForm simples primeiro, conforme abordado em achatamento de XFA para AcroForm no Delphi, e os três métodos aqui passarão a se aplicar aos campos AcroForm resultantes exatamente como fazem para qualquer outro formulário carregado

AcroForms de cálculo automático e autoformatação se resumem a três chamadas em um documento carregado — anexar o cálculo, anexar o formato e validação, gerar a aparência — seguidas por SaveLoadedDocument. A API de formulário carregado mostrada aqui faz parte do Componente HotPDF para Delphi e C++Builder, que documenta as enumerações completas de THPDFFieldValidationKind e THPDFFieldCalcOp ao lado do restante da superfície do AcroForm