Artículo técnico

RenderCacheFolder de HotPDF: caché de páginas en disco

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

Diagrama de HotPDF de la búsqueda del caché de render para RenderLoadedPageToBitmapCached: primero se chequea el tier en memoria con clave de página, DPI y variante de render, luego el tier de disco RenderCacheFolder de archivos PNG con reemplazo atómico, luego el intérprete de content streams, y cada hit devuelve una copia del caller
HotPDF busca primero en memoria, luego en disco, y solo entonces rasteriza; un hit de disco se promueve de vuelta a memoria y cada camino le entrega una copia que es suya y debe liberar

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 primero
  • RenderCacheMaxBytes (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

FuenteIdentidadCostoCuándo se captura
LoadFromFileTamaño + LastWriteTime + los primeros y últimos 64 KiB, con hash SHA-256A lo sumo 128 KiB de lectura, independiente del tamaño del archivoCada carga exitosa, incluso si RenderCacheFolder se asigna después
LoadFromStreamSHA-256 de todo el streamUna pasada completa por el fuenteSolo si RenderCacheFolder estaba puesto antes de la carga
LoadFromRandomAccessSourceSHA-256 de toda la fuenteUna pasada completa por la fuenteSolo si el folder se puso primero y todo el rango está disponible
Cualquier fuente con una entrada /EncryptNingunaNingunoNunca; el tier de disco se pasa por alto
Mapa de identidad de fuentes de HotPDF para el caché de render en disco: LoadFromFile hashea tamaño, LastWriteTime y los primeros y últimos 64 KiB, LoadFromStream y LoadFromRandomAccessSource hashean todo el contenido solo cuando RenderCacheFolder se puso primero, y un trailer con /Encrypt no captura identidad alguna
Los archivos se huellan por sus extremos porque el header, el xref y el trailer viven allí, los streams solo pagan un hash completo cuando usted pidió el caché primero, y los documentos cifrados jamás se escriben a disco

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

Semánticas de invalidación de HotPDF para el caché de disco RenderCacheFolder: editar el documento cargado o cualquier objeto dirty tira la identidad del fuente así el tier se pasa por alto, cambiar el backend de render o el workflow ICC conserva la identidad bajo una clave de variante nueva, y guardar más recargar le da al documento una clave nueva
una edición jamás borra el folder guardado, un cambio de settings renderiza bajo una clave distinta, y solo guardar más recargar le gana al documento editado una identidad fresca

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, RenderCacheMaxDocuments y RenderCacheMaxBytes antes de la primera llamada a RenderLoadedPageToBitmapCached; 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 RenderFallbackPolicy no sea rfpIgnore
  • Libere la instancia THotPDF normalmente; desde v2.770.140 ni Free ni InvalidateRenderedPageCache borran entradas de disco
  • Cambiar PageRenderBackend o 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