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 se formatea automáticamente necesita tres cosas incorporadas en su AcroForm, and 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 el visor realmente dibuja

HotPDF es un componente PDF VCL nativo para Delphi y C++Builder, y estos tres métodos operan en un documento que 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 del campo, por lo que todo el trabajo consiste en cargar, indexar, adjuntar y llamar a SaveLoadedDocument. Construir el conjunto de campos en sí es una tarea separada, cubierta en añadir campos AcroForm a un PDF cargado en Delphi, mientras que esta página trata sobre cómo dar aritmética y formato a un conjunto de campos existente

¿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, 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 adjunte un script para hacerlo. El mecanismo es el diccionario de acciones adicionales del campo, /AA (ISO 32000-1 §12.7.5), que asocia eventos desencadenantes con acciones: un desencadenador de pulsación de tecla (K) a medida que llegan los caracteres, un desencadenador de formato para la visualización, un desencadenador de validación (V) al confirmar y un desencadenador 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 the script que ejecuta es lo que realmente realiza la suma o el reformateo. Acrobat incluye una biblioteca de asistentes listos para usar —AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx y el resto— por lo que el script adjunto suele ser una llamada de una sola línea a esa biblioteca en lugar de código personalizado

Un detalle decide si un formulario de varios pasos da la respuesta correcta: el orden de cálculo. Acrobat ejecuta cada campo que lleva una acción de cálculo y los ejecuta en la secuencia que enumera el arreglo /CO de AcroForm, no en el orden de las páginas. ApplyLoadedFieldCalculation adjunta la acción de cálculo al campo que especifique; cuando un campo calculado alimenta a otro —un subtotal que lee una línea de impuestos, luego un gran total que lee ambos— debe confirmar que el orden de /CO enumere cada entrada antes del campo que la consume, o el gran total leerá un subtotal obsoleto y se retrasará una edición por detrás

¿Cómo hacer 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 elige la función: fccSum, fccAverage, fccProduct, fccMinimum y fccMaximum se asignan a SUM, AVG, PRD, MIN y MAX de Acrobat. Los nombres de origen son nombres de campos de AcroForm, las mismas cadenas 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;

La llamada a EnsureLoadedFieldAppearanceStream en el total no es cosmética. El cálculo se ejecuta dentro del visor, por lo que hasta que un lector con capacidad para scripts abra el archivo, el campo de total no tiene contenido renderizado propio. Generar un flujo de apariencia le da una cifra para mostrar mientras tanto, que es lo que recogerá un trabajo de impresión o un generador de miniaturas cuando nunca ejecute el script

Formatear y validar campos con el conjunto de asistentes AF

ApplyLoadedFieldValidation adjunta los scripts de formato, pulsación de tecla y validación en una sola llamada, y el THPDFFieldValidationKind que pase decide qué asistente de Acrobat se conecta. Pase fvkCurrency y HotPDF escribirá AFNumber_Format(2,0,0,0,"$ ",true) como script de formato con su correspondiente AFNumber_Keystroke; fvkNumber usa "." para el argumento del separador, fvkPercentage emite AFPercent_Format y fvkDate emite AFDate_FormatEx("dd/mm/yyyy") a menos que pase su propia máscara en el argumento DateFormat. Los formatos especiales —fvkZipCode, fvkSSN y fvkPhone— se asignan a AFSpecial_Format con el código que Acrobat reserva para cada uno, y fvkArbitraryMask toma una cadena de máscara a través del argumento Mask para cualquier cosa que esas plantillas predefinidas no cubran. Para los tipos numéricos, HotPDF también adjunta un script AFRange_Validate para que se rechace la entrada fuera de rango al confirmar

// 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 AF cubren las formas comunes para que nunca tenga que escribir JavaScript a mano para ellos. Cuando un campo necesita una lógica que ninguna plantilla predefinida expresa —una suma de comprobación, una regla entre campos, una expresión regular personalizada—, adjunte el script original usted mismo con los métodos de acción a nivel de campo descritos en construcción de campos y acciones AcroForm con HotPDF, que toman una cadena 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

¿Por qué debe regenerar el flujo de apariencia?

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

// 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 de 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 activa 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 la razón por la que importa generar /AP /N. Trate la capa AF* como una conveniencia para el subconjunto de usuarios en un lector con capacidad para 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á— debe recalcularse de su lado después de recibir el formulario, porque no puede asumir que el lector que lo rellenó ejecutó una sola línea de su script

Vale la pena mencionar un caso más: los formularios que llegaron como XFA. Un formulario XFA dinámico contiene su propio motor de cálculo en una carga útil XML que los asistentes AF* no conocen en absoluto, por lo que adjuntar AFSimple_Calculate a un campo XFA no sirve de nada. Primero aplane el formulario XFA a un AcroForm simple, como se describe 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 con cualquier otro formulario cargado

Los AcroForms con autocalculo y autoformato se reducen a tres llamadas en un documento cargado —adjuntar el cálculo, adjuntar el formato y la validación, generar la apariencia— seguidas de SaveLoadedDocument. El loaded-form API que se muestra aquí forma parte de HotPDF Component para Delphi y C++Builder, que documenta las enumeraciones completas THPDFFieldValidationKind y THPDFFieldCalcOp junto con el resto de la superficie de AcroForm