En la lista de campos de casi cualquier visor de PDF, el campo de calle de un formulario de reclamos en alemán dice 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 bytes adelante. El código que hace match de nombres de campo contra esos bytes crudos reporta el campo como faltante, aunque un usuario pueda hacer clic directo en él. El mismo formulario puede guardar el estado encendido de un checkbox como 2 en lugar de Yes y su lista de países como pares [export display], lo que le da tres maneras separadas de que el valor que usted escribe y el valor que un visor pinta discrepen
Las tres variaciones vienen del mismo lugar: un formulario construido hace años en la herramienta de autor que fuera, una que su aplicación Delphi ahora tiene que llenar desde una base de datos y devolver como un archivo que renderiza igual en cada lector. Usted no puede re-autorar el formulario, así que el código de llenado tiene que seguir las reglas bajo las que el formulario fue escrito. 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, Tabla 231 en §12.7.4.4 para selecciones de elección. Crear campos que todavía no existen es un trabajo distinto, cubierto en agregar campos AcroForm a un PDF cargado en Delphi
Encontrar el campo: HotPDF decodifica /T antes de hacer match de nombres
THotPDF.SetFormFieldValue tiene dos sobrecargas, una que toma un índice de campo base cero y otra que toma un nombre totalmente calificado, y ambas resuelven contra una tabla de nombres que el componente construye del árbol AcroForm en el primer uso. Mientras esa tabla se construye, cada nombre parcial pasa por un decodificador. Una cadena /T hexadecimal se vuelve a convertir en bytes; si esos bytes arrancan con FE FF y tienen longitud par, el resto se lee como UTF-16BE y se recodifica como 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 en el archivo conserva su grafía hex, y guardar el documento no toca la identidad de un campo que usted solo llenó
Las claves se pasan a minúsculas antes de entrar a la tabla, así que 'address.city' encuentra Address.City. Eso va más allá del estándar, que trata los nombres de campo como sensibles a mayúsculas, y significa que dos campos que difieran solo 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 una falla mucho más común que dos campos en un formulario que difieran solo por mayúsculas
Los nombres decodificados son bytes UTF-8 llevados en un AnsiString, y ahí es donde las conversiones de cadena del propio Delphi estorban. Un literal como 'Applicant.Straße' pasado a un parámetro AnsiString se convierte por 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 afectan. Para cualquier otra cosa, enumere los campos una vez, decodifique cada nombre con UTF8ToString, y escriba por la sobrecarga de índice:
// FindFieldIndex es un helper que usted agrega: compara los
// nombres UTF-8 decodificados contra una cadena Unicode, así que ninguna
// página de códigos ANSI se interpone entre su 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 directo 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 cadena hex
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Qué escribe SetFormFieldValue, paso a paso
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 conjunto de sucios. Ese último paso alimenta el RecalculateLoadedFormFieldsIncremental sin parámetros, que vuelve a correr solo los scripts de cálculo que leen un campo cambiado, directa o indirectamente a través de una cadena como ítem de línea, subtotal, total
El tipo de objeto del nuevo /V depende del campo. Para un checkbox o radio button, SetFormFieldValue siempre escribe un objeto name, porque §12.7.4.2.3 guarda el estado seleccionado como un name y los nombres PDF son bytes planos sin codificación de text string. Todos los demás campos reciben un objeto string, y el valor que usted pasa se inspecciona primero. Una cadena que arranca con FEFF, tiene longitud par y contiene solo dígitos hex se toma como la forma UTF-16BE de §7.9.2.2 y se serializa como <FEFF...>. De eso depende la línea de City de arriba. Cualquier otra cosa se guarda como cadena literal con exactamente los bytes que usted suministró, así que texto latino plano entra como texto plano
¿Por qué un checkbox conserva su tilde vieja después de cambiar el valor?
Para un campo de botón el valor no decide qué se dibuja. Un widget de checkbox lleva una entrada /AS que nombra cuál stream de /AP /N se está mostrando, y los visores pintan desde /AS, no desde /V. Cambie /V a Yes mientras /AS todavía dice Off y el archivo se contradice; un flattener horneará el checkbox sin marcar en la página mientras los datos del formulario dicen que está marcado. SetFormFieldValue por eso visita el campo de botón y cada entrada de su arreglo /Kids, lee el nombre del estado encendido del /AP /N de cada widget, y fija /AS en ese nombre cuando coincide con el valor del campo o en Off cuando no
Los formularios reales doblan esa regla de dos maneras. La apariencia apagada se llama Off en el estándar, pero las herramientas de autor 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 encendido es el que el autor eligió: Yes, On, 2, una palabra en el idioma del propio formulario. Así que el estado encendido se toma como la primera clave en /AP /N que no sea Off, y la comparación con su valor ignora mayúsculas. Los radio buttons agregan el arreglo de §12.7.4.2.4: el valor seleccionado vive en /V del campo padre, mientras que cada widget hijo usualmente no tiene /V propio. Cada hijo se compara contra el valor heredado a través de su cadena /Parent
// Checkbox: el valor debe ser igual a la clave del estado encendido en /AP /N
// ('Yes' aquí; el diseñador del formulario pudo haber elegido '2' o 'On')
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grupo de radio: /V va en el padre como name; cada widget hijo
// recibe /AS en su propio nombre de estado encendido o en Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Limpiar un checkbox: un valor que no coincide con ningún estado encendido deja /AS en Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
El revés es que un error de tipeo no produce excepción. 'yes ' con un espacio final no coincide con nada, así que el box termina en Off y el archivo sigue siendo perfectamente válido. Si sus valores vienen de una columna de base de datos, recórtelos antes de la llamada
Campos de elección: mantener /I en sintonía con /V
Un combo box o list box registra su selección en dos lugares. Junto a /V, la Tabla 231 define /I como un arreglo de índices base cero dentro de /Opt, y un visor que encuentra /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 arreglo viejo in place: un arreglo indirecto puede estar compartido con otro campo, y mutarlo cambiaría también la selección de ese campo. Luego resuelve /Opt a través de la cadena /Parent, ya que las opciones pueden heredarse, y recorre las entradas. Una opción de cadena pelada se compara directamente, un par [export display] por su elemento de exportación, y un par con menos de dos elementos se salta
La comparación corre sobre texto decodificado, así que una opción guardada en PDFDocEncoding coincide con un valor escrito como hex UTF-16BE y viceversa. Un valor escalar produce a lo sumo un índice; cualquier multiselección que el campo tuviera antes se reemplaza. Cuando nada coincide, no se escribe ningún /I. Para un combo box editable ese es el resultado correcto, porque §12.7.4.4 deja que el usuario teclee un valor fuera de la lista y tal valor no tiene índice a donde apuntar
La misma regla está detrás de la queja más común sobre los combo boxes: la llamada tiene éxito, /V carga el texto nuevo, y el visor no muestra nada seleccionado. Usualmente el caller pasó la etiqueta de display (Canada) a una lista construida de pares [CA Canada]. El componente no puede tratar eso como error sin romper los combos editables, así que el chequeo pertenece a su código. GetFormFieldOptionValue devuelve el elemento de exportación de cada opción, lo que hace un guardia corto:
// SetChoiceByExport es un guardia suyo, no parte del componente:
// rechaza valores que no coinciden con ningún elemento de exportación en /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' coincide con el elemento de exportación 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íbalo directamente. /V se fija,
// /I se elimina, y no se inventa ningún índice
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
¿Aparece el valor nuevo sin /NeedAppearances?
Sí, para campos de texto y elección. Un visor dibuja el stream /AP almacenado y no mira /V, así que un valor nuevo con apariencia vieja deja el texto viejo en pantalla y en impresión a menos que el diccionario AcroForm cargue /NeedAppearances true (§12.7.3.3) y el visor decida honrarlo. Los flatteners y generadores de miniaturas frecuentemente ignoran ese flag. SetFormFieldValue regenera el stream de apariencia para campos de texto y elección como parte de la llamada, usando el mismo constructor 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 valor mismo. 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 su diseñador dibujó; para ellos solo se mueve /AS
SetFormFieldValue es un procedure y no le dice si algo quedó pintado. Un widget sin un /Rect utilizable no recibe stream, y existen formularios rotos con widgets huérfanos o sin rectángulo. Cuando el resultado renderizado importa, llame usted mismo a EnsureLoadedFieldAppearanceStream después de fijar el valor. Devuelve True cuando al menos un widget recibió un stream. GetFormField le entrega un THPDFLoadedFormField cuya propiedad Index es lo que la llamada espera; ese objeto es suyo y debe liberarlo
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// False significa que ningún widget de este campo tuvo un rectángulo
// donde pintar; el valor queda guardado, pero nada lo va a mostrar
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;
Chequear que /V cambió prueba muy poco sobre lo que un usuario va a ver. Una prueba que significa 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 afuera no. Mantenemos ese chequeo porque los bytes de apariencia solos ya nos engañaron antes: un stream cargaba cada byte correcto mientras un visor que honra /NeedAppearances true reconstruía el display desde /V y no mostraba nada de eso
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. Una cadena literal vuelve con los bytes con los que se guardó. Para chequeos de ida y vuelta, compare texto decodificado de ambos lados en lugar de la grafía que usted pasó
Mover datos de campo en bloque entre el formulario y un sistema externo, en lugar de una llamada por campo, es el tema de la importación y exportación XFDF en Delphi. Bloquear un formulario terminado para que nadie lo edite es el flattening de campos AcroForm y XFA en Delphi, que convierte exactamente los estados /AS y los streams de apariencia descritos aquí en contenido estático de página. Lo que quede inconsistente en ese momento se queda inconsistente para siempre, así que llene, chequee, y recién entonces aplane
SetFormFieldValue, EnsureLoadedFieldAppearanceStream y la API de recálculo incremental son parte del HotPDF Delphi Component, junto a las llamadas que renombran, eliminan y aplanan campos en un documento que usted cargó desde Delphi o C++Builder