Artículo técnico

Por qué las ediciones de campos XFA desaparecen al guardar en PDFium para Delphi

TPdf.SetFocusedFormFieldText en el componente PDFium escribe en el búfer de edición vivo del campo de formulario actualmente enfocado, y para un formulario XFA ese búfer nunca llega al paquete datasets que se serializa a disco —así que un valor que un usuario escribe, y que su código confirma que fue aceptado, desaparece silenciosamente la próxima vez que se abre el archivo. Los campos AcroForm no tienen este problema: la misma llamada confirma en la entrada /V del campo en el momento en que el foco se mueve. Un usuario que llena un formulario de admisión XFA, guarda, y vuelve a abrir para encontrar el campo de monto en blanco otra vez no se está topando con un glitch de renderizado —se está topando con el límite de lo que el propio motor PDFium expone para escribir datos de formulario

Esta es una pregunta más estrecha que detectar un formulario XFA en primer lugar, o hacer que su JavaScript se ejecute: no "¿PDFium soporta XFA?" ni "¿cómo ejecuto scripts de AcroForm?" sino específicamente qué le pasa a un valor después de que SetFocusedFormFieldText reporta éxito. La versión corta es que AcroForm y XFA no son dos dialectos del mismo modelo de formulario en lo que respecta a la ruta de escritura de PDFium —son dos modelos de formulario con dos relaciones completamente distintas entre lo que un usuario escribe y lo que realmente captura un guardado, y confundir los dos es lo que convierte una llamada de API de una línea en un ticket de soporte tres semanas después de que un despliegue piloto de un cliente sale a producción. El artículo sobre JavaScript de AcroForm muestra la llamada de una línea y plantea el resultado AcroForm-versus-XFA en un comentario de código; este se queda en esa misma API y recorre la ruta de escritura interna, la prueba del paquete de datasets de que la escritura XFA nunca aterriza, por qué la brecha está en el propio PDFium en lugar de en el binding de Delphi, y una solución alternativa de parchear-su-propio-XML para documentos que necesitan que la edición sobreviva a un guardado

¿Cómo escribe SetFocusedFormFieldText un valor de campo?

TPdf.SetFocusedFormFieldText funciona simulando una edición a nivel de pulsación de tecla, no metiendo un valor directamente en el modelo de documento. Internamente llama a FORM_SelectAllText para seleccionar el contenido actual del campo enfocado, y luego a FORM_ReplaceSelection para sobrescribir la selección con la cadena nueva —las mismas dos operaciones que dispararía un seleccionar-todo-y-escribir manejado por teclado. Como la escritura pasa por la ruta de edición de texto interactiva de PDFium en lugar de rodearla, cualquier script de tecla presionada, formato, o cálculo vinculado al campo se dispara exactamente como lo haría para un humano escribiendo, que es lo que hace útil la API para el llenado programático de formularios en un visor que mantiene JavaScript activo. La contraparte de lectura es FocusedFormFieldText, respaldada por FORM_GetFocusedText, y refleja el mismo búfer vivo que SetFocusedFormFieldText acaba de escribir

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

¿Por qué AcroForm conserva el valor y XFA lo pierde?

Los campos de texto y combo de AcroForm persisten porque el propio entorno de relleno de formulario de PDFium confirma el búfer de edición por usted: en el instante en que el campo pierde el foco, el búfer se escribe en la entrada /V del campo, la misma clave que consulta cada lector de PDF conforme para conocer el valor almacenado de un campo. TPdf.ClearFormFieldFocus —que llama a FORM_ForceToKillFocus por debajo— fuerza esa confirmación a demanda, así que el código que establece un valor programáticamente no tiene que esperar un clic de mouse real en algún otro lugar de la interfaz. Guarde inmediatamente después, y el texto nuevo es parte del grafo de objetos del documento antes de que TPdf.SaveAs siquiera se ejecute, porque /V es una entrada real en un diccionario de campo real, no algo atornillado después

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

¿Dónde vive realmente una edición de campo XFA?

Los campos XFA no tienen ese cableado. El texto que escribe un usuario aterriza en un búfer CPWL_Edit que pertenece a la capa de renderizado e interacción XFA de PDFium, y esa capa no tiene ninguna ruta de código que copie el búfer de vuelta al paquete datasets almacenado en el PDF. TPdf.GetXfaDatasets hace visible la brecha: llámelo antes y después de una edición en un campo XFA y los bytes que devuelve son idénticos, porque el método lee el paquete original con el que se abrió el documento, nunca el estado vivo del widget que acaba de editar. Nada de eso es un bug de caché o un problema de sincronización de actualización —el paquete datasets en disco y el búfer de edición en memoria son simplemente dos piezas de estado distintas que la API pública de PDFium nunca conecta

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

