Article technique

File d'attente de rendu PDF en arrière-plan en Delphi avec HotPDF

La classe THPDFBackgroundRenderer de HotPDF est un descendant de TThread qui rend les pages PDF chargées en bitmaps sur un thread de travail, de sorte qu'une visionneuse Delphi puisse continuer à défiler et à se redessiner pendant qu'une page est encore en train d'être rastérisée en arrière-plan. THPDFBackgroundRenderer.RequestPage met en file un index de page pour ce thread de travail, CancelAll abandonne tout ce qui attend encore, et GetCachedBitmap renvoie un bitmap terminé dont l'appelant devient propriétaire et qu'il doit libérer. Faites défiler un contrat scanné de deux cents pages à résolution d'impression sur le seul thread d'interface, et chaque changement de page met en pause la fenêtre jusqu'à ce que GDI termine de la dessiner — c'est exactement ce bégaiement que THPDFBackgroundRenderer existe pour éliminer

Pourquoi rendre les pages PDF sur un thread en arrière-plan ?

Un thread en arrière-plan justifie sa complexité parce que le moteur de rendu de page de HotPDF est un véritable interpréteur de flux de contenu, et non une copie de bitmap bon marché qui revient avant que quiconque ne s'en aperçoive : il parcourt les opérateurs PDF, maintient une pile d'état graphique, et rastérise les chemins, images et glyphes via GDI, le même moteur couvert dans le rendu des pages PDF chargées vers un TBitmap. Exécuter ce travail de façon synchrone dans un gestionnaire de défilement ou de peinture arrête la pompe de messages jusqu'à ce que l'appel revienne, ce qu'est exactement une fenêtre gelée. Insérer Application.ProcessMessages dans l'appel de rendu ne corrige pas cela : cela permet à la file de messages de se vider, mais le rendu lui-même occupe toujours le thread appelant, si bien que la fenêtre redessine du contenu périmé plus vite alors que le vrai travail n'a pas avancé d'un pouce. Le seul moyen de garder une visionneuse réactive pendant un rendu véritablement lent est d'exécuter ce rendu ailleurs, ce qui explique pourquoi THPDFBackgroundRenderer existe comme sous-classe de TThread plutôt que comme un callback ou un timer

Mettre en place une file de requêtes pour une visionneuse à défilement

THPDFBackgroundRenderer.Create prend l'instance THotPDF chargée et un DPI qui reste fixe pendant toute la durée de vie de ce moteur de rendu, si bien que chaque page mise en file via une instance se rend à une seule résolution ; une visionneuse qui prend en charge le zoom a besoin d'un nouveau moteur de rendu, pas d'une nouvelle propriété DPI, chaque fois que le niveau de zoom change. RequestPage ajoute un index de page à une file interne et revient immédiatement : elle n'effectue aucun rendu elle-même et ne touche jamais le thread d'interface. Execute, le point d'entrée hérité de TThread que HotPDF exécute une fois que vous appelez Start, retire un index à la fois du début de cette file, le rend via le cache de pages du document, et stocke une copie indexée par page afin que GetCachedBitmap puisse la restituer plus tard

type
  TViewerForm = class(TForm)
    RenderPollTimer: TTimer;
    procedure RenderPollTimerTimer(Sender: TObject);
  private
    FDoc: THotPDF;
    FRenderer: THPDFBackgroundRenderer;
    FPendingPage: Integer;
    procedure RequestPageWindow(CenterPage: Integer);
  end;

procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
  I: Integer;
begin
  if FRenderer <> nil then
  begin
    FRenderer.CancelAll;
    FRenderer.Free;
  end;
  FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
  for I := CenterPage - 1 to CenterPage + 1 do
    if (I >= 0) and (I < FDoc.LoadedPageCount) then
      FRenderer.RequestPage(I);
  FPendingPage := CenterPage;
  FRenderer.Start;
end;

procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  if FRenderer = nil then Exit;
  Bmp := FRenderer.GetCachedBitmap(FPendingPage);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

