Artículo técnico

Asignar valores a campos de formulario en un PDF en Delphi

HotPDF Delphi Component llena un campo AcroForm existente en un PDF cargado a través de THotPDF.SetFormFieldValue, direccionado por índice de campo base cero o por nombre de campo totalmente calificado. Escribir la nueva entrada /V es lo fácil; lo que hace confiable la llamada en formularios del mundo real es que el mismo método además mantiene consistentes tres estados que son invisibles hasta que salen mal: la identidad decodificada del campo, para que un nombre no ASCII se pueda encontrar, el estado de apariencia /AS en los widgets de checkbox y radio, y el array de índices de selección /I en los campos de elección. El stream de apariencia visible es un paso aparte y explícito, a través de EnsureLoadedFieldAppearanceStream

El escenario es el de todos los días: un cliente le manda su propio formulario, una declaración de impuestos, un reclamo de seguro, una orden de compra que alguien armó en Acrobat hace años, y su aplicación Delphi tiene que llenarlo desde una base de datos y devolver un archivo que abra bien en todos lados. Usted no tiene control sobre cómo se creó el formulario. Los nombres de campo pueden estar en UTF-16, los valores de exportación de un checkbox pueden ser 2 en lugar de Yes, y los combo box pueden usar pares de opciones [export display]. Cada uno de esos detalles tiene una regla en ISO 32000-1, y cada regla es algo que SetFormFieldValue ahora resuelve por usted. Este artículo trata de qué hace, por qué y dónde se detiene. Para el problema hermano de crear campos que todavía no existen, vea agregar campos AcroForm a un PDF cargado en Delphi

¿Por qué SetFormFieldValue no encuentra un campo con nombre no ASCII?

Antes de la v2.752.1 la respuesta era la codificación: el campo vivía en el archivo bajo un nombre hexadecimal UTF-16BE, y el caché de nombres guardaba la grafía hex en lugar del texto. ISO 32000-1 §12.7.3.1 define el nombre parcial de campo /T como un text string, y §7.9.2.2 dice que un text string puede ser UTF-16BE con un byte order mark FE FF adelante. Las herramientas de autor serializan esos nombres como strings hex según §7.3.4.3, así que un campo llamado Straße llega como <FEFF005300740072006100DF0065>. Dentro de HotPDF, THPDFStringObject.Value guarda el texto hexadecimal crudo siempre que IsHexadecimal esté puesto, que es exactamente lo que usted quiere para un ida y vuelta sin pérdida del diccionario original y exactamente lo que no quiere como clave de búsqueda. HPDFLoadedFormTextName separa las dos preocupaciones. Cuando se arma el caché de relaciones, cada valor /T pasa por ella: si el objeto string es hexadecimal, HPDFHexToBytes restaura la secuencia de bytes; si los bytes empiezan con FE FF y tienen longitud par, el payload se decodifica como UTF-16BE y se recodifica como UTF-8; el resultado se une después al nombre de su padre con un punto para formar el nombre totalmente calificado que describe §12.7.3.1, así que un hijo llamado City bajo un padre llamado Address queda registrado como Address.City. La clave del caché se normaliza a minúsculas, lo que hace que SetFormFieldValue('address.city', ...) también funcione; eso es una comodidad más allá del estándar, ya que la especificación trata los nombres como sensibles a mayúsculas. Lo importante es que solo cambia la clave del caché. El objeto /T del diccionario del campo conserva su codificación hexadecimal, así que guardar el documento no reescribe la identidad de un campo que usted solo llenó

