Pulse Tab en un formulario PDF creado por su código y el cursor puede caer dos campos más allá de donde debería, saltarse por completo la segunda columna o volver arriba después del tercer campo en vez de pasar al cuarto. Quien rellena una factura en su visor espera que el teclado recorra el formulario igual que recorre cualquier formulario web que haya utilizado. Cuando no lo hace, recurre al ratón, busca el siguiente cuadro y decide en silencio que su herramienta está inacabada. Un recorrido predecible por los campos marca la diferencia entre un visor de introducción de datos que la gente tolera y uno en el que confía, y depende casi por completo de usar la API de foco correcta en lugar de simular la entrada de teclado con clics artificiales
Los ejemplos siguientes usan PDFium Component, un componente VCL/LCL basado en PDFium para Delphi, C++Builder y Lazarus. La navegación es una de las tres cosas que un visor de formularios debe resolver; las otras dos, abrir correctamente el formulario y guardar los valores rellenados para que realmente aparezcan, son donde se esconden la mayoría de las sorpresas, así que aquí se cubren las tres
Abrir un formulario: FormFill, FormType y la cuestión de XFA
El acceso a campos requiere que el subsistema de relleno de formularios, controlado por la propiedad FormFill, esté activado antes de abrir el documento. Una vez activo, FormType indica qué tipo de formulario se tiene delante, y la respuesta cambia el conjunto de funciones que se puede prometer:
Pdf.FileName := FormPath;
Pdf.FormFill := True; // actívalo antes que Active; obligatorio para tocar campos
Pdf.Active := True;
case Pdf.FormType of
ftNone:
DisableFormPanel('This document has no interactive form');
ftAcroForm:
BuildFieldList; // navegación y edición de campos completas
ftXfaFull:
ShowXfaNotice; // XFA se renderiza desde su propia plantilla XML;
// trata la edición de campos como limitada
end;
De ese bloque se derivan dos observaciones prácticas. AcroForm es el modelo de formularios estándar de ISO 32000, y es al que se dirigen todas las API de este artículo. Los documentos XFA incorporan su propia arquitectura de formularios XML, por lo que prometer a un cliente edición XFA completa después de una rápida demostración de AcroForm es un compromiso del que se arrepentirá. La segunda observación tiene que ver con los efectos secundarios: establecer FormFill en True también inicializa JavaScript del documento. En un visor de introducción de datos es justo lo correcto, porque los scripts de cálculo mantienen actualizado un total acumulado mientras alguien escribe. En una ventana de vista previa para archivos de origen desconocido es justo lo contrario. El artículo sobre vista previa segura de PDF explica el otro lado de esa decisión, FormFill := False
Recorrido con la tecla Tab que llega donde esperan los usuarios
Volvamos al problema de teclado del principio. La tentación consiste en simular Tab generando un clic de ratón sobre el rectángulo del siguiente widget, lo que falla en cuanto un campo queda fuera de la pantalla tras desplazarse o dos widgets se superponen. En cambio, la API de foco desplaza directamente el foco propio del formulario, sin suposiciones geométricas. Cinco llamadas lo cubren: FocusFormField por índice, FocusNextFormField y FocusPreviousFormField para avanzar y retroceder, FocusedFormFieldIndex para saber dónde se está y ClearFormFieldFocus para quitar el foco por completo
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
if ssShift in Shift then
PdfView.FocusPreviousFormField
else
PdfView.FocusNextFormField;
UpdateFieldStatus; // e.g. "Field 4 of 17: InvoiceDate"
end;
El comportamiento que más suele confundir es el retorno. El recorrido sigue el orden de tabulación de la página actual y se repite dentro de él: al avanzar más allá del último campo se vuelve al primero. Ambas funciones de avance devuelven el índice del nuevo campo, o -1 cuando la página no contiene ningún campo. Esa repetición es por página, no por documento, lo que significa que pasar a la página siguiente es responsabilidad de su código, no de la biblioteca. Compare el índice devuelto con el de partida, detecte cuándo se ha producido el retorno y avance PageNumber por su cuenta si el formulario debe leerse como una secuencia continua. Si se omite esa comprobación, un formulario de dos páginas atrapa silenciosamente el cursor en la primera, otra variante de la queja sobre Tab que no funciona
El recorrido resulta útil cuando el resto de la interfaz reacciona a él. El evento OnFormFieldEnter se dispara cuando llega el foco y, en el visor, OnFormFieldFocusChange informa del nuevo índice de campo, de modo que un panel lateral puede mantenerse sincronizado con lo que el teclado acaba de seleccionar. Cuando se necesita la asignación inversa, de una posición en pantalla a un campo, la propiedad indexada FormFieldAt realiza la detección de impacto para vistas previas de información emergente y paneles de editar al hacer clic. Todo ello aporta una ventaja silenciosa de accesibilidad: como el foco sigue el orden de campos del propio documento, la ruta conectada a la tecla Tab es la misma que anuncia un lector de pantalla, sin trabajo adicional
Mostrar nombres de campo en vez de números de índice sin procesar requiere una propiedad más. FormFieldInfo[] devuelve un registro TPdfFormFieldInfo por índice, con el nombre, tipo, tamaño de fuente, estado marcado, valor de exportación y pertenencia a grupo del campo, que es lo que debería mostrar una lista de navegación («Campo 4 de 17: InvoiceDate» en lugar de «4»). Los grupos de botones de opción son el caso que merece un archivo de prueba específico. Varios widgets pueden compartir un mismo nombre de campo, así que una lista formada ingenuamente a partir de widgets muestra el mismo grupo varias veces y confunde a cualquiera que la consulte
Por qué los valores rellenados salen en blanco y la llamada que lo corrige
La otra queja que llena las colas de soporte es más alarmante que una tecla Tab que se comporta mal: un formulario se rellena mediante programación, el cliente lo abre en Acrobat y todos los campos parecen vacíos. Al hacer clic dentro de un campo, su valor aparece de repente. Los datos han estado siempre en el archivo. Lo que falta es la representación de los datos, y merece la pena entender el motivo una vez porque explica toda una familia de errores
Un campo de texto AcroForm almacena su valor en la entrada /V del diccionario de campo (ISO 32000-1 §12.7.3.3). Lo que un visor pinta realmente es algo distinto: el flujo de apariencia del widget bajo /AP (§12.5.5), un pequeño fragmento de contenido prerenderizado. Escriba /V y deje /AP intacto, y ambos quedan desincronizados. El valor está ahí; su versión renderizada está desactualizada o ausente. Acrobat resulta que reconstruye la apariencia de un campo cuando recibe el foco, y esa es toda la explicación de los valores que solo aparecen al hacer clic. La antigua marca NeedAppearances, que pedía a los visores que regenerasen las apariencias, nunca funcionó de forma uniforme y está obsoleta en PDF 2.0, y los servidores de impresión y generadores de miniaturas la ignoran por completo. Pintan /AP y nada más, así que si /AP está vacío imprimen un cuadro en blanco
Asignar un valor mediante FormField[i] escribe solo /V. Por eso rellenar un formulario es una secuencia de tres pasos, y el que suelen omitir los equipos es el del medio:
procedure TFormViewer.FillAndSave(const Values: array of WString;
const OutputPath: string);
var
i: Integer;
begin
for i := 0 to Pdf.FormFieldCount - 1 do
Pdf.FormField[i] := Values[i]; // writes /V only
// Reconstruye los streams de apariencia /AP; sin esto el formulario
// se ve vacío en Acrobat hasta que se pincha cada campo
Pdf.GenerateFormAppearances;
Pdf.SaveAs(OutputPath);
end;
GenerateFormAppearances es toda la solución. Reconstruye el flujo de apariencia de cada widget a partir de los valores, fuentes y alineación actuales, de forma que incluso un visor que nunca ejecuta un evento de foco, un servidor de impresión o un generador de miniaturas pinta el estado rellenado. Llámelo una vez después del lote de asignaciones, no una vez por campo. La generación de apariencias realiza trabajo real de maquetación, y las llamadas por campo lo multiplican innecesariamente en un formulario grande
Regenerar las apariencias es también el momento en que se imponen las fuentes y la alineación, de donde surge una sorpresa de segundo orden. El nuevo flujo distribuye cada valor dentro del rectángulo del widget usando la fuente, tamaño y alineación del campo. Un valor que cabe holgadamente en el formulario de prueba puede recortarse o reducirse en la copia de un cliente donde el mismo campo es más estrecho. Los campos de tamaño automático (tamaño de fuente cero) reducen el texto para ajustarlo; los de tamaño fijo simplemente lo recortan. Ambos comportamientos son válidos, y la única forma honesta de saber qué hace un formulario concreto es examinar la salida regenerada en vez de la cadena escrita. Cuando alguien informa de texto cortado en el borde de un cuadro, casi siempre esta es la razón
Trate la verificación como parte de finalizar el trabajo, no como una ocurrencia tardía. Abra el archivo guardado en Acrobat y confirme que los valores son visibles antes de tocar ningún campo. Después imprímalo a PDF o a una imagen desde otro visor, uno que ignore por completo la lógica de formularios, y confirme que los valores sobreviven también a esa ruta. Entre ambas comprobaciones se detectan todas las variantes de desincronización entre /V y /AP
Configuraciones de campos que superan la demostración y fallan en producción
Los formularios de demostración limpios ocultan un conjunto de casos límite que los archivos de clientes no ocultan. Cuatro de ellos explican la mayoría de los informes de «funcionaba en mi equipo»
- Valores de exportación de casillas de verificación. El estado «activado» no siempre es
Yes. Un formulario puede definir su propio valor de exportación, y escribir la cadena equivocada deja la casilla visualmente desmarcada mientras el código cree que la ha marcado. Lea el valor de exportación desdeFormFieldInfo[]en lugar de darlo por supuesto - Grupos de botones de opción con nombres compartidos. Un campo, varios widgets. El valor que se asigna decide qué widget aparece seleccionado, por lo que un código de interfaz que supone que un nombre corresponde a un rectángulo acaba dibujando el anillo de foco en el botón equivocado
- Campos calculados. Los totales mantenidos por JavaScript del documento se actualizan en respuesta a eventos de campo. Un relleno mediante programación que evita esos eventos debe desencadenar el recálculo o sobrescribir directamente los campos calculados. Un formulario donde las partidas y el total no coinciden es peor que cualquiera de ambas soluciones
- Campos obligatorios ocultos. Los formularios condicionales ocultan campos que siguen marcados como obligatorios. Decida desde el principio si la validación respeta la visibilidad o la marca de obligatorio sin procesar, y deje escrita esa decisión en algún sitio que soporte pueda encontrar
Hay una distinción que conviene resolver antes de que cause problemas: generar apariencias no es aplanar. GenerateFormAppearances hace visibles los valores en todas partes y mantiene los campos editables. Aplanar incorpora la apariencia al contenido estático de la página y elimina definitivamente la interactividad, lo cual es adecuado para una copia de archivo y erróneo para un formulario que la siguiente persona aún debe rellenar. Si FormType informa ftXfaFull en lugar de ftAcroForm, ninguna de las superficies de edición descritas aquí se aplica limpiamente de todos modos, ya que el documento se renderiza desde su propia plantilla XML; detecte ese caso e informe al usuario, en vez de dejar que descubra el límite por sí mismo
El subsistema de relleno de formularios, el recorrido de foco y la generación de apariencias mostrados aquí forman parte de PDFium Component para Delphi, C++Builder y Lazarus/FPC. Si el visor también gestiona marcas de revisión junto con los datos de formulario, el artículo sobre revisión de anotaciones cubre ese modelo relacionado