Dos quejas suelen acompañar a todo visor de PDF incrustado: los documentos grandes se vuelven a renderizar desde cero en cada inicio, y el texto se vuelve borroso en el momento en que la ventana se arrastra a un monitor de alta densidad de píxeles (DPI). losLab PDF Library resuelve ambas cuestiones en su control TPDFlibViewer: la propiedad DiskCacheFolder habilita una caché de páginas en disco entre sesiones identificada por la huella digital del documento, y la propiedad ScreenDPI, respaldada por GetDpiForWindow y un controlador de WM_DPICHANGED, le otorga al control una verdadera compatibilidad con DPI por monitor
Vale la pena tomar en serio ambos problemas porque los usuarios los notan de inmediato. Un contrato escaneado de 400 páginas que ayer tardó ocho segundos en permitir el desplazamiento vuelve a tardar dos o tres segundos hoy, a pesar de que nada en el archivo ha cambiado. Y en un escritorio con DPI mixto, un visor que se renderiza a los 96 DPI del monitor primario se ve bien allí, pero visiblemente difuminado en el panel 4K contiguo. Este artículo explica cómo TPDFlibViewer resuelve cada uno y, lo que es más útil, por qué el diseño se estructuró de esa manera. Si es nuevo en el uso del control en sí, comience con la introducción al control del visor de PDF interactivo y regrese luego para conocer la capa de rendimiento
¿Cómo almacenar en caché páginas PDF renderizadas entre sesiones en Delphi?
Asigne una carpeta a TPDFlibViewer.DiskCacheFolder y el control guardará de forma persistente los mapas de bits de las páginas renderizadas en el disco, de modo que un documento visto recientemente se vuelva a abrir al instante en lugar de renderizarse de nuevo. En el disco, la estructura es <Carpeta>\PDFlibPas-PageCache\<huella digital del documento>\p<página>_z<clave de zoom>.bin: una subcarpeta por documento y un archivo por combinación de página y zoom. Todo lo demás es automatico; no hay ninguna API de caché a la que llamar ni nada que vaciar
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// One line turns on the cross-session page cache
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
El contador DiskCacheHits informa cuántos renderizados de página evitó la caché en disco en la sesión actual, lo que permite que la mejora sea medible en lugar de anecdótica. En un documento que cerró y volvió a abrir, cada página que se desplaza a la vista sin pasar por un renderizado incrementa este valor
Por qué la clave de caché es una huella digital y no un número de versión
La huella digital del documento es un hash FNV-1a de 64 bits sobre la cadena ruta|tamaño|fecha de última escritura, con formato de dieciséis dígitos hexadecimales. Esa triple combinación representa toda la estrategia de invalidación: cuando se edita el archivo, cambian su tamaño o su fecha de escritura, el hash cambia y el visor simplemente abre una subcarpeta de caché diferente. La carpeta obsoleta nunca se vuelve a consultar y, con el tiempo, se descarta mediante la depuración LRU. No hay ningún protocolo de etiquetas de versión que mantener, ninguna lógica de comparación de marcas de tiempo que pueda desfasarse y ninguna forma de que se sirva un mapa de bits antiguo para un documento modificado
Esta es la misma idea de "identidad de contenido como clave de caché" que utilizan los sistemas de compilación, y demuestra su valor en los modos de fallo que elimina. Una caché que almacena archivos bajo el nombre simple del documento debe detectar las ediciones de forma explícita, y cada comprobación explícita es un error potencial ante casos extremos: un archivo restaurado desde una copia de seguridad con una marca de tiempo antigua, un guardado que conserva el tamaño o una ruta comparada distinguiendo mayúsculas y minúsculas en un equipo pero no en otro. Convertir mediante hash estas tres señales en el nombre de la carpeta hace que la obsolescencia sea estructuralmente imposible en lugar de requerir una comprobación procedimental
Por qué la rotación se excluye deliberadamente de la clave de disco
El nombre del archivo de caché codifica el número de página y el zoom, y nada más; el ángulo de rotación de la vista está ausente de forma intencionada. La caché en disco siempre almacena los bytes de renderizado sin rotar; cuando se carga una página de la caché mientras la vista está rotada, los píxeles se reorientan en la memoria después de la decodificación. Si la rotación fuera parte de la clave en disco, un usuario que alterne entre los cuatro ángulos de vista escribiría la misma página en el disco cuatro veces, cuadruplicando el crecimiento de la caché sin obtener información adicional, dado que la rotación es una reorganización de píxeles económica en comparación con un renderizado completo
La caché de mapas de bits en memoria sí utiliza una clave compuesta, Round(Zoom * 1000) * 4 + Rotation div 90, porque allí el mapa de bits rotado es exactamente lo que necesita el controlador de dibujo. El hilo de prebúsqueda en segundo plano sigue la misma disciplina: siempre renderiza páginas sin rotar e informa claves de zoom simples, y el hilo principal aplica la rotación al procesar los resultados (la misma regla de que "el hilo principal es propietario de la caché" explicada en renderización de páginas en paralelo y seguridad de hilos). Mantener la rotación fuera de todo lo que esté por debajo de la capa de memoria significa tener un renderizado canónico único por página y zoom en cualquier lugar donde sea costoso almacenar
¿Cómo mantiene acotada la carpeta de caché la depuración LRU?
La caché conserva hasta diez carpetas de documentos y depura las menos utilizadas recientemente cuando llegan nuevos documentos. La antigüedad de uso se rastrea mediante un archivo last-used.marker en cada carpeta de documento, cuya marca de tiempo se actualiza tanto en las escrituras como en las lecturas de la caché, de modo que un documento que simplemente vuelve a leer se mantiene activo sin necesidad de volver a escribirse. La carpeta que pertenece al documento abierto actualmente está exenta de eliminación, por lo que la depuración nunca puede eliminar páginas que estén bajo la vista activa
Cada operación de disco en la caché (cálculo de huella digital, creación de carpetas, actualización de marcadores, lectura y escritura de páginas, depuración) captura sus propias excepciones. Esa es una postura deliberada, no descuido: la caché es un acelerador que funciona bajo el principio del mejor esfuerzo, y un disco lleno, una carpeta de solo lectura o un bloqueo por antivirus deben degradar el comportamiento del visor a "renderiza como antes", nunca a "fallar al mostrar la página". El peor resultado que puede producir un error de E/S de caché es que no ocurra una coincidencia (missed hit)
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// How many render passes did the disk cache save this session?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
¿Cómo hacer que un visor de PDF sea compatible con DPI en Delphi?
TPDFlibViewer enruta cada cálculo dependiente de DPI a través de un único campo, y ese diseño con un único punto de cambio es lo que hace que el soporte por monitor sea manejable. El código de distribución, prebúsqueda y renderizado lee un único valor FScreenDPI; al 100% de zoom, un punto de PDF equivale a ScreenDPI / 72 píxeles. El configurador de la propiedad pública ScreenDPI borra la caché de mapas de bits, reconstruye la distribución, actualiza los rangos de desplazamiento e invalida la ventana, de modo que cambiar los DPI consiste en una sola asignación y todos los consumidores se actualizan de forma automática. Esta misma matemática de píxeles es importante cuando se renderiza para una impresora en lugar de un monitor, un tema analizado en vista previa de impresión y renderizado de contexto de dispositivo
La detección ocurre en una invalidación de CreateWnd en lugar de en el constructor, porque GetDpiForWindow requiere un identificador de ventana válido y el constructor se ejecuta mucho antes de que exista uno. Hasta que se activa CreateWnd, el control mantiene el valor heredado predeterminado de 96 para que la matemática de distribución sea coherente. GetDpiForWindow en sí es una API de Windows 10 1607+, por lo que el control la vincula dinámicamente con GetProcAddress y recurre a GetDeviceCaps(LOGPIXELSY) en sistemas más antiguos: la unidad sigue cargándose en cualquier lugar, y las versiones anteriores de Windows simplemente informan el DPI del sistema
¿Qué sucede cuando la ventana se mueve a un monitor 4K?
Windows envía el mensaje WM_DPICHANGED cuando una ventana compatible con DPI por monitor pasa a un monitor con un factor de escala diferente, y TPDFlibViewer lo gestiona directamente. El controlador lee los nuevos DPI desde la palabra baja de wParam, asigna el valor a través de SetScreenDPI (lo que descarta la caché de mapas de bits ahora incorrecta y vuelve a distribuir las páginas) y luego acepta el rectángulo de ventana sugerido que Windows pasa en lParam a través de SetWindowPos, lo que mantiene estable el tamaño del cliente lógico durante el movimiento. El usuario observa cómo el documento se renderiza con nitidez a la nueva escala, en lugar de ver un mapa de bits estirado por el sistema operativo
// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144; // clears the bitmap cache and rebuilds layout
Límites que vale la pena conocer antes de realizar el lanzamiento
Tres límites prácticos merecen un espacio en su revisión de diseño. En primer lugar, WM_DPICHANGED solo llega si el proceso se suscribe: el manifiesto de su aplicación debe declarar la compatibilidad con DPI por monitor (per-monitor v2 en las versiones actuales de Windows), lo que en el IDE de Delphi es la opción de compatibilidad con DPI dentro de las opciones de Aplicación. Un proceso compatible con DPI del sistema nunca recibe el mensaje, y el control se renderiza a los DPI detectados al crear la ventana. En segundo lugar, la caché en disco cambia espacio de disco por velocidad: el espacio necesario para los mapas de bits de las páginas de diez documentos en varios niveles de zoom es almacenamiento real, por lo que debe apuntar DiskCacheFolder a una ubicación donde se sienta cómodo con el crecimiento del almacenamiento, no a un perfil móvil. En tercer lugar, la carpeta de caché requiere permisos de escritura para el usuario que la ejecuta; sin ellos, el visor sigue funcionando silenciosamente con cero aciertos; verifique DiskCacheHits en las pruebas si sospecha que la caché no está actuando
Juntas, las dos características cambian la calidad percibida de una aplicación con un uso intensivo de documentos más que la mayoría de las optimizaciones de renderizado: los documentos que sus usuarios vuelven a visitar se abren al instante, y la vista permanece nítida en cualquier monitor en el que se ubique. Ambas se distribuyen como propiedades normales de TPDFlibViewer en losLab PDF Library, por lo que adoptarlas es cuestión de una tarde de integración en lugar de un proyecto de motor de renderizado