Artículo técnico

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

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

Diagrama de HotPDF de la búsqueda en la caché de renderizado para RenderLoadedPageToBitmapCached: primero se comprueba el nivel en memoria indexado por página, DPI y variante de render, luego el nivel de disco RenderCacheFolder de archivos PNG con reemplazo atómico, luego el intérprete de content stream, y cada acierto devuelve una copia propiedad del llamador
HotPDF mira primero en memoria, luego en disco, y solo entonces rasteriza; un acierto en disco se asciende de vuelta a memoria y cada camino le entrega una copia que es suya y debe liberar

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

FuenteIdentidadCosteCuándo se captura
LoadFromFileTamaño + LastWriteTime + primeros y últimos 64 KiB, hasheados con SHA-256Como mucho 128 KiB leídos, independiente del tamaño del archivoCada carga con éxito, aunque RenderCacheFolder se fije después
LoadFromStreamSHA-256 del stream enteroUna pasada completa por el fuenteSolo si RenderCacheFolder se fijó antes de la carga
LoadFromRandomAccessSourceSHA-256 de la fuente enteraUna pasada completa por la fuenteSolo si la carpeta se fijó primero y todo el rango está disponible
Cualquier fuente con una entrada /EncryptNingunaNingunoNunca; el nivel de disco se omite
Mapa de identidad de fuentes de HotPDF para la caché de render en disco: LoadFromFile hashea tamaño, LastWriteTime y los primeros y últimos 64 KiB, LoadFromStream y LoadFromRandomAccessSource hashean el contenido entero solo cuando RenderCacheFolder se fijó primero, y cualquier trailer /Encrypt no captura identidad alguna
los archivos se huellan desde sus extremos porque la cabecera, el xref y el trailer viven ahí, los streams solo pagan un hash completo cuando usted pidió la caché primero, y los documentos cifrados jamás se escriben a disco

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

Semántica de invalidación de HotPDF para la caché de disco RenderCacheFolder: editar el documento cargado o cualquier objeto sucio suelta la identidad de fuente así que el nivel se omite, cambiar el backend de render o el flujo ICC conserva la identidad bajo una clave de variante nueva, y guardar más recargar recambia la clave del documento
una edición jamás borra la carpeta guardada, un cambio de ajustes 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 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, RenderCacheMaxDocuments y RenderCacheMaxBytes antes de la primera llamada a RenderLoadedPageToBitmapCached; 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 RenderFallbackPolicy no esté en rfpIgnore
  • Libere la instancia THotPDF con normalidad; desde v2.770.140 ni Free ni InvalidateRenderedPageCache borran entradas de disco
  • Cambiar PageRenderBackend o 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