Cuando un formulario XFA dinámico en un visor Delphi agrega o quita páginas, PDFium Component reporta el total nuevo por medio de TPdf.PageCount y TPdf.OnXfaPageCountChanged desde v3.126.1, porque el evento de página nativo carga un delta de agregadas/quitadas y no un total. Las librerías Windows V8 de v3.126.1 también mueven las áreas de hit de input junto con los fields reubicados, y v3.126.2 recarga los page handles rancios después de que el callback de layout retorna. El reporte de bug que inició esto fue un formulario de reembolso de gastos: clic en Add Row dos veces, el formulario crece a dos páginas, y el indicador de página orgullosamente dice 1 de 1. Teclee en un field que se mudó a la página 2 y las teclas caen en algún lugar invisible. Nada de esto apareció con los formularios de muestra de longitud fija con los que todos prueban primero, y las razones valen la pena conocerlas si embebe un visor de formularios
¿Qué pasa cuando un formulario XFA dinámico se repagina?
Un formulario XFA dinámico no tiene lista de páginas fija, así que su conteo de páginas es una salida del layout y puede cambiar cada vez que el usuario edita datos. XFA 3.3 describe el formulario como un árbol de subforms; un subform repetitivo lo controla un instanceManager, y un script como _Row.addInstance() clona una fila más. El procesador de layout entonces vuelve a fluir el contenido dentro de las áreas de página, lo que puede agregar una página, quitar una página o empujar fields existentes a otra página. ISO 32000-1 §12.7.8 solo define cómo viajan los paquetes XFA dentro del PDF; todo lo que pasa después pertenece al engine XFA, que en PDFium Component es el layout XFA del propio PDFium corriendo en el proceso anfitrión. Un visor Delphi por lo tanto lidia con un documento cuyo conteo de páginas, tamaños de página y posiciones de widgets son todos estado vivo. Tres cosas salen mal cuando el anfitrión asume lo contrario:
- El conteo de páginas que el anfitrión guarda en caché para navegación, rangos de scroll y spinners de página se pone rancio, o peor, se actualiza con el número equivocado
- Los fields que se reubican muestran su borde en la posición nueva mientras el editor y el área de hit del mouse quedan en las coordenadas viejas
- El visor conserva un page handle que el layout ya reemplazó, así que clics y pintadas van a una página que ya no existe en ese formulario
Persistir las ediciones de filas entre guardado y reapertura es un problema aparte con reglas propias; este artículo se queda con lo que pasa en runtime dentro del visor
¿Qué runtime de PDFium necesita el XFA dinámico?
El XFA dinámico en PDFium Component requiere el build V8/XFA de la librería nativa, seleccionado por la variable global EnableV8Engine de la unidad PDFium antes de que cargue el primer documento. El proceso se compromete a una DLL la primera vez que cualquier TPdf carga la librería, y un build PDFium plano no puede correr el engine XFA para nada. Cuando un documento se abre, TPdf sí echa un vistazo al archivo buscando marcadores XFA y cambia al build V8 automáticamente, pero solo si ninguna librería plana se ha cargado todavía en ese proceso. Cuando el compromiso ya fue por el camino equivocado, TPdf.OnXfaRuntimeMissing se dispara una vez para que el anfitrión le diga al usuario que reinicie. Fijar la bandera explícitamente al arranque elimina las conjeturas. La estructura de callbacks FPDF_FORMFILLINFO que carga los eventos XFA también debe coincidir con la DLL; el trasfondo está en FPDF_FORMFILLINFO versión 2 y el ABI de callbacks XFA, y detectar formularios XFA y leer sus paquetes cubre cómo distinguir los tipos de formulario antes de abrir un visor
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Decida antes de que el primer TPdf cargue la librería nativa:
// el proceso no puede pasar de pdfium.dll a pdfium.v8.dll después
EnableV8Engine := True;
FPdf := TPdf.Create(nil);
FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
FPdf.FileName := 'C:\Forms\expense-claim.pdf';
FPdf.Active := True;
PdfView1.Pdf := FPdf;
PdfView1.OnPageChange := PdfViewPageChange;
PdfView1.Active := True;
UpdatePageRange(FPdf.PageCount);
end;
procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
StatusBar1.SimpleText :=
'This XFA form needs the V8 runtime; restart the application to enable it';
end;
¿Por qué PageCount reportaba 1 para un formulario de dos páginas?
Antes de v3.126.1, PDFium Component guardaba el argumento page_count del evento de página nativo como el total del documento, y ese argumento en realidad es la diferencia absoluta entre los conteos de páginas nuevo y viejo. PDFium dispara FFI_PageEvent cuando una pasada de layout termina, con un tipo de evento de página agregada o página quitada; internamente primero actualiza su conteo de páginas guardado y luego pasa abs(new - old). En el layout inicial el conteo viejo es cero, así que el delta iguala al total, y una muestra estática de tres páginas reporta tres páginas como se espera. Eso es exactamente por qué los formularios de prueba de longitud fija jamás expusieron el bug. La primera vez que un formulario dinámico crece de una página a dos, el delta es 1, y el wrapper ponía tanto TPdf.PageCount como el parámetro NewCount de OnXfaPageCountChanged en 1. Quitar una fila de un formulario de tres páginas producía el mismo tipo de disparate en la otra dirección
Acumular el delta sobre el valor anterior tampoco es una reparación segura. El orden de los callbacks de inicialización y layout significa que el wrapper no siempre puede fiarse de su conteo anterior como línea base, así que una suma acumulada puede desviarse. Desde v3.126.1, el callback ignora el argumento como conteo y llama a FPDF_GetPageCount sobre el documento, que lee el total del layout que acaba de completarse. Después limpia las escenas de página en caché, guarda ese total como el override de conteo de páginas XFA detrás de TPdf.PageCount, y solo después dispara OnXfaPageCountChanged. Para cuando corre su handler, NewCount y FPdf.PageCount concuerdan
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount es el total del layout completado, jamás un delta.
// Esto corre dentro del callback de layout de PDFium: actualice solo el
// estado de UI del host, no cierre el documento ni recargue páginas aquí
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Se dispara tras cada recarga de página, incluido el refresh XFA diferido
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
El evento solo se dispara para formularios Full XFA cuyo layout cambia en runtime. Los documentos Static XFA y AcroForm jamás lo disparan, así que un visor que maneje ambos puede dejar el mismo handler asignado. Dejarlo sin asignar también es seguro; el override detrás de TPdf.PageCount se aplica de todos modos, y el evento existe para que el anfitrión refresque lo que tenga en caché
¿Por qué el cuadro de input se queda en la página vieja cuando un field se mueve?
El borde se movió y el editor no porque el notificador XFA nativo comparaba un rectángulo consigo mismo. Cuando el layout cambia la geometría de un widget ya cargado, se supone que PDFium nota el rectángulo nuevo y llama a PerformLayout sobre el widget, lo cual reposiciona el editor de texto y su área de hit. El chequeo comparaba GetWidgetRect() contra RecacheWidgetRect(). Ambas funciones devuelven una referencia const al mismo miembro, y el recache sobrescribe ese miembro en el sitio, así que la comparación siempre veía dos valores idénticos y los widgets cargados se saltaban su relayout
El síntoma asomó cuando una prueba cambió la altura de un subform de modo que fields existentes cruzaran a la página siguiente. En ambas arquitecturas V8, el borde del field se dibujaba en su posición nueva mientras el texto tecleado y el área de hit del mouse se quedaban en la coordenada Y anterior. Un relayout explícito no lo arreglaba, y recargar la página tampoco, porque el widget seguía creyendo que su geometría estaba al día. Las librerías Windows V8 que vienen con v3.126.1 copian el rectángulo viejo por valor antes de recachear y comparan esa copia, así que los widgets movidos hacen relayout y el valor editado aparece exactamente donde está el borde. Esta es una corrección nativa: viaja con las DLLs, así que actualizar las unidades Pascal manteniendo un pdfium.v8.dll más viejo deja las áreas de hit mal ubicadas en su sitio. El chequeo de regresión que lo impulsó primero edita una fila sobreviviente a un valor no por defecto y después exige ese valor en la ubicación nueva del field, porque una fila reconstruida con valores por defecto si no parecería un pase
¿Cómo recarga TPdfView páginas sin arrancarle un handle a PDFium por debajo?
Desde v3.126.2, TPdfView difiere la recarga de página que sigue a un cambio de layout XFA hasta que la pila de llamadas nativa se ha desenrollado. El evento de página normalmente se dispara mientras PDFium todavía procesa input: el usuario hizo clic en un botón Add Row, el clic corrió un script, el script cambió el conteo de instancias, y el layout terminó dentro de esa misma llamada nativa. Cerrar y reabrir el page handle en ese momento liberaría un objeto que quien llamó sigue usando. Antes de v3.126.2, el visor solo se invalidaba a sí mismo, así que el page handle mostrado podía seguir apuntando a estado pre-layout, y si el usuario estaba en la última página cuando desapareció, el número de página seleccionado quedaba fuera de rango
El refresh diferido funciona en unos pocos pasos pequeños, y explican el comportamiento que usted ve desde el anfitrión:
- El callback del evento de página marca la vista como pendiente de un refresh de layout XFA y postea un mensaje de ventana privado; los eventos repetidos antes de que llegue el mensaje se fusionan en un solo refresh
- Una vista sin window handle todavía conserva la bandera pendiente y postea el mensaje desde
CreateWnd, mientras que cambiar de documento, desactivar la vista o destruirla limpia la bandera - Cuando llega el mensaje, la vista limpia la selección de texto, el resaltado de búsqueda y el índice del field con foco, porque los tres se referían al layout viejo
- La página seleccionada se acota al
PageCountnuevo; un número de página cambiado pasa por el cambio de página normal, si no se recarga la página actual, y el modo de ajuste se aplica de nuevo - Si el layout no deja páginas para nada, la vista descarga su page handle viejo en vez de pintar una página que ya no existe
La misma restricción aplica a su propio código. OnXfaPageCountChanged corre dentro de ese callback de layout nativo, así que trátelo como una notificación: actualice ahí etiquetas, rangos de spinners y estado de la barra de herramientas, y ponga en cola cualquier cosa más pesada, como cerrar el documento o abrir otro, con un mensaje posteado para que corra después de que el callback retorne. TPdfView.OnPageChange después le dice cuándo la vista realmente recargó la página, y leer PdfView1.PageNumber en ese punto le da el valor acotado. El recorrido con la tecla Tab y los chequeos de FormType que un visor de formularios corre al abrir están cubiertos en navegación de form fields PDF con PDFium Component
¿Por qué hacer clic en un field Full XFA lanza "Cannot open text page"?
Las páginas Full XFA no tienen página de texto PDF, y antes de v3.126.2 la selección de texto y la detección de enlaces por defecto del visor intentaban cargar una de todos modos. Con TPdfView.AllowUserTextSelection en su valor por defecto True, el hover le pedía a la capa de texto un carácter bajo el mouse, y un clic de mouse-up corría una sonda automática de URL sobre el texto de la página. En una página Full XFA la página de texto no se puede abrir, así que un clic corriente dentro de un field podía terminar en una excepción Cannot open text page. Desde v3.126.2, ambas rutas internas no devuelven resultado cuando TPdf.FormType es ftXfaFull y el runtime XFA está disponible, así que los ajustes por defecto funcionan y el input de fields sigue disponible
Apagar AllowUserTextSelection para documentos Full XFA sigue siendo una decisión de UI razonable, porque no hay texto de página que seleccionar y los gestos de arrastre no deberían arrancar un modo de selección. No es un sustituto de actualizar, eso sí: en versiones anteriores la sonda de URL al hacer clic no dependía de esa propiedad, así que un visor podía toparse con la misma excepción con la selección deshabilitada
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType lee el documento abierto, así que llame esto después de FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// No existe capa de texto PDF en páginas Full XFA; los fields siguen editables
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Teclear necesitó su propia reparación en v3.126.2. El editor de texto XFA nativo no reemplaza una selección cuando recibe un carácter: FORM_OnChar inserta en el caret, y Backspace borra un solo carácter, así que seleccionar un valor y teclear encima producía texto viejo y nuevo lado a lado. PDFium Component ahora recuerda que el clic cayó sobre un text field XFA y enruta los caracteres tecleados, Backspace y Delete por FORM_ReplaceSelection siempre que exista una selección y el documento conceda permiso de llenar formularios o de modificar. Si un field XFA de solo lectura puede cambiar lo sigue decidiendo el editor nativo, así que un field marcado de solo lectura en el formulario conserva su valor incluso en un documento que por lo demás permite llenar. Poner TPdfView.AllowFormEvents en False también detiene este enrutado de teclado, lo que mantiene un visor de solo lectura como de solo lectura
Referencia rápida: XFA dinámico en un visor Delphi
| Síntoma | Causa | Corregido en |
|---|---|---|
| El conteo de páginas muestra 1 después de que el formulario crece a dos páginas | El evento de página nativo pasa un delta de agregadas/quitadas, no un total | v3.126.1 (wrapper) |
| El borde del field se mueve, el texto tecleado y el área de hit se quedan atrás | El widget cargado se saltaba el relayout tras una autocomparación | v3.126.1 (librerías Windows V8) |
| El visor pinta o enruta input a estado de página pre-layout | El page handle no se recargó tras la repaginación | v3.126.2 (refresh diferido) |
| Clic dentro de un field lanza Cannot open text page | Selección de texto y sonda de URL en páginas sin capa de texto | v3.126.2 |
| Teclear sobre un valor seleccionado agrega en vez de reemplazar | El editor XFA nativo inserta en el caret | v3.126.2 |
- Ponga
EnableV8EngineenTrueantes de que cargue cualquier documento, y manejeOnXfaRuntimeMissingpara el caso en que la librería plana se cargó primero - Lea el total de
TPdf.PageCounto del parámetroNewCountdeOnXfaPageCountChanged; jamás sume o reste conteos de páginas por su cuenta - Mantenga ligero el handler de
OnXfaPageCountChanged, porque corre dentro del callback de layout nativo - Sincronice el indicador de página actual en
TPdfView.OnPageChange, que se dispara después de que la recarga diferida acota el número de página - Despliegue las DLLs Windows V8 de v3.126.1 o posteriores junto con las unidades; la corrección de relayout de widgets vive en código nativo
- Pruebe con un formulario que realmente cambie su conteo de páginas y mueva un field editado a través de un salto de página, porque las muestras de longitud fija esconden cada bug de esta lista
El XFA dinámico convierte el conteo de páginas y la geometría de fields en valores vivos, y un visor solo se mantiene correcto cuando los toma del layout completado y recarga páginas en un momento seguro. PDFium Component maneja ambas cosas dentro de TPdf y TPdfView, así que al anfitrión solo le queda escuchar. Detalles y descargas están en la página de producto de PDFium Component para Delphi