Artículo técnico

Agregar campos AcroForm a un PDF cargado en Delphi

Tal vez tengas una plantilla de factura de un tercero, o un contrato archivado que alguien generó hace años en un software que ya nadie puede encontrar, y la exigencia sea volverlo interactivo: colocar un cuadro de firma en la esquina, agregar un par de campos de texto, convertir una lista de verificación plana en casillas reales. El detalle es que no estás creando este PDF desde cero. Ya existe, ya tiene páginas, flujos de contenido y fuentes que no controlas, y necesitas injertar widgets AcroForm en ese grafo de objetos sin reconstruirlo. Ese es un problema distinto al de crear un formulario en un documento nuevo, y lo que suele confundir a la gente no se ve hasta abrir el resultado en un visor y notar que los campos que acabas de escribir no aparecen en la página

HotPDF es un componente PDF VCL nativo para Delphi y C++Builder y, desde la versión v2.247.0, expone una familia dedicada de métodos exactamente para esto: construir los seis tipos estándar de campos directamente sobre un documento cargado con LoadFromFile. Este artículo repasa qué hacen esos métodos, el diccionario ISO 32000-1 que construyen y la única bandera sin la cual todo el ejercicio termina en un archivo que parece vacío

Por qué la creación de campos en documentos cargados usa otra ruta

Cuando construyes un PDF desde cero, HotPDF controla todo el modelo de objetos. Cada página es un contenedor THPDFPage editable, y agregar un campo de texto mediante AddTextField conecta el nuevo widget con el objeto de anotación de la página, el objeto de página y la colección de campos del formulario, y luego genera un flujo de apariencia a partir de los recursos de fuentes del documento. El flujo de apariencia es la superficie visible del widget, el cuadro, el borde y cualquier texto predeterminado, pintados como operadores de dibujo PDF que el visor renderiza tal cual

Un documento cargado no te da nada de ese andamiaje. Las páginas llegan como diccionarios en bruto; no hay un contenedor THPDFPage editable donde colgar un widget, y más importante aún, no hay una canalización de recursos tipográficos lista para pintar flujos de apariencia. La ruta cargada toma por eso otro camino. Escribe los diccionarios de campo directamente sobre el grafo de objetos ya analizado y direcciona las páginas por índice basado en cero en lugar de hacerlo por objeto de página. Los tipos de campo y los bits de bandera coinciden exactamente con la ruta desde cero, así que un campo Text es un campo Text en ambos casos; lo que cambia es la infraestructura subyacente y, sobre todo, cómo se dibuja la superficie del widget

El indicador /NeedAppearances no es opcional aquí

Este es el dato único que decide si tu trabajo se ve. Como la ruta cargada no genera flujos de apariencia, un widget recién agregado llega al visor sin entrada /AP: un campo sin superficie descrita. Muchos visores, al intentar renderizar un widget que no tiene apariencia ni instrucciones para construir una, no dibujan nada. El campo está en el archivo, es estructuralmente válido, una herramienta de llenado de formularios puede encontrarlo, y para una persona es completamente invisible

La salida de emergencia está definida en ISO 32000-1 §12.7.3: el diccionario AcroForm lleva un booleano /NeedAppearances, y cuando es true un lector conforme debe construir por sí mismo los flujos de apariencia faltantes a partir de la cadena /DA (apariencia predeterminada) y del valor de cada campo. HotPDF lo establece por ti. La primera vez que agregas cualquier campo a un documento cargado, se ejecuta EnsureLoadedAcroForm: si el catálogo no tiene /AcroForm, crea uno; si no existe un arreglo /Fields, crea ese arreglo; y fuerza /NeedAppearances true. No lo llamas directamente, pero saber que existe explica el comportamiento. También aclara una advertencia de despliegue que conviene decir sin rodeos: unos cuantos visores mínimos o no conformes ignoran /NeedAppearances y aun así no muestran nada. Para los lectores principales la bandera cumple su función, pero si tu público usa un renderizador incrustado poco común, pruébalo allí antes de prometer algo

Agregar los seis tipos de campo

Todos los métodos siguen la misma estructura. Pasas el índice de página basado en cero, las cuatro esquinas del rectángulo del widget en coordenadas PDF de espacio de usuario, el nombre del campo y cualquier argumento extra que necesite el tipo. El rectángulo es X1, Y1, X2, Y2 con el origen PDF en la esquina inferior izquierda de la página, así que los valores Y más altos quedan más arriba; esta es la convención de coordenadas del formato de archivo, no la convención de pantalla con origen arriba a la izquierda, y confundirse con esto es el segundo error más común después de olvidar la bandera. Cada llamada devuelve el índice basado en cero del nuevo campo, o -1 si el índice de página estaba fuera de rango o no se pudo resolver el objeto de página

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Los tercer y cuarto argumento de cadena del campo de texto son el nombre del campo y su valor inicial /V; el entero es /MaxLen y solo se escribe cuando es mayor que cero. HotPDF da a cada campo editable una cadena de apariencia predeterminada /Helv 12 Tf 0 0 0 rg, que es lo que un visor que respeta /NeedAppearances lee para decidir la fuente y el color con que pinta el valor. La casilla de verificación recibe un valor de exportación, la cadena que el formulario envía cuando la casilla está marcada, además de un booleano para el estado inicial; internamente escribe las entradas de nombre /V, /AS y /DV correspondientes para que el estado activo o inactivo sea coherente en el momento en que se abre el archivo. Un valor de exportación vacío usa Yes por defecto, el nombre convencional de casilla activada

