Article technique

Cache disque et DPI par moniteur du visualiseur PDF Delphi

Deux plaintes suivent partout tout visualiseur PDF intégré : les gros documents se rendent de zéro à chaque lancement, et le texte devient flou dès que la fenêtre est traînée sur un moniteur haute densité. losLab PDF Library répond aux deux dans son contrôle TPDFlibViewer : la propriété DiskCacheFolder active un cache disque de pages entre sessions, indexé par empreinte de document, et la propriété ScreenDPI, adossée à GetDpiForWindow et à un gestionnaire WM_DPICHANGED, donne au contrôle une vraie conscience du DPI par moniteur

Les deux problèmes méritent d'être pris au sérieux parce que les utilisateurs les remarquent immédiatement. Un contrat numérisé de 400 pages qui mettait huit secondes à devenir défilable hier met encore huit secondes aujourd'hui, alors que rien n'a changé dans le fichier. Et sur un bureau à densités mixtes, un visualiseur qui rend aux 96 DPI du moniteur principal y paraît correct et visiblement mou sur le panneau 4K d'à côté. Cet article parcourt la façon dont TPDFlibViewer résout chacun des deux et, plus utile encore, pourquoi la conception a abouti là. Si le contrôle lui-même vous est nouveau, commencez par la présentation du contrôle de visualisation PDF interactif et revenez pour la couche de performance

Comment mettre en cache des pages PDF rendues entre sessions en Delphi ?

Affectez un dossier à TPDFlibViewer.DiskCacheFolder et le contrôle persiste les bitmaps de pages rendues sur le disque, si bien qu'un document consulté récemment se rouvre instantanément au lieu d'être rendu à nouveau. Sur le disque, la disposition 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 aucune API de cache à appeler et rien à vider

Diagramme du cache disque de pages entre sessions de TPDFlibViewer avec un sous-dossier par empreinte de document et un fichier bitmap par page et clé de zoom, alimentant les issues succès et échec
Affectez DiskCacheFolder une fois et les pages rendues persistent entre les sessions, incrémentant DiskCacheHits chaque fois qu'un bitmap stocké épargne une passe de rendu
uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // Une ligne active le cache de pages entre sessions
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

Le compteur DiskCacheHits rapporte combien de rendus de pages le cache disque a épargnés dans la session en cours, ce qui rend le gain mesurable plutôt qu'anecdotique. Sur un document que vous avez fermé puis rouvert, chaque page qui entre dans la vue sans passe de rendu l'incrémente

Pourquoi la clé de cache est une empreinte, pas un numéro de version

L'empreinte de document est un hachage FNV-1a 64 bits sur la chaîne path|size|last write time, formaté en seize chiffres hexadécimaux. Ce triplet est toute la stratégie d'invalidation : quand le fichier est modifié, sa taille ou son heure d'écriture change, le hachage change, et le visualiseur ouvre simplement un autre sous-dossier de cache. Le dossier périmé n'est plus jamais consulté et finit par tomber sous la purge LRU. Il n'y a pas de protocole d'étiquette de version à maintenir, pas de logique de comparaison d'horodatages qui puisse dériver, et aucun moyen pour un ancien bitmap d'être servi face à un document modifié

C'est la même idée d'identité de contenu comme clé de cache que celle qu'emploient les systèmes de build, et elle gagne son salaire par les modes de défaillance qu'elle supprime. Un cache qui range les fichiers sous le nom simple du document doit détecter les modifications explicitement, et chaque vérification explicite est un bug qui attend son cas limite : un fichier restauré depuis une sauvegarde avec un ancien horodatage, un enregistrement qui préserve la taille, un chemin comparé en tenant compte de la casse sur une machine et pas sur une autre. Hacher les trois signaux dans le nom du dossier rend la péremption structurellement impossible plutôt que vérifiée par procédure

Diagramme PDF Library for Delphi de l'empreinte FNV-1a bâtie à partir du chemin, de la taille et de l'heure de dernière écriture servant de nom de dossier de cache, si bien qu'un document modifié passe dans un sous-dossier neuf et que les bitmaps périmés ne peuvent jamais être servis
Une modification déplace le triplet haché, la clé passe dans un sous-dossier neuf, et le dossier abandonné vieillit jusqu'à la purge LRU

Pourquoi la rotation est délibérément absente 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 en est intentionnellement absent. Le cache disque stocke toujours les octets du rendu non pivoté ; quand une page en cache est chargée alors que la vue est pivotée, les pixels sont réorientés en mémoire après décodage. Si la rotation faisait partie de la clé sur disque, un utilisateur parcourant les quatre angles de vue écrirait quatre fois la même page sur le disque, quadruplant la croissance du cache pour zéro information supplémentaire, puisque la rotation est un brassage de pixels bon marché comparé à une passe de rendu complète

Le cache de bitmaps en mémoire utilise bien une clé composite, Round(Zoom * 1000) * 4 + Rotation div 90, car là le bitmap pivoté est exactement ce dont le gestionnaire de peinture a besoin. Le fil de préchargement en arrière-plan suit la même discipline : il rend toujours des pages non pivotées et rapporte des clés de zoom simples, et le fil principal applique la rotation en vidant les résultats — la même règle du cache détenu par le fil principal que celle évoquée dans le rendu parallèle de pages et la sûreté des threads. Tenir la rotation à l'écart de tout ce qui est sous la couche mémoire garantit un rendu canonique unique par page et par zoom, partout où le stockage coûte cher

Comment la purge LRU garde-t-elle le dossier de cache borné ?

