Artículo técnico

Añadir campos AcroForm a un PDF cargado en Delphi

Tienes una plantilla de factura de un proveedor externo, o un contrato archivado que alguien generó hace años en un programa que ya nadie consigue localizar, y la exigencia es volverlo interactivo: colocar un cuadro de firma en una esquina, añadir un par de campos de texto, convertir una lista de verificación plana en casillas de verificación reales. El problema 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. Eso es un problema distinto de crear un formulario en un documento nuevo, y la parte que suele despistar queda oculta hasta que abres el resultado en un visor y los campos que acabas de escribir no aparecen en ningún sitio de la página

HotPDF es un componente PDF VCL nativo para Delphi y C++Builder, y desde la v2.247.0 expone una familia dedicada de métodos para justo esto: construir los seis tipos estándar de campo 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 marca sin la cual todo el ejercicio acaba, en silencio, generando un archivo aparentemente vacío

Por qué crear campos en un documento cargado tiene su propia ruta de código

Cuando construyes un PDF desde cero, HotPDF posee todo el modelo de objetos. Cada página es un contenedor THPDFPage editable, y añadir un campo de texto mediante AddTextField conecta el nuevo widget al objeto de anotación de la página, al objeto de página y a la colección de campos del formulario, y luego genera un flujo de apariencia a partir de los recursos de fuente 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 al pie de la letra

Un documento cargado no te da nada de ese andamiaje. Las páginas entran como diccionarios en bruto, no existe un contenedor THPDFPage editable sobre el que colgar un widget, y, más importante todavía, no hay una canalización de recursos de fuente lista para pintar flujos de apariencia. La ruta de documento cargado, por tanto, toma otro camino. Escribe los diccionarios de campo directamente sobre el grafo de objetos ya analizado y direcciona las páginas por índice base cero en lugar de por objeto de página. Los tipos de campo y los bits de marca coinciden exactamente con la ruta de creación desde cero, así que un campo Text es un campo Text en cualquier caso; lo que cambia es la tubería que hay debajo y, sobre todo, cómo se dibuja la superficie del widget

La marca /NeedAppearances no es opcional aquí

Este es el único dato que decide si tu trabajo se ve. Como la ruta de documento cargado no genera flujos de apariencia, un widget recién añadido llega al visor sin una entrada /AP: un campo sin superficie descrita. Muchos visores, al pedirles que rendericen un widget sin apariencia y sin instrucciones para construir una, no dibujan nada. El campo está en el archivo, es estructuralmente válido, se puede direccionar con una herramienta de cumplimentación de formularios y resulta completamente invisible para una persona

La vía de escape la define ISO 32000-1 §12.7.3: el diccionario AcroForm lleva un booleano /NeedAppearances, y cuando vale true un lector conforme debe construir por sí mismo los flujos de apariencia que faltan a partir de la cadena /DA de apariencia predeterminada de cada campo y de su valor. HotPDF lo establece por ti. La primera vez que añades cualquier campo a un documento cargado, se ejecuta EnsureLoadedAcroForm: si el catálogo no tiene /AcroForm, lo crea; si no existe el array /Fields, lo crea también, y fuerza /NeedAppearances true. No lo llamas directamente, pero saber que existe explica el comportamiento. También explica una advertencia de despliegue que merece decirse con claridad: unos pocos visores mínimos o no conformes ignoran /NeedAppearances y aun así no renderizan nada. Para los lectores habituales la marca cumple su función, pero si tu público usa un renderizador incrustado poco común, pruébalo allí antes de prometer nada

Añadir los seis tipos de campo

Cada método sigue la misma forma. Pasas el índice base cero de la página, las cuatro esquinas del rectángulo del widget en coordenadas de espacio de usuario PDF, el nombre del campo y los argumentos 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 equivocarse aquí es el segundo error más habitual después de olvidar la marca. Cada llamada devuelve el índice base 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 argumentos de cadena del campo de texto son el nombre del campo y su valor inicial /V; el entero es /MaxLen, que solo se escribe cuando es mayor que cero. HotPDF da a cada campo editable una cadena de apariencia predeterminada de /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 toma un valor de exportación, la cadena que el formulario envía cuando la casilla está marcada, más un booleano para el estado inicial; internamente escribe las entradas de nombre /V, /AS y /DV correspondientes para que el estado on/off sea coherente en cuanto se abre el archivo. Un valor de exportación vacío usa por defecto Yes, el nombre convencional del estado activado de una casilla

