TPdf.SetFocusedFormFieldText en el componente PDFium escribe en el búfer de edición en 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 vuestro código confirma que se aceptó, desaparece en silencio la próxima vez que se abra el archivo. Los campos AcroForm no tienen este problema: la misma llamada confirma en la entrada /V del campo en el instante en que el foco se aleja. Un usuario que rellena un formulario de admisión XFA, guarda, y vuelve a abrir para encontrar el campo de importe en blanco de nuevo no se está topando con un fallo 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 conseguir que se ejecute su JavaScript: no «¿admite PDFium XFA?» ni «¿cómo ejecuto scripts AcroForm?» sino específicamente qué le ocurre a un valor después de que SetFocusedFormFieldText reporte é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 vía de escritura de PDFium, son dos modelos de formulario con dos relaciones completamente distintas entre lo que un usuario escribe y lo que un guardado realmente captura, 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 se ponga en marcha el proyecto piloto de un cliente. El artículo sobre JavaScript AcroForm muestra la llamada de una línea y enuncia el resultado AcroForm-frente-a-XFA en un comentario de código; este se queda en esa misma API y recorre la vía de escritura interna, la prueba mediante el paquete datasets de que la escritura XFA nunca llega a aterrizar, por qué la brecha reside en el propio PDFium y no en el binding de Delphi, y una solución alternativa de parchear vuestro 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 introduciendo un valor directamente en el modelo de documento. Internamente llama a FORM_SelectAllText para seleccionar el contenido actual del campo enfocado, y después a FORM_ReplaceSelection para sobrescribir la selección con la cadena nueva, las mismas dos operaciones que dispararía un seleccionar-todo-y-escribir dirigido por teclado. Como la escritura pasa a través de la vía de edición de texto interactiva de PDFium en lugar de esquivarla, cualquier script de pulsación de tecla, formato o cálculo vinculado al campo se dispara exactamente igual que lo haría para un humano escribiendo, que es lo que hace útil la API para el rellenado programático de formularios en un visor que mantiene JavaScript activo. La contrapartida del lado de lectura es FocusedFormFieldText, respaldada por FORM_GetFocusedText, y refleja el mismo búfer en 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é conserva AcroForm el valor y XFA lo pierde?
Los campos de texto y combo de AcroForm persisten porque el propio entorno de rellenado de formularios de PDFium confirma el búfer de edición por vosotros: 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 cualquier lector 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 a un clic de ratón real en otra parte de la interfaz. Guardad inmediatamente después, y el texto nuevo forma parte del grafo de objetos del documento antes de que se ejecute TPdf.SaveAs, porque /V es una entrada real en un diccionario de campo real, no algo añadido 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 vía de código que copie el búfer de vuelta al paquete datasets almacenado en el PDF. TPdf.GetXfaDatasets hace visible la brecha: llamadlo 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 en vivo del widget que acabáis de editar. Nada de eso es un fallo de caché ni un problema de sincronización de actualización, el paquete datasets en disco y el búfer de edición en memoria son sencillamente 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 fallo del componente PDFium o una limitación de PDFium?
La pieza que falta reside en el propio PDFium, no en el binding de Delphi que hay encima. La API pública de PDFium no tiene ningún FPDF_SetXFAPacket para inyectar un paquete actualizado ni 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 TPdf.SaveAs, escribe el grafo de objetos del documento que ya tiene PDFium; no tiene ningún gancho para pedirle al motor XFA que vuelque primero su estado en vivo, porque ese gancho no existe aguas arriba. El componente PDFium no puede añadir una reconciliación que el propio PDFium nunca implementó, y distribuir un serializador casero de DOM a XML que adivinara el estado XFA interno de PDFium sería peor que la brecha honesta: parecería funcionar hasta que la siguiente versión de PDFium cambiara algo que nadie fuera del proyecto puede ver
Este límite salió a la luz durante la misma auditoría v2.13.2 que construyó SetFocusedFormFieldText en primer lugar. FORM_ReplaceSelection llevaba versiones enlazado en la tabla de importación de la DLL sin que jamás se llamara desde código Pascal, y añadir la vía de escritura que finalmente lo usó es lo que hizo que la brecha de persistencia fuera lo bastante concreta como para documentarla en lugar de teórica. La misma ronda de auditoría encontró una brecha sin relación pero emparentada en espíritu: el JavaScript de AcroForm había quedado silenciosamente deshabilitado desde la 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 scripts en absoluto. Ese sí era solucionable, extendiendo la plataforma JS a cada documento independientemente de XFA, y se distribuyó en la misma versión; la brecha de persistencia cubierta aquí no era solucionable, por las razones de arriba. La solución de JavaScript y los eventos de veto del anfitrión que la rodean se cubren en la ejecución de JavaScript AcroForm con el componente PDFium
¿Qué deberíais hacer al respecto en Delphi?
Para documentos AcroForm, la solución no es más que un buen hábito: llamad a ClearFormFieldFocus (o alejad el foco de otro modo) antes de SaveAs siempre que se haya establecido un valor programáticamente, en lugar de asumir que una interacción de interfaz posterior os disparará la confirmación por vosotros. Para un documento que podría ser AcroForm o XFA, que es el caso habitual en un visor de propósito general, comprobad FormType o el booleano XFA antes de prometerle a quien llama que un guardado se mantendrá, y leed la detección de formularios XFA y la extracción de paquetes XFA para el conjunto completo de sondas, 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 vía duradera es tratar GetXfaDatasets como vuestra línea base, no como vuestro resultado: leedla una vez al abrir el documento, manteneded vuestro propio registro de lo que el usuario cambió campo por campo, exactamente los valores que vuestra interfaz ya tiene, ya que PDFium no os los va a devolver después del hecho, parcheadlos vosotros mismos en el XML de línea base, y gobernad vuestra propia salida. Una escritura que pasa por XML que controla vuestro propio código 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 de que lo haga un cliente
TPdf.SaveAs devuelve True tanto si un valor de campo XFA sobrevivió como si no, porque desde el punto de vista de PDFium el guardado tuvo éxito genuinamente, escribió cada byte que se le pidió que escribiera. Eso convierte esto exactamente en el tipo de defecto que se cuela por delante de una prueba de humo y llega a un cliente: nada lanza una excepción, nada registra nada, el archivo se abre bien, solo el valor concreto está mal. Una prueba de ida y vuelta que realmente vuelva a abrir el archivo guardado y compare el valor del campo, o que compare GetXfaDatasets antes y después, según el ejemplo anterior, pertenece a la batería de regresión de cualquier visor que permita a los usuarios editar contenido XFA, no solo a las vías AcroForm que funcionan 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 su nombre dice para ambos modelos de formulario, y la diferencia de resultado se rastrea con claridad 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í forman parte del componente PDFium para Delphi y C++Builder