Artículo técnico

PDF Form Field Navigation in Delphi (PDFium Component)

Presione Tab en un formulario PDF generado por su código, y el cursor terminará a dos campos de distancia de donde debería, o se saltará la segunda columna por completo, o volverá al principio tras el tercer campo en lugar del cuarto. La persona que completa una factura en su visor espera que el teclado recorra el formulario de la misma forma en que recorre cualquier formulario web que haya utilizado. Cuando no es así, recurre al mouse, busca el siguiente cuadro y decide en silencio que su herramienta está incompleta. Un recorrido de campos predecible marca la diferencia entre un visor de entrada de datos que la gente tolera y uno en el que confía, y es casi en su totalidad una cuestión de usar la API de enfoque (focus) correcta en lugar de simular la entrada del teclado con clics simulados

Los ejemplos a continuación utilizan PDFium Component, un componente VCL/LCL basado en PDFium para Delphi, C++Builder y Lazarus. La navegación es una de las tres funciones que un visor de formularios debe resolver correctamente; las otras dos (abrir el formulario adecuadamente y guardar los valores completados de modo que se muestren) son donde se ocultan la mayoría de las sorpresas, por lo que las tres se analizan a continuación

Abrir un formulario: FormFill, FormType y la cuestión de XFA

El acceso a los campos requiere que el subsistema de llenado de formularios, controlado por la propiedad FormFill, se active antes de abrir el documento. Una vez activo, FormType le indica a qué tipo de formulario se enfrenta, y la respuesta define las funcionalidades que puede ofrecer:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Dos notas prácticas se derivan de ese cambio. AcroForm es el modelo de formulario estándar de la norma ISO 32000, y es el objetivo de cada API aquí descrita. Los documentos XFA integran su propia arquitectura de formulario basada en XML, por lo que prometer a un cliente la edición completa de XFA tras una demostración rápida de AcroForm es un compromiso que lamentará. La segunda nota se refiere a los efectos secundarios: establecer FormFill en True también inicializa el JavaScript del documento. En un visor de entrada de datos esto es lo correcto, ya que los scripts de cálculo mantienen el total actualizado a medida que el usuario escribe. En una ventana de vista previa para archivos de origen desconocido, esto es un error. El artículo sobre vista previa de PDF segura cubre la opción FormFill := False de esa compensación

Recorrido con la tecla Tab que llega a donde los usuarios esperan

Volviendo al problema del teclado planteado al principio. La tentación consiste en simular el Tabulador sintetizando un clic del mouse en el rectángulo del siguiente control (widget), lo cual falla en el instante en que un campo se desplaza fuera de la pantalla o dos controles se superponen. En su lugar, la API de enfoque mueve el foco del propio formulario directamente, sin adivinar la geometría. Cinco llamadas cubren esta función: FocusFormField por índice, FocusNextFormField y FocusPreviousFormField para avanzar y retroceder, FocusedFormFieldIndex para leer la posición actual 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 único comportamiento que confunde a los desarrolladores es el ciclo de retorno (wrap). El recorrido funciona a través del orden de tabulación de la página actual y entra en bucle dentro de ella: si avanza más allá del último campo, vuelve al primero. Ambas funciones de avance devuelven el nuevo índice de campo, o -1 si la página no contiene campos. Ese bucle es por página, no por documento, lo que significa que pasar a la siguiente página es tarea de su código, no de la biblioteca. Compare el índice devuelto con el de origen, detecte cuándo ha vuelto al principio y avance el PageNumber usted mismo si el formulario está diseñado para leerse como una secuencia continua. Si omite esa comprobación, un formulario de dos páginas atrapará silenciosamente el cursor en la página uno, lo cual es otra variante del problema del Tabulador defectuoso

El recorrido resulta útil una vez que el resto de la interfaz gráfica reacciona a él. El evento OnFormFieldEnter se activa cuando llega el foco, y en el visor OnFormFieldFocusChange reporta el nuevo índice del campo, de modo que un panel lateral puede sincronizarse con lo que el teclado acaba de seleccionar. Cuando necesite la correspondencia inversa (de una posición en pantalla a un campo), la propiedad indexada FormFieldAt realiza la detección de posición para vistas previas de ayuda (tooltips) y paneles de clic para editar. Hay un beneficio implícito de accesibilidad en todo esto: dado que el foco sigue el orden de campos del propio documento, la ruta que programe para la tecla Tab es la misma que anuncia un lector de pantalla, sin trabajo adicional

Mostrar nombres de campos en lugar de números de índice requiere una propiedad adicional. FormFieldInfo[] devuelve un registro TPdfFormFieldInfo por índice, que contiene el nombre del campo, el tipo, el tamaño de fuente, el estado de selección, el valor de exportación y la pertenencia a grupo, que es lo que debería mostrar una lista de navegación ("Campo 4 de 17: InvoiceDate" en lugar de "4"). Los grupos de opciones (radio groups) son el caso que requiere un archivo de prueba específico. Varios controles pueden compartir un mismo nombre de campo, por lo que una lista generada directamente a partir de controles mostrará el mismo grupo varias veces, confundiendo a los usuarios

Por qué los valores completados aparecen vacíos y la llamada que lo soluciona

