En la lista de campos de casi cualquier visor PDF, el campo de calle de un formulario de reclamación en alemán lee Straße. Los bytes detrás de su entrada /T cuentan otra historia: <FEFF005300740072006100DF0065>, una cadena hexadecimal UTF-16BE con una marca de orden de byte delante. Un código que empareja nombres de campo contra esos bytes crudos reporta el campo como ausente, aunque un usuario pueda hacer clic directamente en él. El mismo formulario puede guardar el estado on de un checkbox como 2 en vez de Yes y su lista de países como pares [export display], lo que te da tres maneras separadas de que el valor que escribes y el que pinta un visor discrepen
Las tres variaciones vienen del mismo sitio: un formulario construido hace años con la herramienta de autoría que tuviera el cliente, uno que tu aplicación Delphi tiene ahora que rellenar desde una base de datos y devolver como un archivo que renderice igual en todos los lectores. No puedes re-autorar el formulario, así que el código de relleno tiene que seguir las reglas bajo las que se escribió el formulario. ISO 32000-1 tiene una regla para cada uno de los tres casos: §7.9.2.2 para text strings, §12.7.4.2.3 para estados de apariencia de botones, la Tabla 231 de §12.7.4.4 para selecciones de elección. Crear campos que aún no existen es otro trabajo, cubierto en añadir campos AcroForm a un PDF cargado en Delphi
Encontrar el campo: HotPDF decodifica /T antes de emparejar nombres
THotPDF.SetFormFieldValue tiene dos sobrecargas, una que toma un índice de campo zero-based y otra que toma un nombre completamente cualificado, y ambas resuelven contra una tabla de nombres que el componente construye desde el árbol AcroForm en el primer uso. Mientras se construye esa tabla, cada nombre parcial pasa por un decodificador. Una cadena /T hexadecimal se convierte de vuelta a bytes; si esos bytes empiezan con FE FF y tienen longitud par, el resto se lee como UTF-16BE y se re-codifica a UTF-8. Cada nombre parcial decodificado se une después a su padre con un punto, exactamente como describe §12.7.3.1, así que un campo hijo llamado City bajo un padre llamado Address se registra como Address.City. Solo la clave de búsqueda se decodifica. El objeto /T del archivo conserva su deletreo hexadecimal, y guardar el documento no toca la identidad de un campo que solo rellenaste
Las claves se pasan a minúsculas antes de entrar en la tabla, así que 'address.city' encuentra Address.City. Eso va más allá del estándar, que trata los nombres de campo como case-sensitive, y significa que dos campos que solo difieran en mayúsculas colisionarían en la búsqueda. Para código que lee nombres de campo de un archivo de configuración tecleado a mano, una mayúscula perdida es un fallo mucho más común que dos campos en un mismo formulario que difieran solo en mayúsculas
Los nombres decodificados son bytes UTF-8 llevados en un AnsiString, y ahí es donde las conversiones de cadena del propio Delphi se entrometen. Un literal como 'Applicant.Straße' pasado a un parámetro AnsiString se convierte a través de la página de códigos ANSI del sistema, así que ß llega como el byte único DF en lugar del par UTF-8 C3 9F, y la búsqueda falla. Los nombres ASCII no se ven afectados. Para cualquier otra cosa, enumera los campos una vez, decodifica cada nombre con UTF8ToString y escribe mediante la sobrecarga de índice:
// FindFieldIndex es un helper que añades tú: compara los nombres
// UTF-8 decodificados contra una cadena Unicode, así que ninguna
// página de códigos ANSI se interpone entre tu literal y la búsqueda
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldCount - 1 do
if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
Exit(I);
Result := -1;
end;
var
Pdf: THotPDF;
Street: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Los nombres ASCII pueden ir directos a la sobrecarga por nombre
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Street := FindFieldIndex(Pdf, 'Applicant.Straße');
// El valor también es un AnsiString, así que un literal 'ß' dependería
// de la página de códigos del sistema; el hex FEFF (Hauptstraße 12) lo evita
if Street >= 0 then
Pdf.SetFormFieldValue(Street,
'FEFF00480061007500700074007300740072006100DF0065002000310032');
// El mismo hex UTF-16BE con prefijo FEFF funciona con la sobrecarga
// por nombre (este es München); se escribe como una cadena hexadecimal
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
¿Qué escribe exactamente SetFormFieldValue?
Ambas sobrecargas corren la misma secuencia después de encontrar el campo: escribir /V, reconstruir el índice de selección de elección, marcar el campo como cambiado, conmutar los estados de apariencia de botón, regenerar la apariencia de texto o elección y registrar el campo en un dirty set. Ese último paso alimenta al RecalculateLoadedFormFieldsIncremental sin parámetros, que re-ejecuta solo los scripts de cálculo que leen un campo cambiado, directa o indirectamente a través de una cadena como línea de pedido, subtotal, total
El tipo de objeto del nuevo /V depende del campo. Para un checkbox o un radio button, SetFormFieldValue siempre escribe un objeto name, porque §12.7.4.2.3 guarda el estado seleccionado como un name y los nombres de PDF son bytes planos sin codificación de text string. Todos los demás campos reciben un objeto string, y el valor que pasas se inspecciona primero. Una cadena que empieza con FEFF, tiene longitud par y contiene solo dígitos hexadecimales se toma como la forma UTF-16BE de §7.9.2.2 y se serializa como <FEFF...>. De eso depende la línea del City de arriba. Cualquier otra cosa se guarda como literal string con exactamente los bytes que suministraste, así que texto latino plano entra como texto plano
¿Por qué un checkbox conserva su marca vieja tras cambiar el valor?
Para un campo botón, el valor no decide qué se dibuja. Un widget de checkbox lleva una entrada /AS que nombra qué stream de /AP /N se está mostrando, y los visores pintan desde /AS, no desde /V. Cambia /V a Yes mientras /AS sigue diciendo Off y el archivo se contradice; un flattener hornará la caja sin marcar en la página mientras los datos del formulario aseguran que está marcada. SetFormFieldValue visita por tanto el campo botón y cada entrada de su array /Kids, lee el nombre del estado on del /AP /N de cada widget y pone /AS a ese nombre cuando coincide con el valor del campo, o a Off cuando no
Los formularios reales doblan esa regla de dos maneras. La apariencia off se llama Off en el estándar, pero las herramientas de autoría a menudo dejan su stream fuera por completo y dejan que el visor no dibuje nada, así que un diccionario de apariencia normal con una sola entrada es perfectamente legal. El nombre del estado on es el que eligiera el autor: Yes, On, 2, una palabra en el idioma del propio formulario. Así que el estado on se toma como la primera clave de /AP /N que no sea Off, y la comparación con tu valor ignora mayúsculas. Los radio buttons añaden el arreglo de §12.7.4.2.4: el valor seleccionado vive en el /V del campo padre, mientras que cada widget hijo normalmente no tiene /V propio. Cada hijo se compara contra el valor heredado a través de su cadena de /Parent
// 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');
La cara negativa es que una errata no produce excepción. 'yes ' con un espacio final no empareja con nada, así que la caja acaba en Off y el archivo sigue siendo perfectamente válido. Si tus valores vienen de una columna de base de datos, recórtalos antes de la llamada
Campos de elección: mantener /I a la par con /V
Un combo box o un list box registra su selección en dos sitios. Junto a /V, la Tabla 231 define /I como un array de índices zero-based dentro de /Opt, y un visor que encuentre un /I apuntando a la opción 0 mientras /V nombra la opción 3 puede resaltar la fila equivocada. SetFormFieldValue reconstruye /I en cada llamada a un campo de elección. Borra la entrada /I propia del campo antes que nada y nunca edita el array viejo in situ: un array indirecto puede estar compartido con otro campo, y mutarlo cambiaría también la selección de ese campo. Después resuelve /Opt a través de la cadena de /Parent, porque las opciones pueden estar heredadas, y recorre las entradas. Una opción de cadena pelada se compara directamente, un par [export display] por su elemento de export, y un par con menos de dos elementos se salta
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
La misma regla está detrás de la queja más común sobre los combo boxes: la llamada tiene éxito, /V contiene el texto nuevo, y el visor no muestra nada seleccionado. Normalmente quien llamó pasó la etiqueta de display (Canada) a una lista construida con pares [CA Canada]. El componente no puede tratar eso como error sin romper los combos editables, así que la comprobación vive en tu código. GetFormFieldOptionValue devuelve el elemento de export de cada opción, lo que deja el guard en una línea:
// SetChoiceByExport es un guard tuyo, no forma parte del componente:
// rechaza valores que no casan con ningún elemento de export de /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
ExportValue: AnsiString);
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
begin
// /Opt es [[US United States] [CA Canada] [MX Mexico]]:
// 'CA' casa con el elemento de export y /I pasa a ser [1]
Pdf.SetFormFieldValue(FieldName, ExportValue);
Exit;
end;
raise Exception.CreateFmt('%s is not an export value of %s',
[string(ExportValue), string(FieldName)]);
end;
// Combo editable con texto libre: escríbelo directamente. Se fija /V,
// se elimina /I y no se inventa ningún índice
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
¿Aparece el valor nuevo sin /NeedAppearances?
Sí, para campos de texto y de elección. Un visor dibuja el stream /AP guardado y no mira el /V, así que un valor nuevo con una apariencia vieja deja el texto viejo en pantalla y en impresión salvo que el diccionario AcroForm lleve /NeedAppearances true (§12.7.3.3) y el visor decida honrarlo. Los flatteners y los generadores de thumbnails ignoran ese flag con frecuencia. SetFormFieldValue regenera el stream de apariencia para campos de texto y elección como parte de la llamada, usando el mismo builder que el EnsureLoadedFieldAppearanceStream público. El stream se construye desde la cadena /DA heredada, la alineación /Q, el /MaxLen para campos comb y el propio valor. La fuente nombrada en /DA se resuelve a través de los recursos /DR del AcroForm, así que una fuente Type0 conserva sus propios glifos en lugar de caer a Helvetica. Los botones conservan las apariencias de estado que dibujó su diseñador; para ellos solo se mueve el /AS
SetFormFieldValue es un procedure y no te dice si se pintó algo. Un widget sin un /Rect utilizable no recibe stream, y los formularios rotos con widgets huérfanos o sin rectángulo existen. Cuando el resultado renderizado importa, llama tú a EnsureLoadedFieldAppearanceStream después de fijar el valor. Devuelve True cuando al menos un widget recibió stream. GetFormField te entrega un THPDFLoadedFormField cuya propiedad Index es lo que la llamada espera; ese objeto es tuyo y hay que liberarlo
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;
Comprobar que el /V cambió dice muy poco de lo que verá un usuario. Un test que signifique algo renderiza la página antes y después y confirma que los píxeles dentro del rectángulo del widget cambiaron mientras los de fuera no. Nosotros conservamos esa comprobación porque los bytes de apariencia solos nos han engañado antes: un stream tenía cada byte correcto mientras un visor que honraba /NeedAppearances true reconstruía el display desde el /V y no mostraba nada de ello
Leer valores de vuelta, y lo que suele venir después
GetFormFieldValue devuelve el /V heredado del campo. Un valor guardado como hex con prefijo FEFF vuelve decodificado a UTF-8, así que el campo City de arriba se lee de vuelta como los bytes UTF-8 de München y UTF8ToString lo convierte en una cadena Delphi. Un literal string vuelve con los bytes con los que se guardó. Para comprobaciones de round trip, compara texto decodificado en ambos lados en vez del deletreo que pasaste
Mover datos de campo en bloque entre el formulario y un sistema externo, en lugar de una llamada por campo, es el tema de importación y exportación de XFDF en Delphi. Bloquear un formulario terminado para que nadie pueda editarlo es aplanar campos AcroForm y XFA en Delphi, que convierte exactamente los estados /AS y los streams de apariencia descritos aquí en contenido de página estático. Lo que sea inconsistente en ese momento se queda inconsistente para siempre, así que rellena, comprueba y después aplana
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