Artículo técnico

Navegación de campos PDF en Delphi (PDFium Component)

El reporte de error llega con una captura: "Su herramienta llenó el formulario, pero todos los campos están en blanco en Acrobat. Cuando hago clic en un campo, el valor aparece de pronto". Los datos están en el archivo — la captura incluso lo prueba — y aun así el formulario se ve vacío para todos los que lo reciben. Este es el defecto más común en trabajo programático con formularios PDF, y no es un error de ninguna biblioteca: es lo que ocurre cuando se escriben valores de campo sin regenerar las apariencias de campo. Entenderlo requiere una sección de la especificación PDF; corregirlo requiere una llamada de método. Los ejemplos siguientes usan PDFium Component, un componente VCL/LCL basado en PDFium para Delphi, C++Builder y Lazarus, pero la mecánica subyacente del formato de archivo aplica a cualquier herramienta AcroForm.

Un campo, dos representaciones: /V y /AP

Un campo de texto AcroForm almacena su valor en la entrada /V del diccionario de campo (ISO 32000-1 §12.7.3.3). Sin embargo, lo que los visores pintan realmente es el appearance stream del widget: un pequeño flujo de contenido prerenderizado almacenado bajo /AP (§12.5.5). Escriban /V sin reconstruir /AP y ambos divergen: los datos existen, la imagen de los datos no. Acrobat repinta la apariencia de un campo cuando el campo recibe foco, que es exactamente por eso que hacer clic en un campo "revela" el valor en el reporte anterior.

La salida histórica, la bandera NeedAppearances que pide a los visores regenerar apariencias por su cuenta, nunca fue respetada de manera consistente entre visores y está obsoleta en PDF 2.0 (ISO 32000-2). Las canalizaciones de impresión y los generadores de miniaturas nunca la respetaron: pintan lo que contiene /AP, que no es nada. Por lo tanto, el contrato confiable es: quien escribe el valor también reconstruye la apariencia.

La generación de apariencia también es donde las fuentes y la alineación se hacen sentir. Un flujo regenerado coloca el valor dentro del rectángulo del widget usando la fuente, el tamaño y el quadding del campo, por eso un valor que cabe en su formulario de prueba puede recortarse o reducirse en una copia más estrecha del mismo campo en manos de un cliente. Los campos con tamaño automático (tamaño de fuente cero) reducen el texto para que quepa; los campos de tamaño fijo lo recortan. Ambos son resultados legales, y la única forma de saber cuál produce un formulario dado es inspeccionar la salida regenerada en lugar del valor que escribieron: cuando un cliente reporta que el texto se corta, este párrafo suele ser toda la explicación.

Abrir un formulario: FormFill, FormType y la pregunta XFA

El acceso a campos requiere que el subsistema de llenado de formularios, controlado por la propiedad FormFill, esté habilitado antes de abrir el documento. Una vez activo, FormType indica qué tipo de formulario tienen enfrente, y la respuesta cambia el conjunto de funciones que pueden prometer:

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. AcroForm es el modelo estándar de formularios de ISO 32000 y el objetivo de cada API de este artículo; los documentos XFA incrustan su propia arquitectura XML de formularios, y prometer a clientes edición XFA completa con base en una demo rápida de AcroForm es un compromiso que lamentarán. Segundo, FormFill también inicializa JavaScript del documento, que es justo lo que quieren en un visor de captura de datos donde los scripts de cálculo mantienen totales actualizados, y justo lo que no quieren en una vista previa de archivos no confiables. El artículo sobre vista previa PDF segura cubre el lado FormFill := False de ese intercambio.

Recorrido de teclado que los usuarios pueden predecir

Los usuarios de captura de datos viven en la tecla Tab, así que el recorrido de campos debe comportarse como cualquier otro formulario que usan. La familia de APIs de foco — FocusFormField, FocusNextFormField, FocusPreviousFormField, FocusedFormFieldIndex y ClearFormFieldFocus — mueve el foco de formulario sin simular entrada de mouse:

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;

