HotPDF Delphi Component rellena un campo AcroForm existente de un PDF cargado a través de THotPDF.SetFormFieldValue, dirigido o bien por índice de campo basado en cero o bien por nombre de campo completamente cualificado. Escribir la nueva entrada /V es la parte fácil; lo que hace fiable la llamada en formularios del mundo real es que el mismo método mantiene además consistentes tres piezas de estado que son invisibles hasta que se rompen: la identidad decodificada del campo, para que un nombre no ASCII se pueda encontrar siquiera, 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 appearance stream visible es un paso aparte y explícito a través de EnsureLoadedFieldAppearanceStream
El escenario es el más mundano: un cliente te manda su propio formulario, una declaración de la renta, un parte de seguro, un pedido que alguien montó en Acrobat hace años, y tu aplicación Delphi tiene que rellenarlo desde una base de datos y devolver un archivo que abra bien en todas partes. No tienes ningún 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 boxes pueden usar pares de opción [export display]. Cada uno de esos detalles tiene una regla en la ISO 32000-1, y cada regla es algo que SetFormFieldValue ahora resuelve por ti. Este artículo va de qué hace, por qué, y dónde se detiene. Para el problema hermano de crear campos que todavía no existen, mira añadir 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 UTF-16BE hexadecimal, y la caché de nombres guardaba la grafía hex en lugar del texto. La ISO 32000-1 §12.7.3.1 define el nombre parcial de campo /T como una cadena de texto, y la §7.9.2.2 dice que una cadena de texto puede ser UTF-16BE con un byte order mark FE FF delante. Las herramientas de autor serializan esos nombres como cadenas hex según la §7.3.4.3 casi por costumbre, 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 quieres para un ida y vuelta sin pérdida del diccionario original y exactamente lo que no quieres como clave de búsqueda. HPDFLoadedFormTextName separa las dos preocupaciones. Cuando se construye la 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 por 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 completamente cualificado que describe la §12.7.3.1, así que un hijo llamado City bajo un padre llamado Address queda registrado como Address.City. La clave de 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. Y lo crucial: solo cambia la clave de caché. El objeto /T del diccionario de campo conserva su codificación hexadecimal, así que guardar el documento no reescribe la identidad de un campo que te limitaste a rellenar
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Los nombres cualificados se decodifican de cadenas /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 una cadena hexadecimal de PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
¿Qué escribe exactamente SetFormFieldValue?
Las dos sobrecargas ejecutan los mismos cinco pasos: localizar 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 botones, y por último registrar el índice del campo a través de 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 reejecutar solo los cálculos que leen de forma transitiva un campo modificado. HPDFSetDictFormValue es cuidadosa con el tipo de objeto que reemplaza. Si el /V existente es un objeto name, que es lo que usan los campos 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. En caso contrario escribe un objeto string e inspecciona el valor que le pasaste: una cadena que empieza por FEFF, tiene longitud par y se compone solo de dígitos hex se trata como la forma de cable UTF-16BE de la §7.9.2.2 y se guarda con IsHexadecimal puesto, así que serializa como <FEFF...> en lugar de como un literal (FEFF...). Ese es el mecanismo en el que se apoya la línea de City de arriba; cualquier otra cadena se guarda como string literal con los bytes que le diste, así que para texto latino corriente pasas texto corriente
¿Por qué un checkbox conserva su marca antigua tras cambiar el valor?
Porque en un campo de botón el valor por sí solo no decide lo que se dibuja. La ISO 32000-1 §12.7.4.2.3 especifica que un widget de checkbox lleva un estado de apariencia /AS que nombra qué stream de /AP /N se muestra en ese momento, y los visores pintan desde /AS, no desde /V. Si cambias /V a Yes pero dejas /AS en Off, el archivo es internamente contradictorio, y el flattening se encargará alegremente de hornear en la página la apariencia obsoleta de no marcado 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 en sí y cada entrada de su array /Kids, lee el nombre del estado on de /AP /N, y reescribe /AS a ese nombre cuando casa con el valor del campo o a Off cuando no
Dos detalles de formularios reales dieron forma al fix de la v2.752.3. Primero, a un diccionario de apariencia normal se le permite contener solo el estado on; la §12.7.4.2.3 llama Off a la apariencia de apagado, pero las herramientas de autor omiten con frecuencia su stream y dejan que el visor no dibuje nada. El código anterior se rendía cuando el diccionario tenía menos de dos entradas, así que esos checkboxes de un solo estado conservaban su marca antigua en silencio. La comprobación ahora es simplemente que el diccionario no esté vacío, y el nombre del estado on se toma como la primera clave que no sea Off. Segundo, el nombre del estado on es el que eligió 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 añaden una arruga más, descrita en la §12.7.4.2.4: la selección vive en el /V del campo padre, mientras que los hijos individuales son dueños de los widgets y normalmente no tienen /V propio. El helper anidado InheritedButtonValue recorre por tanto la cadena /Parent hacia arriba, hasta 64 niveles, hasta encontrar un valor no vacío, así que cada hijo se compara contra el valor del grupo al que pertenece. Poner el padre al valor de exportación de un hijo enciende exactamente ese hijo y apaga todos sus hermanos
// Checkbox: el valor de exportación debe casar con la clave del estado on 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 a su propio nombre de exportación o a Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Desmarcar un checkbox: cualquier valor que no case con ningún estado on da /AS 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 sitio donde se registra una selección. La Tabla 231 de la §12.7.4.4 define /I como un array de índices basados en cero dentro de /Opt que identifica los elementos seleccionados, y un visor que encuentre /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 local /I se borra primero, sin tocar su contenido: si el array viejo era un objeto indirecto compartido con otro campo, mutarlo in situ corrompería la selección del otro campo, así que la rutina suelta la referencia y crea un array directo nuevo. Luego resuelve /Opt a través de la cadena /Parent, ya que las opciones de elección pueden heredarse, y escanea las entradas. Una opción de cadena pelada se compara directamente; un par [export display] se compara por su elemento de exportación, y un par con menos de dos elementos se salta. Los dos lados pasan por HPDFLoadedFormTextName, así que una opción hex UTF-16 casa con un valor hex UTF-16 sin que tengas que escribirlos idénticos. En la primera coincidencia se escribe un /I de un elemento y el escaneo se detiene; un valor escalar reemplaza siempre cualquier multiselección anterior, independientemente del flag MultiSelect
Cuando no casa nada, no se escribe /I en absoluto. Ese es el resultado correcto para un combo box editable, donde la §12.7.4.4 permite al usuario teclear un valor fuera de la lista de opciones; ese valor no tiene índice, y un índice obsoleto sería peor que ninguno. También es lo que obtienes si le pasas una etiqueta de visualización en lugar de un valor de exportación a una lista de opciones emparejadas, así que cuando un combo box se niegue a mostrar tu selección, comprueba qué mitad del par le has dado
// /Opt es [[US United States] [CA Canada] [MX Mexico]]:
// casar 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 distintas
SetFormFieldValue nunca toca el appearance stream de un campo de texto o de elección. Después de la llamada, /V contiene 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 la §12.7.3.3 y de si el visor lo respeta. Si necesitas que el archivo renderice el valor nuevo en todos los lectores, incluidos flatteners y generadores de miniaturas que ignoran el flag, llama a EnsureLoadedFieldAppearanceStream con el índice del campo. Construye un Form XObject a partir de la cadena /DA heredada, el quadding /Q, la disposición 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 te devuelve ningún índice, así que consigue uno con GetFormField, que devuelve un THPDFLoadedFormField que es tuyo y que tienes que liberar. La suite de regresión del cambio de la v2.752.1 es explícita sobre esta separación: asigna un valor, llama a EnsureLoadedFieldAppearanceStream, y luego renderiza la página y comprueba que los píxeles dentro del rectángulo del widget cambiaron mientras los de fuera no. Verificar que /V cambió no demuestra nada sobre lo que verá un usuario
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Pintar el valor nuevo en /AP para que los visores que ignoran
// /NeedAppearances lo sigan mostrando
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 construir sobre esto
ReconcileLoadedButtonAppearanceStates comprueba el /FT local del diccionario al que dirigiste, así que actúa sobre el padre de un radio o sobre un checkbox que lleve su propio /FT; un widget hijo dirigido por sí solo, con el /FT solo en su padre, no se reconcilia por ese camino. HPDFReconcileChoiceSelection maneja un único valor escalar y escribe como mucho un índice; los list boxes de multiselección con varios elementos elegidos quedan fuera de lo que modela SetFormFieldValue. Ninguna de las dos rutinas valida el valor que pasas contra /Opt ni contra las claves de estado on, así que una errata produce un checkbox 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, que para un valor codificado en hex significa la grafía hexadecimal, no el texto decodificado
Una vez que los valores están dentro y las apariencias pintadas, los dos siguientes pasos naturales se sientan a cada lado de esta operación. Intercambiar datos de campo con sistemas externos en bloque, en lugar de una llamada a SetFormFieldValue cada vez, es lo que cubre la importación y exportación XFDF en Delphi. Y cuando el formulario relleno es definitivo y ya no debe ser editable, el flattening de campos AcroForm y XFA en Delphi hornea exactamente los estados /AS y los appearance streams descritos aquí en contenido de página estático, 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 del HotPDF Delphi Component para Delphi y C++Builder