GetCachedBitmap renvoie nil tant que la copie de cette page n'est pas prête, si bien qu'un schéma d'interrogation par minuterie comme celui ci-dessus suffit ; il n'existe pas d'événement de disponibilité séparé à câbler, HotPDF résout cela avec une simple vérification de nil plutôt qu'une API de notification plus lourde. La section suivante couvre ce que font réellement CancelAll et cet appel à Free, car les deux comptent une fois que les pages commencent à se rendre dans le désordre ou qu'un défilement se produit plus vite que la file ne peut se vider

Le raccourci en un appel pour une seule page

THotPDF.RenderLoadedPageToBitmapAsync existe pour le cas courant consistant à lancer exactement une page sans toucher directement à THPDFBackgroundRenderer : il construit le moteur de rendu en interne, appelle RequestPage une fois, démarre le thread, et renvoie la référence TThread à l'appelant, qui en devient propriétaire et est responsable de la libérer. Récupérer le résultat passe par THotPDF.GetLoadedCachedRenderedBitmap plutôt que par le GetCachedBitmap propre au moteur de rendu, car GetLoadedCachedRenderedBitmap lit le cache partagé du document, indexé par index de page et par DPI, le même cache que RenderLoadedPageToBitmapCached et le préchargeur intégré alimentent déjà — une page déjà rendue par une autre partie de la visionneuse à ce DPI peut revenir immédiatement, avant même que le thread en arrière-plan qui vient de démarrer n'ait été planifié par l'OS

// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
  if FAsyncWorker <> nil then
    FAsyncWorker.Free; // waits if a prior page is still rendering
  FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
  FPendingPage := PageIndex;
end;

procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

Peut-on annuler une page déjà mise en file ?

CancelAll ne supprime que les tâches encore présentes dans la file ; une page que HotPDF a déjà retirée de la tête de file et transmise à son appel de rendu continue jusqu'à son terme, car THPDFBackgroundRenderer n'a aucun mécanisme pour interrompre un travail déjà en cours. C'est un compromis raisonnable en pratique — le rendu d'une seule page est rarement assez long pour justifier la complexité supplémentaire de la préemption — mais un défilement rapide qui déclenche CancelAll à chaque événement de défilement paie tout de même le coût de la page unique qui était en cours de rendu au moment de chaque annulation. La référence officielle est directe à ce sujet : un rendu déjà en cours peut se terminer avant que le thread ne se termine

Execute a un second comportement, facile à manquer : la boucle se termine dès qu'elle trouve la file vide, elle ne reste pas inactive à attendre du travail supplémentaire. Une instance de THPDFBackgroundRenderer est donc un travailleur par lot à usage unique, pas un service d'arrière-plan persistant — mettez en file une poignée de pages, appelez Start, et une fois la dernière page en file rendue, le thread OS sous-jacent se termine de lui-même. Rappeler RequestPage sur cette même instance après qu'Execute a déjà vidé la file ne la redémarre pas, ce qui explique précisément pourquoi RequestPageWindow ci-dessus remplace l'instance du moteur de rendu à chaque appel plutôt que d'essayer de continuer à alimenter un seul objet à longue durée de vie

Est-il sûr de toucher un TBitmap depuis un thread en arrière-plan en Delphi ?

Toucher un TBitmap depuis un thread en arrière-plan est sûr dans la conception de HotPDF tant qu'un seul thread à la fois opère sur une instance de bitmap donnée, et THPDFBackgroundRenderer fait respecter cette limite au lieu de la laisser à la charge de l'appelant. Execute rend chaque page à l'intérieur du verrou de rendu propre au document, la même section critique que chaque appel à RenderLoadedPageToBitmapCached et le préchargeur intégré PrefetchLoadedPages partagent déjà, si bien que le dessin GDI réel pour une page donnée se produit sur exactement un thread à la fois et ne chevauche jamais un autre rendu de ce même document. Le bitmap résultant est un objet appartenant au thread de travail que THPDFBackgroundRenderer ne publie jamais directement à un appelant