Campos de elección y los bits de /Ff

ComboBox y ListBox son ambos campos de elección, tipo de campo /Ch en ISO 32000-1 §12.7.4. La diferencia entre una lista desplegable y una lista con desplazamiento es un bit en el entero de marcas de campo /Ff: el bit 18, la marca Combo, valor $40000. HotPDF establece ese bit para AddLoadedComboBox y lo deja despejado para AddLoadedListBox; por lo demás, ambos son idénticos, y los dos toman sus opciones como un array 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 difiera de lo que lee el usuario; los métodos de creación cargada usan la forma más simple de una sola cadena, así que, si necesitas valores de exportación y presentación distintos, tendrías que establecerlos 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 con la lista

El botón pulsable es el otro caso guiado por marcas: tipo de campo /Btn con el bit 17, la marca PushButton, valor $10000. Ese bit es lo que separa un botón pulsable de una casilla de verificación, que también es un campo /Btn pero sin él. El título que pasas se escribe en el diccionario de características de apariencia /MK como el título normal /CA. Conviene ser claro 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 enviar, restablecer o JavaScript es una cuestión aparte; para la parte de autoría desde cero, el flujo de campo más acción se trata en crear campos y acciones AcroForm en Delphi, que es el punto de comparación adecuado para lo que la ruta cargada deja fuera a propósito

El diccionario que comparten todos los campos

Detrás de los seis métodos hay un único generador compartido que construye la anotación widget y la registra en dos sitios. Escribe /Type /Annot y /Subtype /Widget, el array /Rect a partir de tus cuatro coordenadas, la marca de anotación /F 4, que activa el bit Print para que el campo aparezca tanto en papel como en pantalla, el nombre del campo /T, el tipo de campo /FT, las marcas /Ff y una referencia inversa /P al objeto de página. Después añade el nuevo campo al array /Fields del AcroForm y al array /Annots de esa página, resolviendo las referencias indirectas por el camino para ampliar los arrays reales en lugar de dejar el widget huérfano

Ese registro dual importa porque un widget que vive solo en una de las dos listas queda roto de 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 el sistema de formularios no lo conoce. HotPDF mantiene ambas listas sincronizadas en cada alta, que es el tipo de contabilidad que de otro modo tendrías que hacer a mano con precisión absoluta contra la especificación

Algunas limitaciones reales

Ajusta las expectativas antes de montar un flujo de trabajo sobre esto. El comportamiento de aplanar y regenerar depende de que el visor respete /NeedAppearances, algo que cubre Acrobat, los motores PDF de los navegadores modernos y los lectores de escritorio habituales, pero no es una garantía absoluta en todos los renderizadores que existen. Si necesitas producir un archivo cuyos campos se rendericen de forma idéntica en todas partes, incluidos los visores que ignoran la marca, 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 opción más adecuada. El campo de firma, del mismo modo, se crea como un widget de firma vacío listo para firmarse; colocar el campo no es lo mismo que aplicar una firma criptográfica

Para cambiar lo que ya existe en lugar de añadirlo, la operación relacionada es el aplanado de formularios, donde incrustas los campos interactivos de nuevo en el contenido estático de la página para que los valores se vuelvan permanentes e ineditables; ese recorrido de ida y vuelta, incluida la forma en que se tratan los formularios con XFA, se comenta en aplanar campos XFA y AcroForm en Delphi. Añadir campos y aplanar campos son los dos extremos del mismo ciclo de vida: este artículo muestra cómo llevar la interactividad a un documento que no la tenía, y el aplanado es cómo se la quitas una vez que el formulario ya ha cumplido su propósito

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