HotPDF RenderCacheFolder transforme le cache de pages rendues en mémoire du composant HotPDF Delphi en un cache de pages persistant sur disque : les pages rendues sont écrites en fichiers PNG sous un dossier de votre choix, et la prochaine fois que la même source PDF est ouverte, RenderLoadedPageToBitmapCached les relit au lieu de rastériser à nouveau. L’ordre de recherche est mémoire, puis disque, puis le moteur de rendu
Le niveau disque est dans l’API depuis v2.416.0, mais jusqu’à v2.770.140 il n’a jamais réellement servi une page pour un appel LoadFromFile ou LoadFromStream normal. La correction a forcé une question à laquelle tout cache persistant doit répondre : comment savez-vous que le fichier ouvert aujourd’hui est le document rendu hier, et qu’advient-il des pages en cache quand ce n’est pas le cas ? Voici les réponses retenues par HotPDF, y compris là où il refuse délibérément de mettre en cache
Comment fonctionne le cache de rendu disque de HotPDF ?
Le cache de rendu disque de HotPDF est un second niveau derrière le cache raster en mémoire, et il ne participe que quand RenderCacheFolder est un chemin non vide. Un appel à RenderLoadedPageToBitmapCached(PageIndex, DPI) balaie d’abord les entrées en mémoire, indexées par index de page, DPI et une variante de réglages de rendu. Sur un raté, il interroge le niveau disque ; un succès disque décode le PNG, le repromeut en mémoire et renvoie une copie dont l’appelant est propriétaire. Ce n’est que quand les deux niveaux ratent que la page passe par l’interpréteur de flux de contenu décrit dans le rendu d’une page PDF chargée vers un TBitmap, et le bitmap frais est alors aussi écrit sur disque
Sur disque, l’agencement est délibérément ennuyeux. Chaque document reçoit un sous-dossier nommé d’après une clé de document de 16 caractères hexadécimaux plus une variante de rendu de 16 caractères hexadécimaux, chaque page est stockée en <page>@<dpi>.png, et un index.txt à la racine garde les documents en ordre dernier utilisé derrière une balise de schéma. Un désaccord de schéma vide le dossier au premier usage. Les écritures vont d’abord dans un fichier temporaire et sont permutées en place par un remplacement atomique, si bien qu’un crash en pleine écriture laisse soit l’ancienne page soit rien, jamais un demi-PNG. Un PNG qui échoue au décodage est supprimé et compté comme un raté
Trois limites bornent le dossier :
RenderCacheMaxDocuments(défaut 20) plafonne le nombre de sous-dossiers de documents ; le dossier le moins récemment utilisé est évincé en premierRenderCacheMaxBytes(défaut 524288000, soit 500 Mo) plafonne la taille totale de tous les fichiers PNG sous la racine- Chaque dossier de document garde au plus 200 images de pages ; ce plafond par document est figé dans THotPDF et n’est pas une propriété publiée
RenderCacheCapacity (défaut 8) est un bouton séparé : il règle combien de pages rendues le niveau en mémoire garde, et il n’a rien à voir avec l’empreinte disque
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Configurez le niveau disque avant le premier rendu mis en cache :
// le dossier et les deux limites sont lus au premier usage du niveau
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 Gio
Pdf.RenderCacheCapacity := 16; // pages en mémoire
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
// Remettez la copie à la bande de vignettes ici
finally
Bmp.Free; // l’appel en cache renvoie toujours une copie à l’appelant
end;
end;
finally
Pdf.Free; // depuis v2.770.140 ceci ne supprime plus les entrées disque
end;
end;
Exécutez la même procédure deux fois et le second passage ne rastérise jamais une page qui tenait dans le cache. L’objet de cache disque est créé paresseusement au premier rendu mis en cache et vit jusqu’à la libération de l’instance THotPDF, donc changer RenderCacheFolder, RenderCacheMaxDocuments ou RenderCacheMaxBytes après ce point ne déplace ni ne redimensionne un cache déjà ouvert. Les pages trop grandes pour la politique d’admission en mémoire (par défaut une seule entrée ne peut pas dépasser 64 Mio de pixels 32 bits) ne sont pas non plus persistées, et le niveau disque n’est consulté que tant que RenderFallbackPolicy garde son rfpIgnore par défaut, parce que les diagnostics de repli ne sont pas stockés à côté du PNG
Pourquoi RenderCacheFolder n’a-t-il jamais marché avant v2.770.140 ?
RenderCacheFolder n’avait aucun effet avant v2.770.140 parce que le niveau disque indexait les documents sur un hash des octets sources que les chargements ordinaires ne conservaient jamais. La clé de document venait d’un SHA-256 sur une copie interne des octets PDF bruts, mais LoadFromFile et LoadFromStream analysent la source sur place et ne gardent pas une telle copie ; le champ n’était rempli que temporairement sur un chemin de récupération chiffrée puis aussitôt vidé. Sans octets, la clé était toujours vide, et une clé vide veut dire que le niveau disque est contourné. Pas d’erreur, pas d’avertissement, juste un dossier qui restait vide
Rendre la clé non vide a exposé un second bug qui se cachait derrière le premier. L’ancien InvalidateRenderedPageCache supprimait le dossier disque du document, et InvalidateRenderedPageCache s’exécute au début de chaque chargement, à chaque édition et à l’intérieur de Free. Dès que la clé aurait marché, chaque session de visualiseur aurait détruit son propre cache en sortant, et la session suivante serait de toute façon repartie à froid. Pire, la clé était recalculée depuis la même source après une édition, si bien que les rendus du document édité auraient été stockés sous la clé du fichier d’origine et servis à la session suivante qui ouvrirait le PDF non modifié. v2.770.140 corrige l’identité et l’invalidation ensemble ; n’en corriger qu’une aurait livré soit un cache mort soit un cache menteur
Comment HotPDF identifie un PDF sans lire tout le fichier
HotPDF identifie un PDF chargé depuis un fichier local par une empreinte de sa taille, de sa date de dernière écriture et de ses premiers et derniers 64 Kio, et identifie une source flux ou à accès aléatoire par un SHA-256 de tout son contenu. Les deux sont capturés une fois, quand un chargement réussit, et les 16 premiers caractères hexadécimaux du condensat SHA-256 (64 bits) deviennent la clé de document
| Source | Identité | Coût | Capturé quand |
|---|---|---|---|
LoadFromFile | Taille + LastWriteTime + premiers et derniers 64 Kio, hachés en SHA-256 | Au plus 128 Kio lus, indépendant de la taille du fichier | Chaque chargement réussi, même si RenderCacheFolder est défini plus tard |
LoadFromStream | SHA-256 de tout le flux | Une passe complète sur la source | Seulement si RenderCacheFolder était défini avant le chargement |
LoadFromRandomAccessSource | SHA-256 de toute la source | Une passe complète sur la source | Seulement si le dossier était défini d’abord et que toute la plage est disponible |
Toute source avec une entrée /Encrypt | Aucune | Aucun | Jamais ; le niveau disque est contourné |
L’empreinte de fichier est un compromis délibéré. Hacher en entier une archive scannée de 400 Mo à chaque ouverture peut coûter plus que le rendu des deux pages que l’utilisateur regarde réellement. Les régions échantillonnées ne sont pas arbitraires : l’en-tête siège au début du fichier, et le trailer et la dernière section de références croisées siègent à la fin (ISO 32000-1 §7.5). Une mise à jour incrémentale ajoute un nouveau corps, une section de références croisées et un trailer (§7.5.6), donc elle change la taille et la queue d’un coup. Une réécriture complète par n’importe quel outil normal change la date de dernière écriture. Pour les fichiers jusqu’à 128 Kio, les deux échantillons couvrent chaque octet, donc les petits documents sont de fait hachés en entier
Le risque résiduel est une modification sur place, à taille égale, au milieu d’un gros fichier dont le rédacteur restaure ensuite l’horodatage d’origine. Il faut pour ça un outil qui préserve délibérément les dates de modification tout en éditant le contenu, ce qui est rare mais pas impossible, et dans ce cas le cache sert des pages périmées. L’envers de la médaille est bénin : copier un fichier sous Windows préserve normalement sa date de dernière écriture, si bien qu’une copie d’un document déjà en cache touche les mêmes entrées, ce qui est correct parce que les octets sont identiques
Les flux n’ont aucune date de modification du tout, donc la seule identité honnête est le contenu. HotPDF ne paie cette passe SHA-256 complète que si vous avez demandé un cache disque avant le chargement ; tout autre appelant de LoadFromStream ne voit aucun coût supplémentaire. L’ordre d’affectation des propriétés devient donc déterminant :
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Mauvais ordre pour les flux : le hash du contenu n’est calculé que
// quand le dossier est déjà défini, donc ce document contournerait le niveau disque
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // défini d’abord
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Une source à accès aléatoire encore en téléchargement (certaines plages pas encore disponibles) ne reçoit aucune identité plutôt qu’un hash de contenu partiel, et si le calcul de l’identité échoue pour une raison quelconque, le chargement réussit quand même ; le document se rend simplement sans le niveau disque
Qu’est-ce qui invalide une entrée du cache disque HotPDF ?
Une entrée du cache disque HotPDF n’est jamais invalidée en la supprimant à l’édition ; à la place, éditer le document chargé abandonne l’identité du document, si bien que le niveau disque est contourné pour le reste de ce chargement et que les pages stockées restent valides pour la source non modifiée. Les entrées quittent le disque seulement par les limites LRU et octets, un PNG corrompu, ou un changement de schéma
La clé décrit une source sur disque, pas le graphe d’objets en mémoire. Dès que vous tamponnez une page ou changez une annotation, le document ne correspond plus à cette source, donc ni lire ni écrire sous sa clé ne serait correct. Depuis v2.770.140, l’invalidation au niveau document comme au niveau page efface l’identité au lieu de toucher au dossier, et il y a un second garde-fou pour les éditions qui n’ont pas appelé InvalidateRenderedPageCache : avant d’utiliser le niveau disque, THotPDF vérifie si un objet chargé est dirty et traite un document dirty comme sans identité
Les réglages de rendu marchent dans l’autre sens. Changer PageRenderBackend (ou appeler UseNativeGDIRenderBackend), et appeler ConfigureRenderICCWorkflow ou ClearRenderICCWorkflow, vide les pages en mémoire mais garde l’identité, parce que le document correspond toujours à sa source. Ces réglages changent les pixels sans faire partie de la variante en mémoire, si bien que la clé disque incorpore le nom du backend, le drapeau de compensation du point noir et les condensats SHA-256 des profils ICC d’épreuve et de sortie. La variante couvre déjà elle-même l’intention colorimétrique, le dithering de sortie, l’aperçu de surimpression, le mode masque de luminosité, la politique de repli et la visibilité de chaque groupe de contenu optionnel, donc basculer un calque rend dans un dossier différent au lieu d’écraser la vue par défaut
Pour remettre un document édité sur le niveau disque, donnez-lui une nouvelle identité de source en le sauvegardant et en chargeant le résultat :
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Après avoir édité le document chargé : rafraîchissez les pages en mémoire.
// L’identité de source est déjà partie, donc rien n’est lu ni
// écrit dans le dossier disque du document d’origine
Pdf.InvalidateRenderedPageCache;
// Un fichier sauvegardé a une nouvelle taille et date, donc une nouvelle
// identité ; les rendus après ce chargement sont mis en cache sous la nouvelle clé
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Le dossier du document d’origine est laissé tranquille et vieillit via RenderCacheMaxDocuments et RenderCacheMaxBytes comme n’importe quelle autre entrée. Si l’utilisateur rouvre l’original non édité, ses pages sont toujours là
Frontières de sécurité : sources chiffrées et dossiers liés
Le cache de rendu disque de HotPDF refuse deux sortes d’entrées à dessein : il n’écrit jamais les pages d’un PDF chiffré sur disque, et il ne suit jamais un sous-dossier de document qui est une jonction ou un autre reparse point. Les deux règles échangent des succès de cache contre le fait de ne pas fuiter de données ni supprimer les mauvais fichiers
Les PDF chiffrés ne sont jamais mis en cache sur disque
Une page rendue est du contenu déchiffré. L’écrire en PNG ordinaire dans un dossier de cache laisserait une copie lisible d’un document protégé par mot de passe sur disque, hors de la protection choisie par l’auteur (ISO 32000-1 §7.6). HotPDF ne capture donc aucune identité pour toute source dont le trailer porte une entrée /Encrypt, y compris les fichiers ouverts avec un mot de passe ou avec un mot de passe utilisateur vide. Ces documents utilisent toujours le niveau en mémoire, qui meurt avec le processus
Les sous-dossiers jonction sont rejetés depuis v2.770.173
La racine du cache est votre choix, et la pointer vers une jonction est permis. Les sous-dossiers de documents en dessous sont une autre affaire : le cache les crée, les lit, les touche et les supprime de son propre chef, pendant la récupération au démarrage (qui retire les fichiers temporaires restants), la recherche (qui met à jour les horodatages), le stockage, l’invalidation et les trois limites d’éviction. Si quelqu’un ayant accès en écriture à la racine du cache remplace un dossier de document par une jonction vers un autre répertoire, chacun de ces chemins le suivrait, et l’éviction supprimerait des fichiers quelque part que le cache n’a jamais possédé. Depuis v2.770.173, chacun de ces points d’entrée vérifie l’attribut de reparse point et saute un dossier de document lié : une recherche compte un raté, un stockage compte un échec d’écriture, et l’éviction le laisse tranquille
Chemins Unicode et racines partagées
Deux corrections liées comptent si vous déployez vers des profils utilisateurs. Avant v2.770.135, RenderCacheFolder était un AnsiString, si bien qu’un dossier hors de la page de code système (un nom d’utilisateur chinois sur une installation Windows anglaise, par exemple) était converti avec perte avant que le cache ne le voie ; la propriété est désormais une string Unicode, et le remplacement atomique passe par l’API Windows large. Depuis v2.770.52, plusieurs instances THotPDF dans un même processus qui pointent vers la même racine (après expansion du chemin, comparaison insensible à la casse) partagent un index et un verrou uniques à comptage de références. Avant, chaque instance réécrivait index.txt avec sa propre copie et faisait respecter les limites contre sa vue partielle, si bien que le dossier pouvait dépasser plusieurs fois son budget
Ce partage s’arrête à la frontière du processus. Deux processus séparés sur la même racine tiennent toujours des index en mémoire distincts, donc donnez à chaque application tournant concurremment sa propre racine de cache. Les visualiseurs qui rendent sur des threads de travail vont bien au sein d’un processus : PrefetchLoadedPages et la file traitée dans le rendu en arrière-plan avec une file de requêtes passent tous deux par le même chemin en cache et le même verrou
Aide-mémoire : checklist RenderCacheFolder
- Définissez
RenderCacheFolder,RenderCacheMaxDocumentsetRenderCacheMaxBytesavant le premier appel àRenderLoadedPageToBitmapCached; pour les chargements par flux et à accès aléatoire, définissez le dossier avant de charger - Passez à v2.770.140 ou ultérieure si vous comptez sur le niveau disque ; les versions antérieures acceptent la propriété mais ne servent jamais une page depuis le disque pour des chargements normaux
- N’attendez aucun cache disque pour les PDF chiffrés, pour les documents édités après le chargement, ou tant que
RenderFallbackPolicyn’est pasrfpIgnore - Libérez l’instance THotPDF normalement ; depuis v2.770.140, ni
FreeniInvalidateRenderedPageCachene suppriment les entrées disque - Changer
PageRenderBackendou le workflow ICC garde le document sur le niveau disque sous une clé différente - Utilisez une racine de cache par application en cours d’exécution ; les instances au sein d’un processus partagent l’index depuis v2.770.52
- Gardez la racine du cache dans un emplacement par utilisateur ; les sous-dossiers de documents qui sont des jonctions sont sautés depuis v2.770.173
Un cache de pages persistant rapporte le plus dans un visualiseur qui rouvre les mêmes documents toute la journée, exactement la forme de l’architecture de visualiseur PDF personnalisé en Delphi décrite ailleurs sur ce blog. RenderCacheFolder, le cache raster en mémoire et le moteur de rendu de pages sont livrés avec le composant HotPDF Delphi PDF pour Delphi et C++Builder