Cómo resuelve HotPDF los nombres AcroForm no ASCII: HPDFHexToBytes restaura el payload UTF-16BE detrás de un string /T hexadecimal, el byte order mark FE FF se decodifica y se recodifica como UTF-8, y el nombre calificado se une al de su padre para que tanto Applicant.FullName como un campo llamado Straße caigan en el caché de búsqueda
Solo cambia la clave del caché: el diccionario del campo conserva su codificación hexadecimal, las búsquedas se normalizan a minúsculas como comodidad más allá del estándar, y guardar el documento nunca reescribe la identidad de un campo que usted solo llenó
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // Los nombres calificados se decodifican de strings /T UTF-16BE y
    // se unen con puntos, así que los nombres anidados y no ASCII resuelven
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // Los valores que no son Latin-1 viajan como hex UTF-16BE con prefijo FEFF
    // y se escriben como un string hexadecimal de PDF
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

¿Qué escribe realmente SetFormFieldValue?

Las dos sobrecargas corren los mismos cinco pasos: ubicar el diccionario del campo, escribir /V a través de HPDFSetDictFormValue, reconciliar los índices de selección de elección, marcar el diccionario como sucio, reconciliar los estados de apariencia de los botones y, por último, registrar el índice del campo con NoteLoadedFormFieldDirty. Ese último paso importa si el formulario lleva scripts de cálculo, porque el conjunto de sucios es lo que consume la sobrecarga sin parámetros de RecalculateLoadedFormFieldsIncremental para volver a correr solo los cálculos que lean de forma transitiva un campo modificado. HPDFSetDictFormValue en sí tiene cuidado con el tipo de objeto que reemplaza. Si el /V existente es un objeto name, que es lo que usan los campos de checkbox y radio para su valor de exportación, el valor nuevo se escribe como name, nunca como string, porque los nombres PDF son solo ASCII por construcción. Si no, escribe un objeto string e inspecciona el valor que usted pasó: un string que empieza con FEFF, tiene longitud par y consiste solo en dígitos hex se trata como la forma de cable UTF-16BE de §7.9.2.2 y se guarda con IsHexadecimal puesto, así que se serializa como <FEFF...> y no como un literal (FEFF...). Ese es el mecanismo con el que cuenta la línea del campo City de arriba; cualquier otro string se guarda como string literal con los bytes que usted dio, así que para texto latino común pase texto común

¿Por qué un checkbox conserva su tilde viejo después de cambiar el valor?

Porque en un campo de botón el valor por sí solo no decide qué se dibuja. ISO 32000-1 §12.7.4.2.3 especifica que un widget de checkbox lleva un estado de apariencia /AS que nombra cuál stream de /AP /N se muestra en ese momento, y los visores pintan desde /AS, no desde /V. Si usted cambia /V a Yes pero deja /AS en Off, el archivo es internamente contradictorio, y el flattening va a hornear sin problema la apariencia vieja sin marcar en la página mientras los datos del formulario dicen marcado. ReconcileLoadedButtonAppearanceStates existe para cerrar esa brecha: para un campo cuyo /FT es Btn, visita el diccionario del campo mismo y cada entrada de su array /Kids, lee el nombre del estado encendido de /AP /N y reescribe /AS con ese nombre cuando coincide con el valor del campo, o con Off cuando no

Por qué un checkbox de HotPDF conserva su tilde viejo cuando solo cambia /V: los visores pintan desde el estado de apariencia /AS hacia /AP /N, así que ReconcileLoadedButtonAppearanceStates visita el campo y cada hijo, lee el nombre del estado encendido como la primera clave distinta de Off, y reescribe /AS si hay coincidencia o lo pone en Off si no
Los grupos de radio comparan cada hijo contra el valor del padre que InheritedButtonValue recupera recorriendo la cadena /Parent, así que fijar el grupo en un valor de exportación enciende exactamente ese widget y apaga a todos sus hermanos