La otra queja que llega a los departamentos de soporte es más preocupante que un Tabulador defectuoso: un formulario se completa mediante código, el cliente lo abre en Acrobat y todos los campos se muestran vacíos. Al hacer clic en un campo, su valor aparece de inmediato. Los datos están en el archivo todo el tiempo. Lo que falta es la representación visual de los datos, y vale la pena comprender el motivo de esto ya que explica toda una familia de fallas

Un campo de texto AcroForm almacena su valor en la entrada /V del diccionario del campo (ISO 32000-1 §12.7.3.3). Lo que un visor dibuja en realidad es algo distinto: el flujo de apariencia del control bajo /AP (§12.5.5), un pequeño fragmento de contenido renderizado de antemano. Si escribe en /V y no altera /AP, ambos se desincronizan. El valor existe, pero su versión renderizada está desactualizada o ausente. Acrobat reconstruye la apariencia de un campo cuando este recibe el foco, lo cual explica por qué los valores aparecen solo al hacer clic. La antigua bandera NeedAppearances flag, que solicitaba a los visores regenerar las apariencias, nunca funcionó de manera uniforme y quedó obsoleta en PDF 2.0, y los servidores de impresión y generadores de miniaturas la ignoran por completo. Dibujan /AP y nada más, por lo que si /AP está vacío, imprimen un cuadro en blanco

Asignar un valor a través de FormField[i] escribe únicamente en /V. Por eso completar un formulario es una secuencia de tres pasos, y el paso que se suele omitir 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

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances es la solución definitiva. Reconstruye el flujo de apariencia de cada control a partir de los valores actuales, fuentes y justificación (quadding), de modo que un visor que nunca ejecuta un evento de foco (como un servidor de impresión o un generador de miniaturas) dibuje de todos modos el estado completado. Llámelo una vez después del lote de asignaciones, no por cada campo. La generación de apariencia realiza un trabajo de diseño real, y las llamadas por campo multiplican ese esfuerzo innecesariamente en un formulario grande

La regeneración de apariencias es también el momento en que se aplican las fuentes y la alineación, lo cual genera una sorpresa adicional. El nuevo flujo distribuye cada valor dentro del rectángulo del control utilizando la fuente, el tamaño y la alineación del campo. Un valor que encaja bien en su formulario de prueba puede recortarse o reducirse en la copia del cliente donde el mismo campo sea más estrecho. Los campos de tamaño automático (tamaño de fuente cero) reducen el texto para que quepa; los de tamaño fijo simplemente lo recortan. Ambas opciones son válidas, y la única forma real de saber cuál aplica un formulario determinado es observar el resultado regenerado en lugar de la cadena de texto que escribió. Cuando se reporta texto recortado en el borde de un cuadro, esta es casi siempre la razón

Considere la verificación como parte del proceso final, no como algo secundario. Abra el archivo guardado en Acrobat y confirme que los valores sean visibles antes de interactuar con cualquier campo. Luego imprímalo en PDF o en una imagen desde un visor diferente (uno que ignore por completo la lógica del formulario) y confirme que los valores se mantengan también en esa ruta. Entre ambos, estos dos controles detectan cualquier 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 una serie de casos límite que los archivos de los clientes no tienen. Cuatro de ellos explican la mayoría de los reportes del tipo "funcionaba en mi computadora":

  • Valores de exportación de casillas de verificación. El estado activo no siempre es Yes. Un formulario puede definir su propio valor de exportación, y escribir la cadena incorrecta deja la casilla visualmente desmarcada mientras su código asume que se ha activado. Lea el valor de exportación de FormFieldInfo[] en lugar de asumir uno
  • Grupos de opciones con nombre compartido. Un campo, varios controles. El valor asignado define qué control se muestra como seleccionado, por lo que el código de interfaz que asume que un nombre corresponde a un solo rectángulo termina dibujando el anillo de enfoque en el botón incorrecto
  • Campos calculados. Los totales gestionados por JavaScript del documento se actualizan en respuesta a eventos de campo. Un llenado por código que omita esos eventos debe activar la recalculación o sobrescribir los campos calculados directamente. Un formulario donde los elementos de línea y el total no coincidan es peor que cualquiera de las dos soluciones
  • Campos obligatorios ocultos. Los formularios condicionales ocultan campos que siguen marcados como obligatorios. Decida de antemano si su validación respeta la visibilidad o la marca de obligatoriedad directa, y registre esa decisión en algún lugar accesible para soporte

Una distinción importante antes de que cause problemas: generar apariencias no es lo mismo que acoplar (flattening). GenerateFormAppearances hace que los valores sean visibles en todas partes mientras mantiene los campos editables. El acoplamiento integra la apariencia en el contenido estático de la página y elimina la interactividad de forma definitiva, lo cual es adecuado para una copia de archivo y erróneo para un formulario que otra persona debe seguir completando. Si FormType reporta ftXfaFull en lugar de ftAcroForm, ninguna de las funciones de edición descritas aquí se aplicará correctamente, ya que el documento se renderiza a partir de su propia plantilla XML; detecte ese caso e infórmelo al usuario en lugar de dejar que descubra ese límite por su cuenta

El subsistema de llenado de formularios, el recorrido de foco y la generación de apariencia mostrados aquí forman parte del PDFium Component para Delphi, C++Builder y Lazarus/FPC. Si su visor también maneja anotaciones de revisión junto con los datos del formulario, el artículo sobre revisión de anotaciones cubre ese modelo complementario