¿Es esto un bug del componente PDFium o una limitación de PDFium?

La pieza faltante está en el propio PDFium, no en el binding de Delphi encima de él. La API pública de PDFium no tiene ningún FPDF_SetXFAPacket para inyectar un paquete actualizado y ningún FPDF_SaveAsXFA para pedirle al motor XFA que serialice su DOM actual de vuelta al XML de datasets antes de un guardado. FPDF_SaveAsCopy —la exportación que respalda a TPdf.SaveAs— escribe el grafo de objetos del documento que PDFium ya tiene; no tiene ningún gancho para pedirle al motor XFA que vuelque primero su estado vivo, porque ese gancho no existe aguas arriba. El componente PDFium no puede agregar una reconciliación que el propio PDFium nunca implementó, y enviar un serializador de DOM a XML casero que adivine el estado XFA interno de PDFium sería peor que la brecha honesta: se vería como si funcionara hasta que la siguiente versión de PDFium cambie algo que nadie fuera del proyecto puede ver

Este límite salió a la superficie durante la misma auditoría de v2.13.2 que construyó SetFocusedFormFieldText en primer lugar. FORM_ReplaceSelection había estado vinculado en la tabla de importación de la DLL durante versiones sin nunca ser llamado desde código Pascal, y agregar la ruta de escritura que finalmente lo usó es lo que hizo la brecha de persistencia lo bastante concreta como para documentarla en lugar de teórica. La misma ronda de auditoría descubrió una brecha no relacionada pero de espíritu similar: el JavaScript de AcroForm había estado silenciosamente deshabilitado desde v2.13.0 porque la plataforma JS solo estaba conectada dentro de la rama de inicialización XFA, así que los documentos AcroForm ordinarios con app.alert o campos calculados nunca obtenían ningún motor de script en absoluto. Ese sí era corregible —extender la plataforma JS a cada documento sin importar XFA— y se incorporó en la misma versión; la brecha de persistencia cubierta aquí no era corregible, por las razones de arriba. La corrección de JavaScript y los eventos de veto del host alrededor de ella se cubren en ejecutar JavaScript de AcroForm con el componente PDFium

¿Qué debería hacer al respecto en Delphi?

Para documentos AcroForm, la corrección no es más que un buen hábito: llame a ClearFormFieldFocus (o de otra manera mueva el foco) antes de SaveAs cada vez que se estableció un valor programáticamente, en lugar de asumir que una interacción de interfaz posterior disparará la confirmación por usted. Para un documento que podría ser AcroForm o XFA —que es el caso común en un visor de propósito general— compruebe FormType o el booleano XFA antes de prometerle a quien llama que un guardado se mantendrá, y lea detectar formularios XFA y extraer paquetes XFA para el conjunto completo de sondeos, incluido el caso XFAF donde el contenido XFA se superpone sobre widgets AcroForm por lo demás ordinarios que sí respetan /V

Para un formulario XFA dinámico genuino donde los valores editados tienen que sobrevivir a un guardado, el búfer de edición interactivo no es en absoluto la herramienta correcta. La ruta duradera es tratar GetXfaDatasets como su línea base, no como su resultado: léalo una vez cuando se abre el documento, mantenga su propio registro de lo que el usuario cambió campo por campo —exactamente los valores que su interfaz ya tiene, ya que PDFium no se los devolverá después del hecho— parchee eso en el XML de línea base usted mismo, y controle su propia salida. Una escritura que pasa por XML que su propio código controla sobrevive a un guardado que un búfer CPWL_Edit nunca podría

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

Detectar la brecha antes que un cliente

TPdf.SaveAs devuelve True sin importar si un valor de campo XFA sobrevivió, porque desde el punto de vista de PDFium el guardado genuinamente tuvo éxito —escribió cada byte que se le pidió escribir. Eso hace que este sea exactamente el tipo de defecto que se escapa de una prueba de humo y llega a un cliente: nada lanza una excepción, nada registra, el archivo abre bien, solo el valor específico está mal. Una prueba de ida y vuelta que realmente reabra el archivo guardado y compare el valor del campo —o compare GetXfaDatasets antes y después, según el ejemplo anterior— pertenece a la suite de regresión de cualquier visor que permita a los usuarios editar contenido XFA, no solo a las rutas AcroForm que resultan funcionar por defecto

Nada de esto es tanto un defecto que reportar contra el componente PDFium como un límite en torno al cual diseñar: SetFocusedFormFieldText hace precisamente lo que dice su nombre para ambos modelos de formulario, y la diferencia en el resultado se rastrea limpiamente hasta a qué conecta cada uno, AcroForm y XFA, ese búfer del lado de PDFium. La API, las primitivas de foco y guardado, y los lectores de paquete referenciados aquí son parte del componente PDFium para Delphi y C++Builder