Artículo técnico

Formularios PDF autocalculables en Delphi con HotPDF

Una factura o un PDF de gastos donde los elementos de línea se suman a sí mismos y la fecha tiene su propio formato requiere tres cosas integradas en su AcroForm, y HotPDF añade las tres a un archivo cargado en Delphi: ApplyLoadedFieldCalculation para totales de AFSimple_Calculate, ApplyLoadedFieldValidation para los asistentes de validación y formato AF de Acrobat, y EnsureLoadedFieldAppearanceStream para generar el valor que un visor realmente dibuja

HotPDF es un componente PDF nativo de VCL para Delphi y C++Builder, y estos tres métodos operan en un documento que usted abrió con LoadFromFile en lugar de uno que esté construyendo desde cero. Cada uno toma un índice de campo basado en cero en el arreglo /Fields de AcroForm (el mismo orden que devuelve GetLoadedFormFieldNames) y escribe directamente en el diccionario de campos, por lo que todo el trabajo consiste en cargar, indexar, asociar y llamar a SaveLoadedDocument. Construir el conjunto de campos en sí es una tarea separada, cubierta en agregar campos de AcroForm a un PDF cargado en Delphi, mientras que esta página trata sobre cómo dotar a un conjunto de campos existente de aritmética y formato

¿Por qué un formulario PDF necesita scripts para sumar sus propios campos?

Porque PDF no tiene una capa de hoja de cálculo. Un campo de texto contiene una cadena de caracteres, no una fórmula, y nada en el archivo vuelve a calcular un total cuando un elemento de línea cambia a menos que se asocie un script para hacerlo. El mecanismo es el diccionario de acciones adicionales de campo, /AA (ISO 32000-1 §12.7.5), que mapea eventos desencadenantes con acciones: un disparador de pulsación de tecla (K) a medida que llegan los caracteres, un disparador de formato para la visualización, un disparador de validación (V) al confirmar, y un disparador de cálculo (C) que se activa cada vez que cambia cualquier campo del formulario. Cada acción es una acción de JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), y el script que ejecuta es lo que realmente realiza la suma o el cambio de formato. Acrobat incluye una biblioteca de asistentes predefinidos (AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx y los demás), por lo que el script asociado suele ser una llamada de una sola línea a esa biblioteca en lugar de código a medida

Un detalle determina si un formulario de varios pasos ofrece la respuesta correcta: el orden de cálculo. Acrobat ejecuta cada campo que contenga una acción de cálculo, y los ejecuta en la secuencia que lista el arreglo /CO de AcroForm, no en el orden de las páginas. ApplyLoadedFieldCalculation asocia la acción de cálculo al campo que usted especifique; cuando un campo calculado alimenta a otro (un subtotal que lee una línea de impuestos y luego un gran total que lee ambos), usted debe confirmar que el orden /CO liste cada entrada antes del campo que la consume, o de lo contrario el gran total leerá un subtotal desactualizado y se retrasará una edición

¿Cómo se logra que un formulario PDF calcule totales automáticamente?

ApplyLoadedFieldCalculation toma el índice del campo de destino, una operación y los nombres de los campos de origen que debe agregar. HotPDF escribe AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) en la acción de cálculo del campo de destino, y la enumeración THPDFFieldCalcOp selecciona la función: fccSum, fccAverage, fccProduct, fccMinimum y fccMaximum se mapean a SUM, AVG, PRD, MIN y MAX de Acrobat. Los nombres de origen son nombres de campos de AcroForm, las mismas cadenas de caracteres que devuelve GetLoadedFormFieldNames, por lo que un total sobre tres elementos de línea y un cargo de envío es una sola llamada

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;

Formatear y validar campos con el conjunto de asistentes AF

¿Por qué se debe regenerar el flujo de apariencia?

Porque la cadena de caracteres del valor y los píxeles que dibuja un visor son dos cosas diferentes en PDF, y cambiar una no modifica la otra. Un campo almacena su valor en /V, pero lo que un lector renderiza proviene del flujo de apariencia del campo, /AP /N. Si edita /V a través del grafo de objetos, un visor estricto seguirá mostrando la apariencia antigua hasta que algo la regenere. El diccionario del formulario contiene una bandera /NeedAppearances que solicita al visor reconstruir cada apariencia al abrir el documento, y Acrobat la respeta; sin embargo, los renderizadores incrustados en navegadores, lectores móviles, servidores de impresión y generadores de miniaturas con frecuencia la ignoran y dibujan cualquier flujo que ya esté presente, o nada en absoluto. EnsureLoadedFieldAppearanceStream cierra esa brecha construyendo el Form XObject /AP /N por sí mismo, a partir de los valores de /DA, /V y /Rect del campo, de modo que el valor renderizado viaje dentro del archivo en lugar de depender de que el visor lo vuelva a dibujar

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Los asistentes de validación AF cubren las formas comunes para que usted nunca tenga que escribir JavaScript para ellos de forma manual. Cuando un campo necesita una lógica que ningún ajuste predefinido exprese (una suma de verificación, una regla entre campos, una expresión regular personalizada), asocie el script sin procesar usted mismo con los métodos de acción a nivel de campo descritos en construcción de campos y acciones de AcroForm con HotPDF, que toman una cadena de caracteres de JavaScript directamente. Ambas rutas escriben en el mismo diccionario /AA, por lo que un formato de moneda preestablecido en un campo y una validación personalizada en otro coexisten en el mismo documento

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Dónde dejan de funcionar los scripts AF

Cada script AF* depende de que el visor tenga un motor JavaScript, y la mayoría de los visores no lo tienen. Acrobat y un puñado de productos de escritorio ejecutan estos asistentes; los visores de PDF integrados en Chrome, Edge, Firefox y las principales plataformas móviles, junto con los rasterizadores del lado del servidor y los flujos de impresión, no ejecutan ningún JavaScript de formulario en absoluto. En esos lectores, el cálculo nunca se inicia y el formato nunca se aplica: el campo simplemente muestra el flujo de apariencia con el que se guardó por última vez, que es exactamente por lo que generar /AP /N es importante. Trate la capa AF* como una conveniencia para el subconjunto de usuarios con un visor compatible con scripts, nunca como una garantía. Cualquier total que deba ser correcto en el servidor (un monto de factura contra el cual facturará, una cifra de impuestos que presentará) tiene que volver a calcularse de su lado después de que el formulario regrese, porque no puede asumir que el lector que lo llenó ejecutó una sola línea de su script

Vale la pena mencionar un caso más: los formularios que llegan como XFA. Un formulario XFA dinámico contiene su propio motor de cálculo en una carga XML de la cual los asistentes AF* no saben nada, por lo que asociar AFSimple_Calculate a un campo XFA no logra nada. Primero aplane el formulario XFA a un AcroForm simple, como se cubre en aplanado de XFA a AcroForm en Delphi, y los tres métodos aquí se aplicarán a los campos de AcroForm resultantes exactamente como lo hacen en cualquier otro formulario cargado

Los AcroForms autocalculables y autoformateables se reducen a tres llamadas en un documento cargado (asociar el cálculo, asociar el formato y validación, generar la apariencia) seguidas de SaveLoadedDocument. La API de formulario cargado que se muestra aquí es parte de HotPDF Component para Delphi and C++Builder, el cual documenta las enumeraciones completas THPDFFieldValidationKind y THPDFFieldCalcOp junto con el resto del entorno de AcroForm