Article Technique

PDF Viewer Disk Cache and Per-Monitor DPI in Delphi

Deux plaintes accompagnent régulièrement chaque lecteur PDF intégré : les documents volumineux sont rendus à nouveau à partir de zéro à chaque lancement, et le texte devient flou dès que la fenêtre est déplacée sur un écran haute densité (High-DPI). losLab PDF Library résout ces deux problèmes dans son composant TPDFlibViewer : la propriété DiskCacheFolder active un cache disque de pages persistant d'une session à l'autre, indexé par l'empreinte du document, et la propriété ScreenDPI, s'appuyant sur GetDpiForWindow et un gestionnaire WM_DPICHANGED, confère au composant une véritable prise en charge du DPI par moniteur

Ces deux problèmes méritent d'être pris au sérieux car les utilisateurs les remarquent immédiatement. Un contrat numérisé de 400 pages qui mettait hier huit secondes à devenir défilable mettra encore huit secondes aujourd'hui, même si rien n'a changé dans le fichier. De plus, sur un bureau avec plusieurs écrans aux DPI différents, un lecteur rendu à 96 DPI sur l'écran principal y est net mais devient visiblement flou sur le panneau 4K adjacent. Cet article explique comment TPDFlibViewer résout chacun de ces cas et, plus utile encore, pourquoi cette conception a été choisie. Si vous découvrez ce composant, commencez par consulter l'aperçu du composant de lecteur PDF interactif avant de revenir étudier la couche de performances

Comment mettre en cache les pages PDF rendues d'une session à l'autre dans Delphi ?

Affectez un dossier à TPDFlibViewer.DiskCacheFolder et le composant enregistre les bitmaps de pages rendus sur le disque. Ainsi, un document récemment consulté s'ouvre instantanément au lieu d'être rendu à nouveau. Sur le disque, la structure est <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin : un sous-dossier par document, un fichier par combinaison de page et de zoom. Tout le reste est automatique ; il n'y a pas d'API de cache à appeler ni d'opération de vidage à effectuer

uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // One line turns on the cross-session page cache
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

Le compteur DiskCacheHits indique le nombre de rendus de pages économisés par le cache disque au cours de la session active, ce qui permet de mesurer le gain de manière concrète plutôt qu'anecdotique. Sur un document que vous avez fermé puis rouvert, chaque page qui défile à l'écran sans phase de rendu incrémente ce compteur

Pourquoi la clé de cache est une empreinte numérique et non un numéro de version

L'empreinte du document est un hash FNV-1a 64 bits de la chaîne path|size|last write time, formaté sous la forme de seize chiffres hexadécimaux. Ce triplet constitue toute la stratégie d'invalidation : lorsque le fichier est modifié, sa taille ou sa date d'écriture change, le hash change, et le lecteur ouvre simplement un sous-dossier de cache différent. Le dossier obsolète n'est plus jamais consulté et finit par être supprimé par l'élagage LRU. Il n'y a pas de protocole de balise de version à maintenir, pas de logique de comparaison d'horodatage sujette aux décalages, et aucun risque qu'un ancien bitmap ne soit servi pour un document modifié

C'est le même concept de « l'identité du contenu comme clé de cache » que celui utilisé par les systèmes de build, et il prouve son efficacité par les modes de défaillance qu'il élimine. Un cache stockant les fichiers sous le simple nom du document doit détecter explicitement les modifications, et chaque vérification explicite est une source d'erreurs potentielles face aux cas limites : un fichier restauré à partir d'une sauvegarde avec un horodatage ancien, une sauvegarde qui conserve la taille, un chemin comparé en respectant la casse sur une machine et pas sur une autre. Hacher ces trois signaux dans le nom du dossier rend l'obsolescence structurellement impossible plutôt que procéduralement vérifiée

Pourquoi la rotation est délibérément exclue de la clé disque

Le nom du fichier de cache encode le numéro de page et le zoom, et rien d'autre — l'angle de rotation de la vue est intentionnellement absent. Le cache disque stocke toujours les octets de rendu non pivotés ; lorsqu'une page mise en cache est chargée alors que la vue est pivotée, les pixels sont réorientés en mémoire après le décodage. Si la rotation faisait partie de la clé disque, un utilisateur parcourant les quatre angles de vue écrirait la même page sur le disque quatre fois, quadruplant la croissance du cache pour aucune information supplémentaire, car la rotation est une simple réorganisation de pixels peu coûteuse par rapport à une passe de rendu complète

Le cache bitmap en mémoire utilise quant à lui une clé composite, Round(Zoom * 1000) * 4 + Rotation div 90, car le bitmap pivoté correspond exactement à ce dont le gestionnaire de dessin a besoin. Le thread de pré-chargement en arrière-plan suit la même règle : il effectue toujours le rendu des pages non pivotées et transmet de simples clés de zoom, et le thread principal applique la rotation lors de la récupération des résultats — la règle selon laquelle le thread principal possède le cache, abordée dans rendu parallèle de pages et sécurité des threads. Exclure la rotation de tout ce qui se trouve sous la couche mémoire permet d'avoir un rendu canonique unique par page et par zoom, partout où le stockage est coûteux

Comment l'élagage LRU maintient-il la taille du dossier de cache limitée ?

