HotPDF RenderCacheFolder convierte el caché de páginas renderizadas en memoria del componente HotPDF Delphi en un caché de páginas en disco persistente: las páginas renderizadas se escriben como archivos PNG bajo un folder que usted elige, y la próxima vez que se abre la misma fuente PDF, RenderLoadedPageToBitmapCached las lee de vuelta en vez de rasterizar otra vez. El orden de búsqueda es memoria, luego disco, luego el renderer
El tier de disco está en la API desde v2.416.0, pero hasta v2.770.140 nunca sirvió realmente una página para una llamada normal a LoadFromFile o LoadFromStream. La corrección forzó una pregunta que todo caché persistente tiene que responder: cómo saber que el archivo que abrió hoy es el documento que renderizó ayer, y qué pasa con las páginas cacheadas cuando no lo es. Abajo están las respuestas en que se asentó HotPDF, incluido dónde se niega deliberadamente a cachear
¿Cómo funciona el caché de render en disco de HotPDF?
El caché de render en disco de HotPDF es un segundo tier detrás del caché raster en memoria, y solo participa cuando RenderCacheFolder es una ruta no vacía. Una llamada a RenderLoadedPageToBitmapCached(PageIndex, DPI) primero barre las entradas en memoria, con clave de índice de página, DPI y una variante de render settings. Ante un miss le pregunta al tier de disco; un hit de disco decodifica el PNG, lo promueve de vuelta a memoria y devuelve una copia del caller. Solo cuando ambos tiers fallan la página pasa por el intérprete de content streams descrito en renderizar una página PDF cargada a un TBitmap, y el bitmap fresco también se escribe a disco
En disco el layout es deliberadamente aburrido. Cada documento recibe un subfolder nombrado a partir de una clave de documento de 16 caracteres hex más una variante de render de 16 caracteres hex, cada página se guarda como <page>@<dpi>.png, y un index.txt en el root mantiene los documentos en orden de más recientemente usado detrás de una etiqueta de schema. Un desajuste de schema limpia el folder en el primer uso. Las escrituras van primero a un archivo temporal y se intercambian a su lugar con un reemplazo atómico, así que un crash a mitad de escritura deja la página vieja o nada, jamás medio PNG. Un PNG que falla al decodificarse se borra y cuenta como miss
Tres límites acotan el folder:
RenderCacheMaxDocuments(default 20) acota el número de subfolders de documento; el folder menos recientemente usado se desaloja primeroRenderCacheMaxBytes(default 524288000, o sea 500 MB) acota el tamaño total de todos los PNG bajo el root- Cada folder de documento guarda como máximo 200 imágenes de página; ese tope por documento lo fija THotPDF y no es una propiedad publicada
RenderCacheCapacity (default 8) es un perilla aparte: define cuántas páginas renderizadas conserva el tier en memoria, y no tiene nada que ver con la huella en disco
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Configure el tier de disco antes del primer render cacheado:
// el folder y ambos límites se leen cuando el tier se usa por primera vez
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // páginas en memoria
if Pdf.LoadFromFile(FileName) > 0 then
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
if Bmp <> nil then
try
// Entregue la copia a la tira de thumbnails aquí
finally
Bmp.Free; // la llamada cacheada siempre devuelve una copia del caller
end;
end;
finally
Pdf.Free; // desde v2.770.140 esto ya no borra las entradas de disco
end;
end;
Corra el mismo procedimiento dos veces y la segunda corrida nunca rasteriza una página que cupo en el caché. El objeto de caché de disco se crea lazily en el primer render cacheado y vive hasta que la instancia THotPDF se libera, así que cambiar RenderCacheFolder, RenderCacheMaxDocuments o RenderCacheMaxBytes después de ese punto no mueve ni redimensiona un caché ya abierto. Las páginas demasiado grandes para la política de admisión en memoria (por defecto una sola entrada no puede superar 64 MiB de píxeles de 32 bits) tampoco se persisten, y al tier de disco solo se le consulta mientras RenderFallbackPolicy conserve su rfpIgnore por defecto, porque los diagnósticos de fallback no se guardan junto al PNG
¿Por qué RenderCacheFolder nunca funcionó antes de v2.770.140?
RenderCacheFolder no tenía efecto antes de v2.770.140 porque el tier de disco daba clave a los documentos por un hash de los bytes del fuente que las cargas ordinarias nunca conservaban. La clave de documento salía de un SHA-256 sobre una copia interna de los bytes crudos del PDF, pero LoadFromFile y LoadFromStream parsean el fuente en su lugar y no retienen tal copia; el campo solo se llenaba temporalmente en un camino de recuperación de cifrado y se limpiaba de inmediato. Sin bytes, la clave siempre quedaba vacía, y una clave vacía implica que el tier de disco se pasa por alto. Sin error, sin warning, solo un folder que se quedaba vacío
Hacer la clave no vacía expuso un segundo bug que llevaba tiempo escondido detrás del primero. El viejo InvalidateRenderedPageCache borraba el folder de disco del documento, y InvalidateRenderedPageCache corre al inicio de cada carga, en cada edición y dentro de Free. Así que en el momento en que la clave funcionara, cada sesión de visor habría destruido su propio caché al salir, y la siguiente sesión habría arrancado frío de todos modos. Peor: la clave se recomputaba del mismo fuente después de una edición, así que los renders del documento editado se habrían guardado bajo la clave del archivo original y se habrían servido a la siguiente sesión que abriera el PDF sin modificar. v2.770.140 corrige la identidad y la invalidación juntas; corregir solo una habría entregado o un caché muerto o un caché mentiroso
Cómo HotPDF identifica un PDF sin leer el archivo entero
HotPDF identifica un PDF cargado de un archivo local por una huella de su tamaño, su fecha de última escritura y sus primeros y últimos 64 KiB, e identifica un stream o una fuente random-access por un SHA-256 de su contenido completo. Ambas se capturan una vez, cuando una carga tiene éxito, y los primeros 16 caracteres hex del digest SHA-256 (64 bits) se vuelven la clave de documento
| Fuente | Identidad | Costo | Cuándo se captura |
|---|---|---|---|
LoadFromFile | Tamaño + LastWriteTime + los primeros y últimos 64 KiB, con hash SHA-256 | A lo sumo 128 KiB de lectura, independiente del tamaño del archivo | Cada carga exitosa, incluso si RenderCacheFolder se asigna después |
LoadFromStream | SHA-256 de todo el stream | Una pasada completa por el fuente | Solo si RenderCacheFolder estaba puesto antes de la carga |
LoadFromRandomAccessSource | SHA-256 de toda la fuente | Una pasada completa por la fuente | Solo si el folder se puso primero y todo el rango está disponible |
Cualquier fuente con una entrada /Encrypt | Ninguna | Ninguno | Nunca; el tier de disco se pasa por alto |
La huella del archivo es un trade-off deliberado. Hashear por completo un archivo escaneado de 400 MB en cada apertura puede costar más que renderizar las dos páginas que el usuario de verdad mira. Las regiones muestreadas no son arbitrarias: el header vive al inicio del archivo, y el trailer y la última sección de cross-reference viven al final (ISO 32000-1 §7.5). Un incremental update agrega un cuerpo, una sección de cross-reference y un trailer nuevos (§7.5.6), así que cambia el tamaño y la cola de una vez. Una reescritura completa por cualquier herramienta normal cambia la fecha de última escritura. Para archivos de hasta 128 KiB las dos muestras cubren cada byte, así que los documentos pequeños quedan efectivamente hasheados por completo
El riesgo residual es un cambio del mismo tamaño, in situ, al medio de un archivo grande cuyo writer después restaura el timestamp original. Eso requiere una herramienta que preserve deliberadamente las fechas de modificación mientras edita contenido, algo raro pero no imposible, y en ese caso el caché sirve páginas viejas. El lado benigno: copiar un archivo en Windows normalmente conserva su fecha de última escritura, así que una copia de un documento ya en el caché pega en las mismas entradas, lo que es correcto porque los bytes son idénticos
Los streams no tienen fecha de modificación alguna, así que la única identidad honesta es el contenido. HotPDF solo paga esa pasada completa de SHA-256 cuando usted pidió un caché de disco antes de cargar; todo otro caller de LoadFromStream no ve costo extra. Eso vuelve load-bearing el orden de asignación de la propiedad:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Orden equivocado para streams: el hash del contenido solo se computa
// cuando el folder ya está puesto, así que este documento pasaría de largo el tier de disco
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // póngalo primero
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Una fuente random-access que todavía se está descargando (algunos rangos aún no disponibles) no recibe identidad en vez de un hash de contenido parcial, y si computar la identidad falla por cualquier razón la carga igual tiene éxito; el documento simplemente renderiza sin el tier de disco
¿Qué invalida una entrada del caché de disco de HotPDF?
Una entrada del caché de disco de HotPDF jamás se invalida borrándola al editar; en cambio, editar el documento cargado tira la identidad del documento, así que el tier de disco se pasa por alto por el resto de esa carga y las páginas guardadas siguen siendo válidas para el fuente sin modificar. Las entradas salen del disco solo por los límites LRU y de bytes, por un PNG corrupto o por un cambio de schema
La clave describe un fuente en disco, no el grafo de objetos en memoria. En cuanto usted estampa una página o cambia una anotación, el documento ya no coincide con ese fuente, así que ni leer ni escribir bajo su clave sería correcto. Desde v2.770.140, tanto la invalidación a nivel de documento como la de página limpian la identidad en vez de tocar el folder, y hay una segunda guardia para las ediciones que no llamaron a InvalidateRenderedPageCache: antes de usar el tier de disco, THotPDF chequea si algún objeto cargado está dirty y trata un documento dirty como sin identidad
Los render settings funcionan al revés. Cambiar PageRenderBackend (o llamar a UseNativeGDIRenderBackend), y llamar a ConfigureRenderICCWorkflow o ClearRenderICCWorkflow, vacía las páginas en memoria pero conserva la identidad, porque el documento todavía coincide con su fuente. Esos settings cambian los píxeles sin ser parte de la variante en memoria, así que la clave de disco incorpora el nombre del backend, el flag de black-point compensation y los digests SHA-256 de los perfiles ICC de prueba y de salida. La variante en sí ya cubre la intención de color, el dithering de salida, el overprint preview, el modo luminosity mask, la política de fallback y la visibilidad de cada grupo de optional content, así que alternar una capa renderiza a un folder distinto en vez de sobrescribir la vista por defecto
Para devolver un documento editado al tier de disco, dele una nueva identidad de fuente guardándolo y cargando el resultado:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Después de editar el documento cargado: refresque las páginas en memoria.
// La identidad del fuente ya se fue, así que nada se lee del folder de
// disco del documento original ni se escribe en él
Pdf.InvalidateRenderedPageCache;
// Un archivo guardado tiene nuevo tamaño y nueva fecha de escritura, o sea
// nueva identidad; los renders tras esta carga se cachean bajo la clave nueva
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
El folder del documento original se queda tranquilo y envejece fuera por RenderCacheMaxDocuments y RenderCacheMaxBytes como cualquier otra entrada. Si el usuario reabre el original sin editar, sus páginas siguen ahí
Límites de seguridad: fuentes cifradas y folders enlazados
El caché de render en disco de HotPDF rechaza dos clases de input a propósito: nunca escribe páginas de un PDF cifrado a disco, y nunca sigue un subfolder de documento que sea una junction u otro reparse point. Ambas reglas canjean hits de caché por no filtrar datos ni borrar archivos ajenos
Los PDF cifrados jamás se cachean en disco
Una página renderizada es contenido descifrado. Escribirla como un PNG plano en un folder de caché dejaría una copia legible de un documento protegido por contraseña en el disco, fuera de la protección que eligió el autor (ISO 32000-1 §7.6). HotPDF por eso no captura identidad para ninguna fuente cuyo trailer traiga una entrada /Encrypt, incluidos los archivos abiertos con contraseña o con una contraseña de usuario vacía. Esos documentos siguen usando el tier en memoria, que muere con el proceso
Los subfolders junction se rechazan desde v2.770.173
El root del caché lo elige usted, y apuntarlo a una junction está permitido. Los subfolders de documento debajo de él son otro asunto: el caché los crea, lee, toca y borra por su cuenta, durante la recuperación de arranque (que elimina archivos temporales sobrantes), la búsqueda (que actualiza timestamps), el guardado, la invalidación y los tres límites de desalojo. Si alguien con acceso de escritura al root del caché reemplaza un folder de documento con una junction a otro directorio, cada uno de esos caminos lo seguiría, y el desalojo borraría archivos en un lugar que el caché jamás poseyó. Desde v2.770.173 cada uno de esos puntos de entrada chequea el atributo de reparse point y se salta un folder de documento enlazado: una búsqueda cuenta un miss, un guardado cuenta una falla de escritura, y el desalojo lo deja en paz
Rutas Unicode y roots compartidos
Dos correcciones relacionadas importan si usted despliega en perfiles de usuario. Antes de v2.770.135, RenderCacheFolder era un AnsiString, así que un folder fuera de la code page del sistema (un nombre de usuario chino en un Windows en inglés, por ejemplo) se convertía con pérdidas antes de que el caché lo viera; la propiedad ahora es un string Unicode, y el reemplazo atómico usa la API ancha de Windows. Desde v2.770.52, varias instancias de THotPDF en un proceso que apuntan al mismo root (tras expandir rutas, comparado sin distinguir mayúsculas) comparten un índice y un lock con reference counting. Antes, cada instancia sobrescribía el index.txt con su propia copia y aplicaba los límites contra su vista parcial, así que el folder podía crecer varias veces más allá de su presupuesto
Ese compartir se detiene en la frontera del proceso. Dos procesos separados sobre el mismo root todavía sostienen índices en memoria separados, así que dele a cada aplicación corriendo en paralelo su propio root de caché. Los visores que renderizan en worker threads van bien dentro de un proceso: PrefetchLoadedPages y la cola cubierta en renderizado en background con una cola de requests pasan ambos por el mismo camino cacheado y el mismo lock
Referencia rápida: checklist de RenderCacheFolder
- Asigne
RenderCacheFolder,RenderCacheMaxDocumentsyRenderCacheMaxBytesantes de la primera llamada aRenderLoadedPageToBitmapCached; para cargas por stream y random-access, ponga el folder antes de cargar - Actualice a v2.770.140 o posterior si depende del tier de disco; las versiones anteriores aceptan la propiedad pero jamás sirven una página desde el disco en cargas normales
- Espere sin caché en disco para PDFs cifrados, para documentos editados después de la carga, o mientras
RenderFallbackPolicyno searfpIgnore - Libere la instancia THotPDF normalmente; desde v2.770.140 ni
FreeniInvalidateRenderedPageCacheborran entradas de disco - Cambiar
PageRenderBackendo el workflow ICC mantiene el documento en el tier de disco bajo una clave distinta - Use un root de caché por aplicación corriendo; las instancias dentro de un proceso comparten el índice desde v2.770.52
- Deje el root del caché en una ubicación por usuario; los subfolders de documento que sean junctions se saltan desde v2.770.173
Un caché de páginas persistente rinde más en un visor que reabre los mismos documentos todo el día, que es justo la forma de la arquitectura de visor PDF custom en Delphi descrita en otro lugar de este blog. RenderCacheFolder, el caché raster en memoria y el renderer de páginas vienen con el componente HotPDF Delphi PDF para Delphi y C++Builder