GetCachedBitmap alloue à la place un tout nouveau TBitmap et appelle Assign dessus sous le verrou propre et séparé du moteur de rendu, si bien que la copie se produit toujours pendant qu'Execute est bloqué de remplacer cet emplacement de cache en dessous — le thread appelant reçoit des données de pixels, jamais le handle original. Cette séparation est aussi la raison de résister à la tentation d'écrire soi-même un thread de rendu personnalisé qui appellerait directement les fonctions de rendu de HotPDF sans passer par THPDFBackgroundRenderer ou PrefetchLoadedPages : deux rendus en concurrence sur les caches partagés et le graphe d'objets du même document chargé sont précisément le scénario que le verrouillage interne de HotPDF existe pour empêcher, et la classe de moteur de rendu en arrière-plan vous procure ce verrouillage gratuitement plutôt que de vous obliger à le réimplémenter

En quoi cela diffère-t-il du préchargement de page intégré de HotPDF ?

PrefetchLoadedPages et THPDFBackgroundRenderer résolvent des problèmes apparentés mais différents : PrefetchLoadedPages, étant donné une plage de pages, rend automatiquement tout ce voisinage dans le cache partagé du document sur son propre thread de travail, sans objet de file à créer ou à gérer pour l'appelant. THPDFBackgroundRenderer échange cette automatisation contre le contrôle — l'appelant décide exactement quels index de page comptent et dans quel ordre, et peut annuler ceux encore en file sans toucher à la plage que le préchargeur intégré réchauffe éventuellement ailleurs. Les deux passent par le même verrou de rendu, si bien qu'une visionneuse peut exécuter PrefetchLoadedPages pour le cas ordinaire des quelques pages suivantes et ne recourir à THPDFBackgroundRenderer que lorsque quelque chose en dehors de ce schéma se présente, comme une bande de vignettes sautant directement à une page que l'utilisateur vient de cliquer

begin
  // PrefetchLoadedPages takes a 1-based "start-end" range string, while
  // RequestPage below stays 0-based like every other loaded-page index.
  Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);

  // Reach for THPDFBackgroundRenderer only for a page outside that
  // window, such as a thumbnail the user just clicked.
  FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
  FRenderer.RequestPage(ClickedThumbnailPage);
  FRenderer.Start;
end;

Deux détails de cycle de vie méritent d'être retenus pour du code de production. Le cache à l'échelle du document derrière RenderLoadedPageToBitmapCached est borné par RenderCacheCapacity, huit pages par défaut, et évince l'entrée la moins récemment utilisée une fois plein, mais la propre liste de résultats d'une instance de THPDFBackgroundRenderer n'a pas une telle limite — elle conserve un bitmap par index de page distinct jamais demandé via cette instance jusqu'à ce que l'instance elle-même soit libérée, si bien qu'un moteur de rendu maintenu en vie pendant toute une session de défilement à haut DPI accumulera avec plaisir un bitmap pleine résolution par page défilée. HotPDF n'annule pas non plus automatiquement un moteur de rendu créé par l'appelant de la manière dont il annule son propre préchargeur avant qu'un document ne se charge ou ne se détruise, puisqu'une instance de THPDFBackgroundRenderer n'est jamais enregistrée sur l'objet THotPDF qu'elle référence — le code appelant doit donc annuler et libérer chaque moteur de rendu construit contre un document avant de recharger ou de libérer ce document, la même discipline d'ordonnancement que HotPDF applique en interne à PrefetchLoadedPages

THPDFBackgroundRenderer est un élément de la façade de document chargé derrière l'architecture de visionneuse MVC de HotPDF, et il s'associe naturellement aux flux de travail au niveau fichier de l'API de fichier direct pour les PDF volumineux lorsque le document que l'on fait défiler est lui-même trop volumineux pour être chargé de façon désinvolte en premier lieu. Le rendu en arrière-plan, les files de requêtes, et le cache de rendu décrits ici font tous partie du composant HotPDF standard pour Delphi et C++Builder