Dos quejas suelen acompañar a todo visor de PDF integrado: 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 resolución (high-DPI). losLab PDF Library responde a 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 WM_DPICHANGED, dota al control de una verdadera compatibilidad con DPI por monitor
Vale la pena tomarse en serio ambos problemas porque los usuarios los perciben de inmediato. Un contrato escaneado de 400 páginas que ayer tardó ocho segundos en permitir el desplazamiento vuelve a tardar ocho segundos hoy, a pesar de que no ha cambiado nada en el archivo. Y en un escritorio con DPI mixto, un visor que se renderiza a los 96 DPI del monitor principal se ve bien allí, pero visiblemente difuminado en el panel 4K contiguo. Este artículo describe cómo TPDFlibViewer soluciona cada uno de ellos 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, comience con la descripción general del control de visor de PDF interactivo y regrese para conocer la capa de rendimiento
¿Cómo almacenar en caché las 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 distribución es <Carpeta>\PDFlibPas-PageCache\<huella digital del documento>\p<página>_z<clave de zoom>.bin: una subcarpeta por documento y un archivo por cada combinación de página y zoom. Todo lo demás es automático; 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;
// Una línea activa la caché de páginas entre sesiones
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
El contador DiskCacheHits informa de cuántas renderizaciones de página ahorró la caché en disco en la sesión actual, lo que hace que la mejora sea medible en lugar de anecdótica. En un documento que cerró y volvió a abrir, cada página que se desplace por la pantalla sin necesidad de un paso de 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. Ese trío constituye toda la estrategia de invalidación: cuando se edita el archivo, cambia 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 finalmente es eliminada por el recorte 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, ni ninguna forma de que se sirva un mapa de bits antiguo para un documento modificado
Esta es la misma idea de "la identidad del contenido como clave de caché" que utilizan los sistemas de compilación, y demuestra su utilidad al eliminar varios modos de fallo. Una caché que almacena archivos bajo el nombre sin formato del documento tiene que detectar las ediciones de forma explícita, y cada comprobación explícita es un error potencial a la espera de un caso límite: un archivo restaurado desde una copia de seguridad con una marca de tiempo antigua, un guardado que conserva el tamaño o una ruta que se compara diferenciando mayúsculas y minúsculas en una máquina pero no en otra. Aplicar un hash a estas tres señales en el nombre de la carpeta hace que la obsolescencia sea estructuralmente imposible en lugar de comprobarse mediante procedimientos
Por qué la rotación se excluye deliberadamente de la clave del 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 almacena siempre los bytes de la renderización sin rotar; cuando se carga una página de la caché mientras la vista está rotada, los píxeles se reorientan en memoria tras la decodificación. Si la rotación formara parte de la clave en disco, un usuario que recorriera los cuatro ángulos de visión escribiría la misma página en el disco cuatro veces, cuadruplicando el crecimiento de la caché para no obtener información adicional, ya que la rotación es una reorganización de píxeles económica en comparación con un paso de 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: renderiza siempre páginas sin rotar e informa de claves de zoom simples, y el hilo principal aplica la rotación al procesar los resultados, que es la misma regla de "el hilo principal posee la caché" analizada en renderizado 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 una renderización canónica por página y zoom, en cualquier lugar donde resulte costoso almacenarla
¿Cómo mantiene el recorte LRU limitada la carpeta de caché?
La caché conserva hasta diez carpetas de documentos y recorta las utilizadas menos recientemente cuando llegan nuevos documentos. La antigüedad se rastrea mediante un archivo last-used.marker en cada carpeta de documentos, cuya marca de tiempo se modifica tanto en las escrituras como en las lecturas de caché, por lo que un documento que solo vuelva a leer se mantiene activo sin necesidad de volver a escribirse. La carpeta perteneciente al documento abierto actualmente está exenta de eliminación, por lo que el recorte nunca puede retirar páginas de la vista activa
Cada operación de disco en la caché (cálculo de huella digital, creación de carpetas, modificación de marcadores, lecturas y escrituras de páginas, recorte) ignora sus propias excepciones. Se trata de una postura deliberada, no de descuido: la caché es un acelerador que trabaja bajo el principio de mejor esfuerzo, y un disco lleno, una carpeta de solo lectura o un bloqueo del antivirus debe degradar el visor a "renderiza como antes", nunca a "falla al mostrar la página". El peor resultado que puede producir un error de E/S de caché es una coincidencia perdida
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// ¿Cuántas pasadas de renderizado ahorró la caché en disco esta sesión?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
¿Cómo hacer que un visor de PDF sea compatible con DPI en Delphi?
TPDFlibViewer canaliza cada cálculo que depende de los DPI a través de un único campo, y ese diseño con un único punto de cambio es lo que hace manejable la compatibilidad por monitor. El código de diseño, prebúsqueda y renderizado lee un único valor FScreenDPI value; al 100% de zoom, un punto de PDF se asocia a ScreenDPI / 72 píxeles. El configurador de la propiedad pública ScreenDPI borra la caché de mapas de bits, reconstruye el diseño, actualiza los rangos de desplazamiento e invalida la ventana, por lo que cambiar los DPI consiste en una única asignación y todos los consumidores se actualizan de forma automática. La misma matemática de píxeles es importante cuando se renderiza para una impresora en lugar de un monitor, tema cubierto en vista previa de impresión y renderizado de contexto de dispositivo
La detección se realiza en una invalidación de CreateWnd en lugar de en el constructor, debido a que GetDpiForWindow necesita un manejador 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 predeterminado heredado de 96 para que la matemática del diseño sea coherente. GetDpiForWindow 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 se sigue cargando en todas partes y las versiones antiguas de Windows simplemente informan de los DPI del sistema
¿Qué sucede cuando la ventana se mueve a un monitor 4K?
Windows envía 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 de la palabra baja de wParam, los asigna a través de SetScreenDPI (lo que descarta la caché de mapas de bits ahora incorrecta y vuelve a organizar 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 lógico del cliente a lo largo del desplazamiento. El usuario observa cómo el documento se vuelve a renderizar con nitidez en la nueva escala en lugar de ver un mapa de bits estirado por el sistema operativo
// Normalmente nunca se toca ScreenDPI: CreateWnd detecta el monitor anfitrión
// y WM_DPICHANGED realiza el seguimiento de los movimientos. Invalídelo solo para objetivos especiales,
// por ejemplo, renderizar el diseño como si fuera para una pantalla del 150%:
// borra la caché de mapas de bits y reconstruye el diseño
FViewer.ScreenDPI := 144;
Límites que conviene conocer antes de la distribución
Tres límites prácticos merecen un lugar en su revisión de diseño. En primer lugar, WM_DPICHANGED solo llega si el proceso opta por ello: el manifiesto de su aplicación debe declarar la compatibilidad con DPI por monitor (per-monitor v2 en las versiones actuales de Windows), que en el IDE de Delphi es la configuración de compatibilidad con DPI en las opciones de la aplicación. Un proceso compatible con DPI del sistema nunca recibe el mensaje y, en ese caso, el control se renderiza a los DPI que detectó en la creación de la ventana. En segundo lugar, la caché de disco cambia espacio de disco por velocidad (el equivalente a diez documentos de mapas de bits de páginas 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é necesita permisos de escritura para el usuario en ejecución; sin ellos, el visor sigue funcionando silenciosamente con cero resultados; compruebe DiskCacheHits durante 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 se mantiene nítida en cualquier monitor en el que se ubique. Ambas se distribuyen como propiedades comunes en TPDFlibViewer de 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