Dos detalles de formularios reales moldearon el fix de la v2.752.3. Primero, un diccionario de apariencia normal puede contener solo el estado encendido; §12.7.4.2.3 nombra la apariencia apagada Off, pero las herramientas de autor con frecuencia omiten su stream y dejan que el visor no dibuje nada. El código anterior se plantaba cuando el diccionario tenía menos de dos entradas, así que esos checkbox de un solo estado conservaban su tilde viejo en silencio. Ahora la verificación es simplemente que el diccionario no esté vacío, y el nombre del estado encendido se toma como la primera clave que no sea Off. Segundo, el nombre del estado encendido es el que haya elegido el autor. Los formularios reales usan 2, Yes, On o una palabra localizada, así que la comparación es contra la clave real, sin distinguir mayúsculas, nunca contra un Yes hardcodeado. Los radio buttons agregan una vuelta más, descrita en §12.7.4.2.4: la selección vive en /V sobre el campo padre, mientras que los hijos individuales son dueños de los widgets y normalmente no tienen /V propio. Por eso el helper anidado InheritedButtonValue sube por la cadena /Parent, hasta 64 niveles, hasta encontrar un valor no vacío, y cada hijo se compara contra el valor del grupo al que pertenece. Fijar el padre en el valor de exportación de un hijo enciende exactamente ese hijo y apaga a todos sus hermanos

// Checkbox: el valor de exportación debe coincidir con la clave del estado encendido en /AP /N
// (a menudo 'Yes', pero los formularios reales usan '2', 'On' o cualquier otra cosa)
Pdf.SetFormFieldValue('Consent', 'Yes');

// Grupo de radio: /V se escribe en el padre; cada widget hijo recibe
// /AS puesto en su propio nombre de exportación o en Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Limpiar un checkbox: cualquier valor que no matchee ningún estado encendido deja /AS en Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

Campos de elección: mantener /I en sintonía con /V

En un combo box o un list box, /V no es el único lugar donde se registra una selección. La Tabla 231 de §12.7.4.4 define /I como un array de índices base cero dentro de /Opt que identifica los ítems seleccionados, y un visor que encuentra /I apuntando a la opción 0 mientras /V nombra la opción 3 puede resaltar la fila equivocada. Desde la v2.754.1, HPDFReconcileChoiceSelection corre dentro de cada llamada a SetFormFieldValue y, cuando el /FT heredado es Ch, reconstruye /I a partir del valor nuevo. El orden de las operaciones es deliberado. La entrada /I local se borra primero, sin tocar su contenido: si el array viejo era un objeto indirecto compartido con otro campo, mutarlo in place corrompería la selección del otro campo, así que la rutina suelta la referencia y crea un array directo nuevo. Después resuelve /Opt a través de la cadena /Parent, ya que las opciones de elección se pueden heredar, y escanea las entradas. Una opción de string pelado se compara de forma directa; un par [export display] se compara por su elemento de exportación, y un par con menos de dos elementos se saltea. Los dos lados pasan por HPDFLoadedFormTextName, así que una opción hex UTF-16 matchea un valor hex UTF-16 sin que usted tenga que escribirlos iguales. En la primera coincidencia se escribe un /I de un elemento y el escaneo se detiene; un valor escalar siempre reemplaza cualquier multiselección anterior, sin importar el flag MultiSelect

Cómo mantiene HotPDF consistente un campo de elección: HPDFReconcileChoiceSelection borra el array /I local antes de tocarlo, resuelve /Opt a través de la cadena /Parent, compara la mitad de exportación de cada opción con HPDFLoadedFormTextName, escribe un /I de un elemento en la primera coincidencia y no escribe nada cuando el valor de un combo editable no tiene índice
Una opción de string pelado se compara de forma directa y un par export display por su elemento de exportación, mientras que un valor fuera de /Opt correctamente deja sin índice — un /I viejo apuntando a la fila equivocada sería peor que ninguno

Cuando nada matchea, no se escribe ningún /I. Ese es el resultado correcto para un combo box editable, donde §12.7.4.4 permite que el usuario escriba un valor fuera de la lista de opciones; un valor así no tiene índice, y un índice viejo sería peor que ninguno. También es lo que obtiene si le pasa una etiqueta de display en lugar de un valor de exportación a una lista de opciones con pares, así que cuando un combo box se niega a mostrar su selección, revise cuál de las dos mitades del par entregó

