RenderCacheFolder convierte la caché de páginas renderizadas en memoria del componente Delphi HotPDF en una caché de páginas persistente en disco: las páginas renderizadas se escriben como archivos PNG bajo una carpeta que usted elige, y la próxima vez que se abre la misma fuente PDF, RenderLoadedPageToBitmapCached las lee de vuelta en lugar de rasterizar otra vez. El orden de búsqueda es memoria, luego disco, luego el renderer
El nivel 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. El arreglo obligó a plantear la pregunta que toda caché persistente tiene que responder: ¿cómo sabe 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 que adoptó HotPDF, incluidos los puntos donde deliberadamente se niega a cachear
¿Cómo funciona la caché de renderizado en disco de HotPDF?
La caché de renderizado en disco de HotPDF es un segundo nivel detrás de la caché raster en memoria, y solo participa cuando RenderCacheFolder es una ruta no vacía. Una llamada a RenderLoadedPageToBitmapCached(PageIndex, DPI) barre primero las entradas en memoria, indexadas por índice de página, DPI y una variante de ajustes de renderizado. Si falla, pregunta al nivel de disco; un acierto en disco decodifica el PNG, lo asciende de vuelta a memoria y devuelve una copia propiedad del llamador. Solo cuando fallan ambos niveles pasa la página por el intérprete de content stream descrito en renderizar una página PDF cargada a un TBitmap, y el bitmap fresco se escribe entonces también a disco
En disco la disposición es deliberadamente aburrida. Cada documento recibe una subcarpeta nombrada a partir de una clave de documento de 16 caracteres hexadecimales más una variante de render de otros 16, cada página se guarda como <page>@<dpi>.png, y un index.txt en la raíz mantiene los documentos en orden de uso más reciente tras una etiqueta de esquema. Un desajuste de esquema vacía la carpeta en el primer uso. Las escrituras van primero a un archivo temporal y se intercambian a su sitio con un reemplazo atómico, así que un crash a mitad de escritura deja la página antigua o nada, nunca medio PNG. Un PNG que falla al decodificarse se borra y cuenta como fallo
Tres límites acotan la carpeta:
RenderCacheMaxDocuments(por defecto 20) acota el número de subcarpetas de documento; la carpeta usada hace más tiempo se expulsa primeroRenderCacheMaxBytes(por defecto 524288000, que son 500 MB) acota el tamaño total de todos los PNG bajo la raíz- Cada carpeta de documento guarda como mucho 200 imágenes de página; ese tope por documento lo fija THotPDF y no es una propiedad publicada
RenderCacheCapacity (por defecto 8) es un mando aparte: establece cuántas páginas renderizadas conserva el nivel 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 nivel de disco antes del primer render cacheado:
// la carpeta y ambos límites se leen cuando el nivel 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 aquí la copia a la tira de miniaturas
finally
Bmp.Free; // la llamada cacheada siempre devuelve una copia propiedad del llamador
end;
end;
finally
Pdf.Free; // desde v2.770.140 esto ya no borra las entradas de disco
end;
end;
Ejecute el mismo procedimiento dos veces y la segunda pasada no rasteriza ninguna página que cupiera en la caché. El objeto de caché de disco se crea perezosamente 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 una caché ya abierta. Las páginas demasiado grandes para la política de admisión en memoria (por defecto una entrada no puede superar 64 MiB de píxeles de 32 bits) tampoco se persisten, y al nivel de disco solo se consulta mientras RenderFallbackPolicy mantiene 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 nivel de disco indexaba los documentos por un hash de los bytes del fuente que las cargas corrientes nunca conservaban. La clave de documento salía de un SHA-256 sobre una copia interna de los bytes en crudo del PDF, pero LoadFromFile y LoadFromStream parsean el fuente en el sitio y no retienen semejante copia; el campo solo se rellenaba temporalmente en un camino de recuperación de cifrado y se borraba justo después. Sin bytes, la clave estaba siempre vacía, y una clave vacía significa que el nivel de disco se omite. Sin error, sin aviso, solo una carpeta que seguía vacía
Hacer que la clave dejara de estar vacía destapó un segundo bug que llevaba escondido tras el primero. El antiguo InvalidateRenderedPageCache borraba la carpeta 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 del visor habría destruido su propia caché al salir, y la siguiente sesión habría arrancado fría de todas formas. Peor aún: la clave se recalculaba del mismo fuente tras 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 arregla la identidad y la invalidación juntas; arreglar solo una habría despachado o una caché muerta o una caché mentirosa
Cómo identifica HotPDF un PDF sin leer el archivo entero
HotPDF identifica un PDF cargado desde 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 de acceso aleatorio por un SHA-256 de todo su contenido. Ambas se capturan una vez, cuando una carga tiene éxito, y los primeros 16 caracteres hexadecimales del digest SHA-256 (64 bits) se convierten en la clave de documento
| Fuente | Identidad | Coste | Cuándo se captura |
|---|---|---|---|
LoadFromFile | Tamaño + LastWriteTime + primeros y últimos 64 KiB, hasheados con SHA-256 | Como mucho 128 KiB leídos, independiente del tamaño del archivo | Cada carga con éxito, aunque RenderCacheFolder se fije después |
LoadFromStream | SHA-256 del stream entero | Una pasada completa por el fuente | Solo si RenderCacheFolder se fijó antes de la carga |
LoadFromRandomAccessSource | SHA-256 de la fuente entera | Una pasada completa por la fuente | Solo si la carpeta se fijó primero y todo el rango está disponible |
Cualquier fuente con una entrada /Encrypt | Ninguna | Ninguno | Nunca; el nivel de disco se omite |
La huella del archivo es una compensación deliberada. 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 mira de verdad. Las regiones muestreadas no son arbitrarias: la cabecera está al principio del archivo, y el trailer y la última sección de cross-reference están al final (ISO 32000-1 §7.5). Un incremental update añade un cuerpo nuevo, una sección de cross-reference y un trailer nuevos (§7.5.6), así que cambia el tamaño y la cola a la 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 hasheados en la práctica por completo
El riesgo residual es un cambio del mismo tamaño, en el sitio, al centro de un archivo grande cuyo escritor restaure después la marca de tiempo original. Eso exige una herramienta que preserve deliberadamente las marcas de modificación mientras edita contenido, algo raro pero no imposible, y en ese caso la caché sirve páginas caducadas. El lado amable es benigno: copiar un archivo en Windows normalmente preserva su fecha de última escritura, así que una copia de un documento ya en la caché pega en las mismas entradas, lo cual es correcto porque los bytes son idénticos
Los streams no tienen marca 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ó una caché de disco antes de cargar; todo otro llamador de LoadFromStream no ve coste extra. Eso convierte el orden de asignación de la propiedad en algo load-bearing:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Orden equivocado para streams: el hash del contenido solo se calcula
// cuando la carpeta ya está fijada, así que este documento omitiría el nivel
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // fije primero
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Una fuente de acceso aleatorio que sigue descargándose (algunos rangos aún no disponibles) no recibe identidad en lugar de un hash de contenido parcial, y si calcular la identidad falla por cualquier motivo la carga tiene éxito igualmente; el documento sencillamente renderiza sin el nivel de disco
¿Qué invalida una entrada de la caché de disco de HotPDF?
Una entrada de la caché de disco de HotPDF jamás se invalida borrándola al editar; en su lugar, editar el documento cargado suelta la identidad del documento, así que el nivel de disco se omite por el resto de esa carga y las páginas guardadas siguen siendo válidas para el fuente sin modificar. Las entradas abandonan el disco solo por los límites LRU y de bytes, por un PNG corrupto o por un cambio de esquema
La clave describe una 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 casa con esa 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 borran la identidad en lugar de tocar la carpeta, y hay una segunda guardia para las ediciones que no llamaron a InvalidateRenderedPageCache: antes de usar el nivel de disco, THotPDF comprueba si algún objeto cargado está sucio y trata un documento sucio como sin identidad
Los ajustes de renderizado 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 sigue casando con su fuente. Esos ajustes cambian los píxeles sin formar parte de la variante en memoria, así que la clave de disco incorpora el nombre del backend, el flag de compensación de punto negro y 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, la vista previa de overprint, el modo de máscara de luminosidad, la política de fallback y la visibilidad de cada grupo de contenido opcional, así que alternar una capa renderiza a una carpeta distinta en lugar de sobrescribir la vista por defecto
Para devolver un documento editado al nivel de disco, dele una identidad de fuente nueva guardándolo y cargando el resultado:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Tras editar el documento cargado: refresque las páginas en memoria.
// La identidad de fuente ya se fue, así que nada se lee del ni se
// escribe en la carpeta de disco del documento original
Pdf.InvalidateRenderedPageCache;
// Un archivo guardado tiene tamaño y fecha de última escritura nuevos,
// luego identidad nueva; 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;
La carpeta del documento original se deja tranquila y envejece por RenderCacheMaxDocuments y RenderCacheMaxBytes como cualquier otra entrada. Si el usuario reabre el original sin editar, sus páginas siguen ahí
Fronteras de seguridad: fuentes cifradas y carpetas enlazadas
La caché de renderizado en disco de HotPDF rechaza dos clases de entrada a propósito: jamás escribe páginas de un PDF cifrado a disco, y jamás sigue una subcarpeta de documento que sea un junction u otro reparse point. Ambas reglas cambian aciertos 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 una carpeta 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). Por eso HotPDF no captura identidad para ninguna fuente cuyo trailer lleve una entrada /Encrypt, incluidos los archivos abiertos con contraseña o con una contraseña de usuario vacía. Esos documentos siguen usando el nivel en memoria, que muere con el proceso
Las subcarpetas junction se rechazan desde v2.770.173
La raíz de la caché la elige usted, y apuntarla a un junction está permitido. Las subcarpetas de documento debajo son otra historia: la caché las crea, lee, toca y borra por su cuenta, durante la recuperación de arranque (que retira los archivos temporales sobrantes), la búsqueda (que actualiza marcas de tiempo), el almacenamiento, la invalidación y los tres límites de expulsión. Si alguien con acceso de escritura a la raíz de la caché sustituye una carpeta de documento por un junction a otro directorio, cada uno de esos caminos lo seguiría, y la expulsión borraría archivos en algún sitio que la caché jamás poseyó. Desde v2.770.173 cada uno de esos puntos de entrada comprueba el atributo de reparse point y se salta una carpeta de documento enlazada: una búsqueda cuenta un fallo, un almacenamiento cuenta un fallo de escritura, y la expulsión la deja en paz
Rutas Unicode y raíces compartidas
Dos arreglos relacionados importan si usted despliega en perfiles de usuario. Antes de v2.770.135, RenderCacheFolder era un AnsiString, así que una carpeta fuera de la code page del sistema (un nombre de usuario chino en una instalación de Windows en inglés, por ejemplo) se convertía con pérdidas antes de que la caché lo viera; la propiedad es ahora un string Unicode, y el reemplazo atómico usa la API ancha de Windows. Desde v2.770.52, varias instancias THotPDF en un proceso que apunten a la misma raíz (tras expandir la ruta, comparada sin distinguir mayúsculas) comparten un único índice y lock con recuento de referencias. Antes, cada instancia sobrescribía index.txt con su propia copia y aplicaba los límites sobre su vista parcial, así que la carpeta podía crecer varias veces por encima de su presupuesto
Ese compartimiento se detiene en la frontera del proceso. Dos procesos separados sobre la misma raíz siguen sosteniendo índices en memoria distintos, así que dele a cada aplicación corriendo en paralelo su propia raíz de caché. Los visores que renderizan en worker threads van bien dentro de un proceso: PrefetchLoadedPages y la cola cubierta en renderizado en segundo plano con una cola de peticiones pasan ambos por el mismo camino cacheado y el mismo lock
Referencia rápida: lista de comprobación de RenderCacheFolder
- Fije
RenderCacheFolder,RenderCacheMaxDocumentsyRenderCacheMaxBytesantes de la primera llamada aRenderLoadedPageToBitmapCached; para cargas por stream y de acceso aleatorio, fije la carpeta antes de cargar - Actualice a v2.770.140 o posterior si depende del nivel de disco; las versiones anteriores aceptan la propiedad pero jamás sirven una página desde disco en cargas normales
- Espere sin caché en disco para PDF cifrados, para documentos editados tras la carga, o mientras
RenderFallbackPolicyno esté enrfpIgnore - Libere la instancia THotPDF con normalidad; desde v2.770.140 ni
FreeniInvalidateRenderedPageCacheborran entradas de disco - Cambiar
PageRenderBackendo el flujo ICC mantiene el documento en el nivel de disco bajo una clave distinta - Use una raíz de caché por aplicación en ejecución; las instancias dentro de un proceso comparten el índice desde v2.770.52
- Mantenga la raíz de la caché en una ubicación por usuario; las subcarpetas de documento que sean junctions se saltan desde v2.770.173
Una caché de páginas persistente rinde sobre todo en un visor que reabre los mismos documentos todo el día, que es justo la forma de la arquitectura de visor PDF a medida en Delphi descrita en otro lugar de este blog. RenderCacheFolder, la caché raster en memoria y el renderer de páginas vienen con el componente Delphi PDF de HotPDF para Delphi y C++Builder