Campos de elección y las banderas de bit /Ff

ComboBox y ListBox son ambos campos de elección, con tipo de campo /Ch en ISO 32000-1 §12.7.4. La diferencia entre una lista desplegable y una lista con desplazamiento es un solo bit en el entero de banderas del campo /Ff: el bit 18, la bandera Combo, valor $40000. HotPDF establece ese bit para AddLoadedComboBox y lo deja en cero para AddLoadedListBox; por lo demás, ambos son idénticos y toman sus opciones como un arreglo abierto de cadenas escrito en la entrada /Opt

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Dos notas sobre la lista de opciones. HotPDF escribe cada entrada /Opt como una cadena simple, donde el valor de exportación y la etiqueta mostrada son el mismo texto. ISO 32000-1 §12.7.4.4 también permite la forma de dos elementos [export display] cuando necesitas que el valor enviado sea distinto de lo que lee el usuario; los métodos de creación cargada usan la forma simple de una sola cadena, así que si necesitas valores de exportación y visualización distintos tendrías que fijarlos tú mismo en el diccionario resultante. Y el valor que pases como selección actual del campo debería ser una de las opciones que suministraste, porque el visor lo compara contra la lista

El botón pulsador es el otro caso gobernado por una bandera: tipo de campo /Btn con el bit 17, la bandera PushButton, valor $10000. Ese bit es lo que separa un botón clicable de una casilla de verificación, que también es un campo /Btn pero sin ese bit. La leyenda que pasas se escribe en el diccionario de características de apariencia /MK como la leyenda normal /CA. Vale la pena ser honesto sobre el alcance aquí: el botón se crea con su etiqueta y su rectángulo, pero el método de creación cargada no adjunta una acción, así que por sí solo es un botón que se ve bien y no hace nada al hacer clic. Conectar acciones de envío, reinicio o JavaScript es otro tema; del lado de la autoría desde cero, el flujo de campo más acción se cubre en crear campos y acciones AcroForm en Delphi, que es la comparación adecuada para ver lo que la ruta cargada deja fuera a propósito

El diccionario que comparten todos los campos

Debajo de los seis métodos hay un único generador compartido que construye la anotación del widget y la registra en dos lugares. Escribe /Type /Annot y /Subtype /Widget, el arreglo /Rect a partir de tus cuatro coordenadas, la bandera de anotación /F 4 que activa el bit Print para que el campo aparezca en papel además de en pantalla, el nombre del campo /T, el tipo de campo /FT, las banderas /Ff y una referencia de vuelta /P al objeto de página. Después agrega el nuevo campo al arreglo /Fields del AcroForm y al arreglo /Annots de esa página, resolviendo referencias indirectas en el camino para extender los arreglos reales en lugar de dejar al widget huérfano

Ese doble registro importa porque un widget que vive solo en una de las dos listas queda roto de una forma sutil. Un campo presente en /Fields pero ausente en /Annots de la página es conocido por el formulario pero nunca se pinta; el caso inverso se pinta pero la lógica del formulario no lo conoce. HotPDF mantiene ambas cosas sincronizadas en cada alta, que es el tipo de contabilidad que de otro modo tendrías que hacer exactamente bien a mano contra la especificación

Unas cuantas limitaciones honestas

Fija expectativas antes de construir un flujo de trabajo sobre esto. El comportamiento de aplanar y regenerar depende de que el visor respete /NeedAppearances, lo que cubre Acrobat, los motores PDF modernos del navegador y los lectores de escritorio comunes, pero no garantiza nada en todos los renderizadores que existen. Si necesitas producir un archivo cuyos campos se vean idénticos en todas partes, incluso en visores que ignoran la bandera, ya entras en el terreno de los flujos de apariencia y la ruta de autoría desde cero que pinta /AP por ti es la mejor opción. El campo de firma, del mismo modo, se crea como un widget de firma vacío listo para ser firmado; colocar el campo no es lo mismo que aplicar una firma criptográfica

Para cambiar lo que ya existe en lugar de agregar a lo existente, la operación relacionada es el aplanado de formularios, donde integras los campos interactivos de nuevo en el contenido estático de la página para que los valores queden permanentes e ineditables; ese recorrido de ida y vuelta, incluida la forma en que se manejan los formularios con XFA, se analiza en aplanar campos XFA y AcroForm en Delphi. Agregar campos y aplanar campos son dos extremos del mismo ciclo de vida: este artículo explica cómo llevar la interactividad a un documento que no la tenía, y el aplanado es cómo quitarla una vez que el formulario cumplió su propósito

La API de formularios sobre documentos cargados que se muestra aquí forma parte del HotPDF Component estándar para Delphi y C++Builder, junto con la referencia completa de banderas de campo, manejo de apariencia y el resto del modelo AcroForm