Conozcan el comportamiento en los límites: las llamadas de recorrido avanzan por el orden de tabulación de la página actual y dan vuelta sobre él; avanzar más allá del último campo vuelve al primero, y ambas funciones devuelven el nuevo índice de campo (o -1 cuando la página no tiene campos). Cruzar a la página siguiente es decisión de ustedes: detecten la vuelta comparando índices y avancen PageNumber ustedes mismos si el objetivo es recorrido de todo el documento. Combinen el recorrido con el evento OnFormFieldEnter y, en el visor, OnFormFieldFocusChange, para mantener sincronizado un panel lateral con el documento, y usen la propiedad indexada FormFieldAt cuando necesiten hit-testing: mapear una posición del mouse a un valor de campo para vistas previas de tooltip o paneles de clic para editar. Los usuarios de lectores de pantalla reciben el beneficio del recorrido sin costo: el foco se mueve por el propio orden de campos del documento, así que la ruta que construyen para Tab es también la ruta que sigue la tecnología asistiva.

Para UIs impulsadas por metadatos, la propiedad FormFieldInfo[] devuelve un registro TPdfFormFieldInfo por índice, que es como se etiquetan campos en una lista de navegación en lugar de mostrar números de índice desnudos. Los grupos de radio merecen su propio archivo de regresión aquí: varios widgets comparten un nombre de campo, así que una lista construida ingenuamente desde widgets muestra duplicados aparentes que confunden a los usuarios.

La secuencia de llenar y guardar que sobrevive a Acrobat

Todo lo anterior converge en una secuencia de tres pasos cuyo paso central es el que los equipos omiten:

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 toda la corrección para el reporte inicial. Reconstruye los appearance streams de widgets a partir de los valores, fuentes y quadding actuales, de modo que todos los visores — incluidos los que nunca ejecutan eventos de foco, como servidores de impresión y miniaturizadores — pinten el estado llenado. Llámenlo una vez después del lote de asignaciones en lugar de por campo; la generación de apariencia toca fuentes y diseño, y las llamadas por campo multiplican ese costo en formularios grandes sin beneficio.

La verificación pertenece a la definición de terminado: abran el archivo guardado en Acrobat y confirmen que los valores sean visibles sin hacer clic en ningún campo; luego impriman a PDF o imagen desde un segundo visor y confirmen que los valores sobrevivan a una canalización que ignora por completo la lógica de formularios. Esas dos revisiones juntas capturan cada variante de la divergencia /V contra /AP.

Formularios de producción que rompen implementaciones limpias

Una lista breve de configuraciones de campo que pasan pruebas de demo y fallan con archivos de clientes:

  • Valores de exportación de checkbox. El estado "on" no siempre es Yes: los formularios definen valores de exportación arbitrarios, y escribir el equivocado deja la casilla visualmente sin marcar mientras su código cree que tuvo éxito.
  • Grupos de radio con nombre compartido. Un campo, muchos widgets. La asignación de valor selecciona qué widget aparece marcado, y el código de UI por widget que asume un nombre por rectángulo dibuja el anillo de foco equivocado.
  • Campos calculados. Los totales calculados por JavaScript del documento se actualizan en eventos de campo. Un llenado programático que evita eventos debería disparar el recálculo o sobrescribir explícitamente los campos calculados; enviar un formulario donde partidas y total no coinciden es peor que cualquiera de las dos opciones.
  • Campos requeridos ocultos. Los formularios condicionales ocultan campos que siguen marcados como requeridos. Decidan si su validación respeta la visibilidad o la bandera cruda, y documenten la decisión donde soporte pueda encontrarla.

Preguntas frecuentes

¿Por qué mis valores llenados solo aparecen cuando hago clic en un campo?

Los valores se escribieron en /V, pero los appearance streams /AP nunca se regeneraron, así que los visores pintan la apariencia obsoleta y vacía hasta que un evento de foco fuerza una reconstrucción. Llamen a GenerateFormAppearances después de asignar valores y antes de SaveAs.

¿La navegación de campos funciona en formularios XFA?

Revisen primero FormType. ftAcroForm les da toda la superficie de navegación y edición descrita aquí; ftXfaFull significa que el documento se renderiza desde su propia plantilla XML y la interacción a nivel de campo es limitada. Detéctenlo y muéstrenlo en la UI en lugar de dejar que los usuarios lo descubran.

¿Aplanar es lo mismo que generar apariencias?

No. GenerateFormAppearances mantiene los campos interactivos mientras hace visibles sus valores en todas partes. El aplanado convierte la apariencia en contenido estático de página y elimina permanentemente la interactividad: correcto para salida de archivo, incorrecto para un formulario que la siguiente persona debe editar.

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