Maintenez le bouton de zoom enfoncé dans un visualiseur PDF naïf et regardez la courbe du processeur. Une seule pression sur une commande de zoom à répétition automatique déclenche une douzaine de pas de zoom par seconde, et si chaque pas lance un nouveau rendu pleine qualité de la page visible, les rendus s'empilent plus vite qu'ils ne s'achèvent. La page se rastérise très bien isolément, peut-être 180 ms pour un scan A4, mais vous faites désormais tourner une douzaine de rendus de 180 ms sur un travail que l'utilisateur a déjà dépassé. Le visualiseur se fige, un cœur reste collé à 100 %, et le temps que l'écran rattrape son retard, l'utilisateur s'est arrêté à un niveau de zoom vieux de quatre rendus. Le remède n'est pas un rastériseur plus rapide. C'est un cache qui renvoie instantanément des pages finies et une boucle de rendu prête à abandonner un travail dès qu'il devient périmé
PDFium Component vous tend les pièces des deux et reste en dehors de la politique. Vous obtenez des bitmaps possédés par l'appelant, un moteur de rendu progressif qui prend un jeton d'annulation, des modes d'ajustement qui recalculent le zoom au redimensionnement, et un appel de tuilage pour les pages trop grandes à rastériser d'un bloc. Ce qu'il ne fournit délibérément pas, c'est le cache lui-même, parce que la bonne politique d'éviction dépend de votre zone d'affichage, du plafond mémoire de votre plateforme et de la manière dont vos utilisateurs font défiler. Cette décision vous revient, et les conséquences d'une erreur sont exactement le gel et la fuite
Où passent les millisecondes et les mégaoctets
Chiffrez le coût avant de concevoir quoi que ce soit. Une page A4 à 96 DPI fait environ 794 sur 1123 pixels, à peu près 3,5 Mo en bitmap 32 bits. Zoomez à 200 % et cela quadruple. À 400 % sur un écran haute densité, vous allouez et remplissez un seul bitmap de page de 50 à 60 Mo, et un visualiseur à défilement continu garde plusieurs pages vivantes à la fois. Le coût de rastérisation suit les pixels de sortie : chaque doublement du zoom quadruple donc en gros à la fois le temps de rendu et la mémoire
Deux conséquences tombent directement de cette arithmétique. Un cache dont la clé ignore le niveau de zoom ne vaut rien, car le geste même qu'il doit accélérer, le zoom, produit un nouveau bitmap à chaque fois. Et un cache sans borne épuisera l'espace d'adressage d'un processus 32 bits précisément sur les documents où les gens zooment le plus fort : scans denses de titres de propriété, plans techniques, cartes grand format. Le cache doit être correctement clé et fermement plafonné, et aucun des deux n'est facultatif
Ce qui doit figurer dans la clé de cache
Un bitmap mis en cache n'est réutilisable sans risque que si toutes les entrées qui ont façonné ses pixels correspondent encore. Cela veut dire le numéro de page, le zoom effectif (ou, ce qui revient au même, les dimensions de sortie en pixels), la rotation, la résolution du moniteur et les options de rendu en vigueur au moment de sa production. Une page rendue avec reAnnotations est une image différente de la même page sans elles, et une passe en niveaux de gris via reGrayscale en est encore une autre. Retirez l'un de ces éléments de la clé et les bugs sont prévisibles : une surcouche d'annotation qui traîne après qu'un relecteur a supprimé le commentaire, ou une page qui devient floue à l'instant où un utilisateur fait glisser la fenêtre de la dalle du portable vers un écran 4K externe et où la résolution change sous un bitmap périmé
function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
Key: string;
begin
Key := Format('%d|%.0f|%d|%d|%d',
[PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
if FBitmaps.TryGetValue(Key, Result) then
Exit;
Pdf.PageNumber := PageNo;
Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
OutputHeight(PageNo, ZoomPct), Rotation, Opts);
FBitmaps.Add(Key, Result); // le cache possède désormais ce bitmap
end;
En cas de succès, cela revient en microsecondes, et c'est tout l'intérêt. La question plus difficile est de savoir ce qu'il advient des bitmaps qui sortent du cache, et cela se révèle être une question de propriété
Qui libère le bitmap
La forme fonction de RenderPage renvoie un TBitmap dont l'appelant est propriétaire. Dans un export ponctuel, cette propriété est évidente et facile à honorer. À l'intérieur d'un cache, elle devient la fuite la plus courante des visualiseurs PDF en Delphi, car le dictionnaire détient maintenant l'unique référence de chaque bitmap, et un simple TDictionary ne libère clés et valeurs pour vous que s'il s'agit de types gérés. Un TBitmap n'en est pas un. Évincez une entrée sans appeler Free et les pixels restent alloués sans que rien ne pointe dessus
La raison pour laquelle cela passe à travers les mailles est temporelle. Un test de dix minutes ne zoome jamais assez de pages distinctes pour s'en apercevoir ; la fuite ne se montre qu'après que quelqu'un a fait défiler et zoomé un long document pendant deux heures, moment où le processus détient des centaines de bitmaps de page orphelins et où la machine se met à paginer. C'est pourquoi l'éviction a sa place dans la première version du cache, pas dans une version ultérieure. Plafonnez le cache par octets estimés, calculés comme largeur fois hauteur fois quatre, évincez selon le moins récemment utilisé les pages hors de la zone d'affichage et de la fenêtre de préchargement, et libérez chaque bitmap au moment où vous le retirez. Pour les dessins réellement passagers, les surcharges qui rendent dans un TBitmap fourni par l'appelant ou directement sur un HDC vous permettent de sauter entièrement la danse de la propriété. L'aperçu avant impression est le cas évident, puisque vous rendez chaque feuille une fois et que la mettre en cache n'apporte rien
Rendu progressif et annulation honnête
Les surcharges simples de RenderPage bloquent jusqu'à ce que la page soit finie, ce qui est exactement le comportement dont vous ne voulez pas pendant que l'utilisateur bouge encore la commande de zoom. Pour cela, vous vous tournez vers RenderPageProgressive. Il prend un IPdfCancellationToken et renvoie l'un de prsDone, prsCancelled ou prsFailed. Le détail de comportement qui piège est que l'annulation n'est pas instantanée. Le jeton est interrogé aux frontières de fragments à l'intérieur du rendu : un jeton que vous signalez au milieu d'un fragment ne prend donc effet qu'une fois ce fragment terminé. Sur une page complexe, la latence entre la demande et l'arrêt se compte en dizaines de millisecondes. Concevez autour de cet écart plutôt que d'espérer le voir disparaître : annulez le jeton précédent dès qu'une nouvelle valeur de zoom arrive, mais ne supposez pas que l'ancien rendu s'arrête à l'instant où vous le lui demandez
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // abandonne le rendu précédent encore en vol
FTokenSource := TPdfCancellationTokenSource.New; // unité FPdfAsync
Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
ro0, [reAnnotations]);
case Status of
prsDone: PresentBackBuffer;
prsCancelled: ; // supplanté par une demande plus récente : ignorer
prsFailed: ShowRenderFailure;
end;
end;
Pendant l'interaction, prsCancelled est le résultat normal, pas l'exception. La plupart des rendus qu'un geste de zoom démarre seront supplantés avant de finir : traitez donc l'annulation comme une routine et abandonnez le résultat en silence. Une file de rendu qui consigne chaque annulation comme un avertissement enterrera l'unique échec qui compte vraiment sous des milliers de lignes de bruit. Pour que l'écran n'ait pas l'air mort pendant que le vrai rendu tourne, associez au chemin progressif un remplaçant bon marché : mettez à l'échelle le bitmap précédemment mis en cache au nouveau zoom et présentez-le immédiatement. Cela paraît flou pendant une centaine ou deux de millisecondes, mais cela se lit comme instantané, et cela achète au rendu pleine qualité le temps dont il a besoin, soit pour finir, soit pour être annulé par le geste suivant
Le mode d'ajustement que le zoom coupe discrètement
La propriété FitMode d'un visualiseur, posée à pfmFitPage ou pfmFitWidth, recalcule le zoom à chaque redimensionnement pour que la page continue de tenir quand la fenêtre change. Le piège est qu'affecter Zoom directement remet FitMode à pfmNone. Comme comportement par défaut, c'est correct : un utilisateur qui a délibérément tapé 150 % ne veut pas que le prochain redimensionnement de fenêtre le jette. Mais cela surprend quiconque câble un bouton de zoom avant comme Zoom := Zoom * 1.25 et ne comprend ensuite pas pourquoi l'ajustement à la largeur a cessé de répondre après le premier clic. Si votre barre d'outils propose à la fois le zoom explicite et les modes d'ajustement, vous devez vous-même mémoriser le dernier choix d'ajustement de l'utilisateur et le réaffecter quand il represse le bouton correspondant. Le composant ne restaurera pas un mode qu'une affectation de zoom vient d'effacer, et ce n'est pas son rôle
Un budget mémoire défendable
Un budget que vous pouvez écrire est un budget que vous pouvez défendre en revue de code : partez donc d'un scénario concret. Disons qu'un défilement continu garde la page visible plus une page préchargée au-dessus et au-dessous, à côté d'une bande de vignettes. À 100 % sur un écran à 96 DPI, ces trois bitmaps pleine taille font environ 3,5 Mo chacun, ce qui n'est rien. À 300 % sur un écran 4K, les mêmes trois bitmaps font à peu près 30 Mo chacun, et c'est avant que le cache n'ait retenu la moindre page historique. La croissance est dans le geste, pas dans le document
Un défaut solide pour un processus Delphi 32 bits est un budget de bitmaps de 256 Mo sous éviction LRU. En 64 bits vous pouvez l'échelonner sur la RAM physique, mais gardez un plafond dur quoi qu'il arrive, car l'échec dont vous vous prémunissez n'est pas le plantage de votre processus. C'est toute la machine qui s'épuise sur son fichier d'échange pendant que votre visualiseur continue techniquement de tourner et que l'utilisateur se demande pourquoi tout le reste a ralenti. Un plafond dur échoue de façon prévisible ; un cache sans borne échoue en emportant le bureau avec lui. Les vignettes méritent leur propre traitement : rendez chacune une fois à sa petite taille cible et gardez-la dans un pool séparé auquel la logique LRU ne touche jamais. Régénérer une vignette de 120 pixels en réduisant un bitmap pleine page de 60 Mo est la façon la plus gaspilleuse possible de produire un timbre-poste
Certaines pages seules mettent en échec n'importe quel budget. Un plan technique de format E ou une grande carte rendus entiers à 400 % représentent une allocation de plusieurs centaines de mégaoctets, et aucune politique d'éviction ne rend cela acceptable. La réponse est alors de cesser de rendre des pages entières. RenderTile ne rastérise que la région située au décalage en pixels (Left, Top) à l'intérieur d'une page théoriquement mise à l'échelle sur PageWidth par PageHeight : vous ne rendez donc que le rectangle visible plus une marge d'une tuile autour pour un panoramique fluide, et vous repliez les décalages de tuile dans la clé de cache à côté du zoom. Gardez des dimensions de tuile fixes sur tout le document. Des tuiles fixes signifient qu'un changement de résolution invalide proprement toute la grille, là où des tuiles variables vous laissent courir après des coutures visibles entre des régions rendues à des échelles légèrement différentes
Deux fonctions voisines alourdissent discrètement tout cela. Les passes de filtre couleur comme les niveaux de gris ou l'inversion s'exécutent après le rendu et produisent à chaque fois un second bitmap pleine taille, doublant l'empreinte par page de toute vue qui les utilise ; ce coût fait l'objet du filtrage couleur pour malvoyants dans les visualiseurs PDF Delphi. Et un visualiseur qui surligne les mots pendant la synthèse vocale invalide la vue rendue à chaque mot prononcé, si bien que l'interaction entre les redessins de surlignage et le débit de parole compte plus qu'il n'y paraît, comme le couvre le surlignage TTS mot à mot
Les surcharges de rendu, les codes d'état progressifs et le composant visualiseur lui-même sont documentés sur la page produit de PDFium Component