Le cache conserve jusqu'à dix dossiers de documents et supprime les moins récemment utilisés (LRU) lors de l'arrivée de nouveaux documents. L'utilisation récente est suivie par un fichier last-used.marker dans chaque dossier de document, dont l'horodatage est mis à jour lors des écritures et des lectures du cache. Ainsi, un document que vous lisez simplement reste conservé sans être réécrit. Le dossier correspondant au document actuellement ouvert est exempt de suppression, de sorte que l'élagage ne peut jamais supprimer des pages sous les yeux de la vue active

Chaque opération de disque dans le cache — génération d'empreinte, création de dossiers, mise à jour des marqueurs, lectures et écritures de pages, élagage — intercepte ses propres exceptions. C'est un choix délibéré et non de la négligence : le cache est un accélérateur non critique, et un disque plein, un dossier en lecture seule ou un verrouillage par antivirus doit simplement ramener le lecteur à son mode de fonctionnement sans cache, et non provoquer un échec de l'affichage de la page. Le pire résultat qu'une erreur d'E/S de cache puisse produire est une absence de correspondance

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // How many render passes did the disk cache save this session?
  Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;

Comment rendre un lecteur PDF sensible au DPI dans Delphi ?

TPDFlibViewer centralise chaque calcul dépendant du DPI via un champ unique, et cette conception à point de modification unique rend la prise en charge par moniteur gérable. Les codes de mise en page, de pré-chargement et de rendu lisent tous une valeur unique FScreenDPI ; à 100% de zoom, un point PDF correspond à ScreenDPI / 72 pixels. Le setter de la propriété publique ScreenDPI vide le cache de bitmaps, recrée la mise en page, met à jour les plages de défilement et invalide la fenêtre. Ainsi, la modification du DPI se résume à une simple affectation, chaque processus consommateur s'adaptant automatiquement. Les mêmes calculs de pixels s'appliquent lorsque vous effectuez un rendu pour une imprimante plutôt que pour un écran, un sujet abordé dans l'aperçu avant impression et le rendu du contexte de périphérique

La détection s'effectue dans une surcharge de CreateWnd plutôt que dans le constructeur, car GetDpiForWindow nécessite un descripteur de fenêtre (handle) valide et le constructeur s'exécute bien avant que celui-ci n'existe. Jusqu'au déclenchement de CreateWnd, le composant conserve la valeur héritée par défaut de 96 pour maintenir la cohérence des calculs de mise en page. GetDpiForWindow est une API Windows 10 1607+ ; le composant la lie donc dynamiquement avec GetProcAddress et se rabat sur GetDeviceCaps(LOGPIXELSY) sur les systèmes plus anciens — l'unité se charge ainsi partout, et les versions antérieures de Windows signalent simplement le DPI système

Que se passe-t-il lorsque la fenêtre est déplacée sur un écran 4K ?

Windows envoie le message WM_DPICHANGED lorsqu'une fenêtre prenant en charge le DPI par moniteur passe sur un écran ayant un facteur d'échelle différent, et TPDFlibViewer le traite directement. Le gestionnaire lit le nouveau DPI dans le mot de poids faible de wParam, l'affecte via SetScreenDPI — ce qui libère le cache bitmap devenu obsolète et réorganise les pages — puis accepte le rectangle de fenêtre suggéré transmis par Windows dans lParam via SetWindowPos, ce qui maintient la taille cliente logique stable lors du déplacement. L'utilisateur voit le document s'afficher à nouveau de manière nette à la nouvelle échelle, au lieu d'observer un bitmap étiré par le système d'exploitation

// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144;  // clears the bitmap cache and rebuilds layout

Limites à connaître avant le déploiement

Trois limites pratiques méritent d'être prises en compte dans votre revue de conception. Premièrement, WM_DPICHANGED n'est reçu que si le processus l'accepte : le manifeste de votre application doit déclarer la prise en charge du DPI par moniteur (per-monitor v2 sur les versions actuelles de Windows), ce qui correspond à l'option de prise en charge du DPI sous l'éditeur d'options d'application de Delphi. Un processus configuré uniquement pour le DPI système ne recevra jamais ce message, et le composant effectuera alors le rendu au DPI détecté lors de la création de la fenêtre. Deuxièmement, le cache disque échange de l'espace disque contre de la vitesse — stocker des bitmaps de pages pour dix documents à plusieurs niveaux de zoom consomme de l'espace. Orientez donc DiskCacheFolder vers un emplacement adéquat, et non vers un profil itinérant. Troisièmement, le dossier de cache nécessite des droits d'écriture pour l'utilisateur actif ; sans cela, le lecteur fonctionne silencieusement avec zéro correspondance — vérifiez DiskCacheHits lors des tests si vous suspectez que le cache ne s'active pas

Ensemble, ces deux fonctionnalités améliorent la qualité perçue d'une application manipulant de nombreux documents bien plus que la plupart des optimisations de rendu : les documents consultés à nouveau par vos utilisateurs s'ouvrent instantanément, et l'affichage reste net sur n'importe quel écran. Toutes deux sont proposées comme des propriétés ordinaires de TPDFlibViewer dans losLab PDF Library, faisant de leur adoption une simple configuration d'une après-midi plutôt qu'un projet complexe de moteur de rendu