// /Opt es [[US United States] [CA Canada] [MX Mexico]]:
// se hace match por el valor de exportación y /I pasa a ser [1]
Pdf.SetFormFieldValue('Country', 'CA');

// Combo editable con un valor fuera de /Opt: se escribe /V,
// se elimina /I y no se fabrica ningún índice
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Valor y apariencia son dos operaciones separadas

SetFormFieldValue nunca toca el stream de apariencia de un campo de texto o de elección. Después de la llamada, /V guarda el texto nuevo mientras /AP /N sigue pintando el viejo, y cuál de los dos muestra un visor depende de si el diccionario AcroForm lleva /NeedAppearances true según §12.7.3.3 y de si el visor lo respeta. Si usted necesita que el archivo renderice el valor nuevo en todos los lectores, incluidos los flatteners y los generadores de miniaturas que ignoran el flag, llame a EnsureLoadedFieldAppearanceStream con el índice del campo. Arma un Form XObject a partir del string /DA heredado, el quadding /Q, el layout comb de /MaxLen y el valor, resuelve la fuente nombrada a través de los recursos /DR del AcroForm para que una fuente Type0 conserve su propia fuente descendiente en lugar de degradarse a Helvetica, y devuelve True cuando al menos un widget recibió un stream. La sobrecarga por nombre de SetFormFieldValue no le devuelve ningún índice, así que consígase uno con GetFormField, que devuelve un THPDFLoadedFormField que es suyo y que debe liberar. La suite de regresión del cambio de la v2.752.1 es explícita sobre esta división: fija un valor, llama a EnsureLoadedFieldAppearanceStream, después renderiza la página y chequea que los píxeles dentro del rectángulo del widget cambiaron mientras los de afuera no. Verificar que /V cambió no prueba nada sobre lo que va a ver un usuario

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // Pinta el valor nuevo en /AP para que los visores que ignoran
    // /NeedAppearances igual lo muestren
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Límites que conviene conocer antes de apoyarse en esto

ReconcileLoadedButtonAppearanceStates prueba el /FT local del diccionario que usted direccionó, así que actúa sobre el padre de un radio o sobre un checkbox que lleva su propio /FT; un widget hijo direccionado por su cuenta, con el /FT solo en su padre, no se reconcilia por ese camino. HPDFReconcileChoiceSelection maneja un único valor escalar y escribe como máximo un índice; los list box de multiselección con varios ítems elegidos quedan fuera de lo que modela SetFormFieldValue. Ninguna de las dos rutinas valida el valor que usted pasa contra /Opt o contra las claves de estado encendido, así que un error de tipeo produce un checkbox en Off o un combo sin índice en lugar de una excepción. Y GetFormFieldValue devuelve el texto /V guardado tal como está en el diccionario, lo que para un valor codificado en hex significa la grafía hexadecimal, no el texto decodificado

Una vez que los valores están puestos y las apariencias pintadas, los dos pasos siguientes naturales quedan a uno y otro lado de esta operación. Intercambiar datos de formulario con sistemas externos en bloque, en lugar de una llamada a SetFormFieldValue por vez, es lo que cubre la importación y exportación XFDF en Delphi. Y cuando el formulario lleno ya es final y no debería ser editable, el flattening de campos AcroForm y XFA en Delphi hornea exactamente los estados /AS y los streams de apariencia que se describen acá dentro del contenido estático de la página, y por eso dejarlos consistentes antes del flattening no es opcional

La API de edición de formularios cargados de este artículo, incluidos SetFormFieldValue, EnsureLoadedFieldAppearanceStream y el grafo de recálculo incremental, viene como parte de HotPDF Delphi Component para Delphi y C++Builder