Le cache retient jusqu'à dix dossiers de documents et purge les moins récemment utilisés quand de nouveaux documents arrivent. La récence est suivie par un fichier last-used.marker dans chaque dossier de document, dont l'horodatage est touché aussi bien aux écritures qu'aux lectures de cache, si bien qu'un document simplement relu reste au chaud sans être réécrit. Le dossier du document actuellement ouvert est exempté de suppression, la purge ne peut donc jamais retirer des pages sous la vue active

Chaque opération disque du cache — empreinte, création de dossier, touche du marqueur, lectures et écritures de pages, purge — avale ses propres exceptions. C'est une posture délibérée, pas de la négligence : le cache est un accélérateur au mieux, et un disque plein, un dossier en lecture seule ou un verrou d'antivirus doivent dégrader le visualiseur vers "il rend comme avant", jamais vers "il n'affiche pas la page". Le pire résultat qu'une erreur d'entrée-sortie de cache puisse produire est un succès manqué

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Combien de passes de rendu le cache disque a-t-il épargnées cette session ?
  Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;

Comment rendre un visualiseur PDF conscient du DPI en Delphi ?

TPDFlibViewer fait passer chaque calcul dépendant du DPI par un champ unique, et cette conception à point de changement unique est ce qui rend la prise en charge par moniteur abordable. Le code de mise en page, de préchargement et de rendu lit tous une seule valeur FScreenDPI ; à 100 % de zoom, un point PDF correspond à ScreenDPI / 72 pixels. Le mutateur public de la propriété ScreenDPI vide le cache de bitmaps, reconstruit la mise en page, met à jour les plages de défilement et invalide la fenêtre, si bien que changer de DPI tient en une affectation que tous les consommateurs suivent automatiquement. Le même calcul en pixels compte quand vous rendez pour une imprimante plutôt que pour un moniteur, sujet traité dans l'aperçu avant impression et le rendu sur contexte de périphérique

La détection a lieu dans une surcharge de CreateWnd plutôt que dans le constructeur, car GetDpiForWindow a besoin d'un handle de fenêtre valide et le constructeur s'exécute bien avant qu'il n'en existe un. Jusqu'au déclenchement de CreateWnd, le contrôle conserve la valeur héritée de 96 pour que le calcul de mise en page reste sain. GetDpiForWindow est elle-même une API Windows 10 1607 et suivantes, le contrôle la lie donc dynamiquement avec GetProcAddress et retombe sur GetDeviceCaps(LOGPIXELSY) sur les systèmes plus anciens — l'unité se charge toujours partout, et les Windows plus anciens rapportent simplement le DPI système

Que se passe-t-il quand la fenêtre passe sur un moniteur 4K ?

Windows envoie WM_DPICHANGED quand une fenêtre consciente du DPI par moniteur passe sur un moniteur au facteur d'échelle différent, et TPDFlibViewer le traite directement. Le gestionnaire lit le nouveau DPI dans le mot bas de wParam, l'affecte via SetScreenDPI — ce qui abandonne le cache de bitmaps devenu faux et remet les pages en page — puis accepte le rectangle de fenêtre suggéré que Windows passe dans lParam via SetWindowPos, ce qui garde la taille cliente logique stable au fil du déplacement. L'utilisateur voit le document se rendre nettement à la nouvelle échelle au lieu d'un bitmap étiré par le système

Diagramme du flux WM_DPICHANGED qui redonne un rendu net à un visualiseur PDF Delphi lorsque sa fenêtre atterrit sur un moniteur à plus haut DPI, avec les exigences de déclaration dans le manifeste et de détection dans CreateWnd
Traiter WM_DPICHANGED abandonne le cache de bitmaps, reconstruit la mise en page et adopte le rectangle suggéré pour que le texte reste net sur le moniteur où atterrit la fenêtre
// Normalement vous ne touchez jamais à ScreenDPI : CreateWnd détecte le moniteur hôte
// et WM_DPICHANGED suit les déplacements. Ne le forcez que pour des cibles spéciales,
// par exemple rendre la mise en page comme pour un affichage à 150 % :
FViewer.ScreenDPI := 144;  // vide le cache de bitmaps et reconstruit la mise en page

Des limites à connaître avant de livrer

Trois limites pratiques méritent leur place dans votre revue de conception. D'abord, WM_DPICHANGED n'arrive que si le processus y adhère : le manifeste de votre application doit déclarer la conscience du DPI par moniteur (per-monitor v2 sur les Windows actuels), ce qui dans l'IDE Delphi correspond au réglage de conscience du DPI sous les options d'application. Un processus conscient seulement du DPI système ne reçoit jamais le message, et le contrôle rend alors au DPI détecté à la création de la fenêtre. Ensuite, le cache disque échange de l'espace disque contre de la vitesse — dix documents de bitmaps de pages sur plusieurs niveaux de zoom représentent du stockage réel, alors pointez DiskCacheFolder vers un emplacement que vous acceptez de voir grossir, pas un profil itinérant. Enfin, le dossier de cache a besoin de la permission d'écriture pour l'utilisateur courant ; sans elle, le visualiseur fonctionne quand même, en silence, avec zéro succès — vérifiez DiskCacheHits pendant vos tests si vous soupçonnez que le cache ne se met pas en marche

Ensemble, les deux fonctionnalités changent la qualité perçue d'une application riche en documents plus que la plupart des optimisations de rendu : les documents que vos utilisateurs reconsultent s'ouvrent instantanément, et la vue reste nette sur le moniteur où elle atterrit. Les deux se livrent comme de simples propriétés de TPDFlibViewer dans losLab PDF Library, si bien que les adopter représente une après-midi de câblage plutôt qu'un projet de moteur de rendu