La mayoría de las páginas PDF se rasterizan en unos pocos milisegundos y usted nunca piensa en ello. Luego, un usuario abre un dibujo de ingeniería A1, una página repleta de decenas de miles de trazos vectoriales, o un póster lleno de grupos de transparencia y máscaras suaves, y la única llamada que lo pinta toma dos o tres segundos. Si esa llamada se ejecuta en el hilo de la interfaz de usuario, la ventana deja de repintarse, la barra de título se vuelve gris y el sistema operativo ofrece eliminar la aplicación. El trabajo es legítimo. La página realmente necesita ese tiempo. El defecto es que la representación es una llamada de bloqueo indivisible sin forma de tomar aire y sin forma de detenerse
Este artículo trata exactamente de uno de esos dos problemas: cancelar una representación larga de una sola página sin congelar la interfaz de usuario. El usuario hizo clic en la página siguiente, o hizo zoom, o cerró el documento, y la representación en vuelo es ahora un trabajo desperdiciado que debería finalizar en la próxima oportunidad en lugar de ejecutarse hasta el final. Suavizar el desplazamiento y el zoom almacenando en caché lo que ya se rasterizó es un problema separado con su propio diseño, cubierto en el artículo complementario vinculado al final. Aquí la única pregunta es cómo hacer que una representación progresiva responda a una solicitud de cancelación de forma rápida y limpia
La API de representación progresiva que PDFium ya incluye
PDFium anticipó la mitad de congelación del problema. Junto con la función de un solo disparo FPDF_RenderPageBitmap, expone una variante progresiva que divide una página en fragmentos de trabajo. Usted llama a FPDF_RenderPageBitmap_Start una vez para configurar la representación en un mapa de bits de destino, y luego llama a FPDF_RenderPage_Continue repetidamente. Cada Continue rasteriza un fragmento delimitado y devuelve un estado. FPDF_RENDER_TOBECONTINUED significa que hay más por hacer, FPDF_RENDER_DONE significa que la página está terminada y FPDF_RENDER_FAILED significa que se detuvo por un error. Cuando el bucle termina, usted llama a FPDF_RenderPage_Close para liberar el estado progresivo por página. Debido a que el control regresa a su código entre fragmentos, usted puede bombear mensajes, actualizar un indicador de progreso o verificar si todavía se desea el trabajo
El mecanismo que proporciona PDFium para decidir cuándo ceder es una estructura de devolución de llamada llamada IFSDK_PAUSE. Usted se la entrega a Start y a cada Continue. Después de cada fragmento, PDFium llama a su puntero de función NeedToPauseNow, y si eso devuelve un valor distinto de cero, el Continue actual se detiene antes de tiempo y devuelve el control con FPDF_RENDER_TOBECONTINUED. La estructura también lleva un campo version, que debe establecerse en 1, y un puntero user de forma libre que PDFium nunca toca y pasa intacto. Ese puntero intacto es toda la bisagra del diseño que sigue
Reutilizar la pausa como cancelación
La intención original de NeedToPauseNow es la división del tiempo. Devuelva distinto de cero cuando se gaste su presupuesto de fotogramas, devuelva cero para continuar con la representación, y PDFium se detiene para que pueda hacer otra cosa antes de reanudar la misma representación. El Componente PDFium reutiliza esa misma señal para un verbo diferente. En lugar de responder "¿debería hacer una pausa y dejar que usted reanude?", la devolución de llamada responde "¿se ha cancelado este trabajo?". Los dos se asignan limpiamente porque lo que hace el bucle cuando ve la bandera. Una pausa genuina espera un Continue posterior; una cancelación no. Una vez que el bucle de llamada observa que el token se cancela, cierra el contexto de representación y nunca vuelve a llamar a Continue, por lo que el mismo retorno distinto de cero que PDFium lee como "detener este fragmento" se convierte, en efecto, en "detener definitivamente"
La cancelación se expresa a través de una interfaz, IPdfCancellationToken, cuya propiedad IsCancelled cambia de falso a verdadero cuando alguna otra parte del programa pide que la representación se detenga. El puente entre esa interfaz Pascal y la devolución de llamada en C de PDFium es un solo puntero. La referencia de interfaz del token se escribe en IFSDK_PAUSE.user, y una devolución de llamada estática cdecl la vuelve a leer y la consulta. Este es el problema clásico de permitir que una biblioteca C vuelva a llamar a Pascal: la devolución de llamada tiene que ser una función simple con la convención de llamada de C, no un método, porque PDFium almacena e invoca un puntero de función simple que no sabe nada sobre objetos Pascal o Self
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium reads this; .user holds the token
Token: IPdfCancellationToken; // strong ref keeps the token alive
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // non-zero: PDFium stops this chunk
end;
La devolución de llamada recupera el token al convertir pThis^.user nuevamente al tipo de interfaz y lee IsCancelled. Nada en él asigna, bloquea o detiene, lo que es importante porque PDFium lo llama en el hilo de representación después de cada fragmento y cualquier trabajo realizado aquí se suma al costo de la representación en sí. La protección contra una estructura nula o un campo user nulo significa que es seguro instalar la misma función incluso en una representación a la que nunca se le dio un token real
Mantener vivo el token en el bucle
Convertir un puntero de interfaz a través de un Pointer simple y de regreso es donde nacen los errores de ciclo de vida. Un IInterface en Delphi tiene recuento de referencias, y el recuento solo se mueve cuando el compilador puede ver que se está asignando una variable de tipo de interfaz. Almacenar el token únicamente como un puntero simple dentro de IFSDK_PAUSE.user lo ocultaría completamente del contador de referencias. Si la única otra referencia a ese token saliera del alcance mientras el bucle Continue aún se estuviera ejecutando, el objeto se liberaría por debajo de la devolución de llamada, y el siguiente fragmento desreferenciaría un puntero colgante
Es por eso que el descriptor es un registro que contiene dos cosas, no una. El campo Pause es la estructura que lee PDFium. El campo Token es una referencia real de tipo interfaz que el compilador cuenta, y no existe por otra razón que fijar el token en la memoria mientras viva el registro. El registro es una variable local en la pila de la rutina de representación, por lo que sigue siendo válido durante toda la duración del bucle y solo se derriba cuando finaliza la rutina. El puntero simple en user y la referencia contada en Token nombran el mismo objeto; uno es lo que PDFium puede leer, el otro es lo que evita que ese objeto sea recolectado
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... choose EffectiveToken ...
// Strong ref first, then publish the same object to PDFium via .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
Cerrar el contexto de representación sin importar cómo termine el bucle
Cada llamada a FPDF_RenderPageBitmap_Start asigna el estado progresivo que PDFium asocia con la página, y ese estado solo se libera mediante FPDF_RenderPage_Close. Hay tres formas de salir del bucle de manejo. La página finaliza y el último estado es FPDF_RENDER_DONE. El token se dispara y el bucle sale temprano reportando la cancelación. Algo falla y el estado es FPDF_RENDER_FAILED. Los tres deben llamar a Close, y la ruta de cancelación es la más fácil de equivocar, porque la forma natural de "ver la cancelación, salir" tiende a omitir la limpieza en su camino hacia la salida. Dejar que no se alcance Close filtra el estado por página, y un visor que permita al usuario cancelar la representación una y otra vez acumularía esa fuga en cada página abortada
La forma robusta pone el bucle y la clasificación de resultados dentro de un try y FPDF_RenderPage_Close en el finally correspondiente. El mapa de bits de destino se destruye en el mismo bloque. La cancelación puede salir del bucle a través de un Exit temprano y el finally aún se ejecuta, por lo que hay exactamente un lugar que libera el estado progresivo y no se puede eludir
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Frees the progressive state Start allocated; mandatory on every path.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
El bucle comprueba el token antes de cada Continue, así como también confía en la devolución de llamada en su interior. La devolución de llamada acorta el fragmento actual; la comprobación del bucle impide que comience el siguiente. Juntos limitan el tiempo que tarda una cancelación en surtir efecto a aproximadamente la duración de un fragmento
Tres resultados y lo que contiene el mapa de bits después de una cancelación
El punto de entrada público es TPdf.RenderPageProgressive, y devuelve un TPdfProgressiveStatus que es uno de prsDone, prsCancelled o prsFailed. Los valores reflejan las constantes FPDF_RENDER_* de PDFium en la expresión de Pascal, pero incorporan el caso de cancelación como un resultado de primera clase en lugar de un error
El punto que atrapa a la gente es lo que contiene el mapa de bits de destino después de prsCancelled. No está en blanco. PDFium se representa progresivamente en el mismo mapa de bits fragmento por fragmento, por lo que cuando una cancelación detiene el bucle, el mapa de bits contiene lo que se pintó hasta ese momento, que es una imagen parcial: algunas bandas terminadas, el resto sigue mostrando el color de relleno. Si ese resultado parcial es útil depende de la persona que llama. Un visor que está a punto de desechar el mapa de bits porque el usuario navegó a otra parte simplemente puede ignorarlo. Un visor que quiera mostrar una vista previa de bajo costo puede conservarlo. Lo que no debe hacer es asumir que prsCancelled implica un mapa de bits vacío o indefinido; implica una instantánea veraz de una representación sin terminar
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token starts un-cancelled; flip Token.IsCancelled from elsewhere
// (a UI action, a navigation event) to abort the render in flight.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // fully rendered
prsCancelled: ; // partial bitmap, usually discarded
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
El token nulo y una ruta de devolución de llamada sin ramificaciones
La cancelación es opcional. Una persona que llama que solo quiere una representación progresiva por el beneficio del bombeo de mensajes, sin intención de abortar, debería poder pasar un valor nil para el token. La forma ingenua de admitir eso es dispersar comprobaciones "si se proporcionó un token" a través de la devolución de llamada y el bucle, lo que significa una rama en cada fragmento y una devolución de llamada que tiene que manejar tanto un token real como su ausencia
La implementación evita eso sustituyendo un singleton cuando la persona que llama no pasa nada. Un token nil se intercambia por PdfNoCancellationToken, una interfaz cuyo valor IsCancelled siempre es falso. Desde ese punto, la devolución de llamada y el bucle tienen un token para consultar en todos los casos, por lo que ninguno necesita una verificación nula y ninguno necesita una ruta especial. El token de nunca cancelar simplemente responde siempre falso, la devolución de llamada siempre devuelve cero y la representación se ejecuta hasta el final exactamente como lo haría una que no se puede cancelar. El comportamiento opcional se modela como un token que nunca se dispara en lugar de como la ausencia de un token, lo que mantiene uniforme la ruta crítica
// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
La forma que surge es pequeña y vale la pena repetirla, porque es la parte reutilizable. Una biblioteca C que admite una devolución de llamada le brinda exactamente un canal para pasar el estado a esa devolución de llamada, el puntero opaco del usuario. Ponga una referencia de interfaz Pascal contada detrás de ese puntero, mantenga viva una segunda referencia real junto a la estructura para que el objeto no se pueda recolectar en medio de la llamada, y lea la interfaz nuevamente dentro de una función estática cdecl. Envuelva todo el bucle de manejo en un try y libere el contexto nativo en el finally. La misma plantilla se aplica a cualquier operación de PDFium progresiva o impulsada por devolución de llamada en la que el código de Pascal tiene que mantener el control de la vida útil mientras que C contiene un puntero
La cancelación es solo la mitad de un visor receptivo. La otra mitad es no volver a representar las páginas que ya dibujó, y mantener el zoom y el desplazamiento suaves al servir mapas de bits en caché, lo cual se cubre en nuestro artículo sobre el almacenamiento en caché de la representación y el rendimiento del zoom. Para saber cómo encaja la representación cancelable en un visor completo junto con la navegación, la selección y la búsqueda, consulte cómo crear un visor de PDF rico en funciones con el componente PDFium VCL. La representación progresiva descrita aquí se envía como parte del Componente PDFium para Delphi y Lazarus junto con las API de carga, representación y formularios que se tratan en otras partes de este blog