Article Technique

Créer un lecteur PDF interactif dans Delphi avec TPDFlibViewer

L'intégration d'un panneau de lecture PDF dans une application métier Delphi ne nécessite pas l'écriture d'une boucle de rendu, d'un moteur de défilement ou d'un modèle de zoom. losLab PDF Library fournit TPDFlibViewer, un composant VCL qui ouvre un document en un seul appel et intègre par défaut la sélection de texte, le panoramique, le zoom rectangle, les captures instantanées, la coloration de régions, la mise en valeur des résultats de recherche textuelle, l'interaction avec les annotations et l'impression. Cet article détaille les cinq étapes permettant de transformer une fiche vide en un lecteur fonctionnel : déposer le composant, connecter les actions prédéfinies, ajouter la coloration de recherche, gérer les clics sur les annotations et imprimer

Pourquoi utiliser un composant de visualisation plutôt que d'écrire une boucle de rendu ?

L'argument de la superposition des couches vient en premier, car il explique ce que vous obtenez réellement. Le rendu d'une page sur un bitmap représente la partie facile (20 pour cent) ; les 80 pour cent restants concernent tout ce qui l'entoure : un cache de bitmaps de pages avec un budget d'éviction, la gestion du défilement pour des pages de tailles différentes, des modes de zoom qui recalculent à la redéfinition de taille, des tests de collision (hit-testing) qui convertissent un pixel de souris en coordonnées de page à n'importe quel niveau de zoom ou de rotation, et une planification de rafraîchissement sans scintillement. TPDFlibViewer gère tout cela et délègue le travail de rastérisation au même moteur décrit dans l'article sur le rendu multi-moteur, de sorte que la couche de contrôle et la couche de rendu restent remplaçables séparément. Le code de votre fiche communique avec un contrôle Windows défilable, et non avec un moteur de rendu

Comment intégrer un lecteur PDF dans une application Delphi ?

TPDFlibViewer.LoadFromFile ouvre un document via l'instance interne TPDFlib du composant et renvoie 1 en cas de succès. Ainsi, le lecteur minimal se résume à un appel de constructeur, une affectation de Parent et un chargement. Si vous disposez déjà d'une instance de TPDFlib ailleurs dans l'application, AttachLibrary affiche à la place son document sélectionné, tout en conservant la propriété de votre côté

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

La propriété ViewerMode détermine l'action de la souris parmi cinq valeurs : vmSelect est l'outil de sélection de texte par défaut, vmHand permet le panoramique par glissement avec un curseur de saisie tandis qu'un simple clic suit toujours les liens, vmZoom is a marquee zoom that magnifies the dragged band to fill the viewport, vmSnapshot capture une région sélectionnée dans le presse-papiers (avec un événement OnSnapshot et un commutateur SnapshotToClipboard), et vmHighlight dessine des colorations de région translucides persistantes. L'outil de coloration mérite une remarque concernant les coordonnées : les régions sont stockées dans l'espace utilisateur de la page, et non en pixels clients. Ainsi, les marques de AddHighlightRegion survivent au zoom, à la rotation de la vue et à la navigation dans les pages sans aucune gestion de votre part. Dans une vue pivotée, la direction de glissement peut même aller à l'encontre des axes de la page, ce qui produit une largeur ou une hauteur négative après la conversion de rotation inverse — le contrôle normalise le rectangle pour vous

Comment connecter une barre d'outils PDF dans Delphi sans code de liaison ?

L'unité PDFlibViewerActions élimine complètement le code de liaison de la barre d'outils. Elle fournit vingt descendants de TAction prêts à l'emploi couvrant la navigation dans les pages (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), le zoom (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), la rotation de la vue (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), l'impression, les boîtes de dialogue d'ouverture et d'enregistrement sous, la copie du texte sélectionné, et une action de bascule par outil de souris (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode). Chaque action pointe vers un contrôle via sa propriété publiée Viewer et maintient son propre état : chaque action se désactive lorsqu'aucun document n'est ouvert, les actions de navigation suivent la position de la page, la copie détecte si une sélection existe, et les actions de mode et d'ajustement synchronisent leur état coché avec l'outil et le mode de zoom actifs. Lors de la conception, elles s'enregistrent toutes dans la catégorie losLab PDF de l'éditeur Action List ; à l'exécution, ces mêmes classes ne nécessitent que trois lignes chacune

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

Un piège lié à la portée des unités mérite d'être connu si vous construisez votre propre enregistrement d'actions sur la base de ce modèle. RegisterActions est déclaré dans System.Actions, et non dans Vcl.ActnList. L'erreur de compilation obtenue avec uniquement Vcl.ActnList dans la clause uses est particulièrement trompeuse : le littéral de tableau de classes [TAction1, TAction2, ...] est interprété par défaut comme un ensemble, ce qui génère une série d'erreurs E2010 Incompatible types: 'Integer' and 'class of ...' semblant indiquer une incompatibilité de type dans vos classes d'actions. La correction consiste en un simple ajout dans la clause uses, bien que le diagnostic pointe partout sauf sur celui-ci. Pour les barres d'outils de zoom, TPDFlibViewer.EffectiveZoomPercent indique le zoom réellement appliqué — y compris le facteur calculé dans les modes d'ajustement en largeur et en page — de sorte qu'une étape de zoom avant puisse se baser sur le grossissement actuel réel plutôt que sur la dernière valeur affectée

La coloration des résultats de recherche et l'inversion des coordonnées sous-jacente

HighlightSearchHits recherche une requête dans tout le document et dessine une barre translucide sur chaque correspondance, en renvoyant le nombre de résultats ; ClearSearchHighlights, SearchHighlightCount et la propriété SearchHighlightColor complètent l'interface. Les rectangles de correspondance étant stockés dans l'espace de la page et projetés à nouveau lors de chaque rendu, les barres restent ancrées à leur texte pendant que l'utilisateur fait défiler, zoome ou pivote la vue. Si vous utilisez le composant complémentaire TPDFlibSearchPanel, sa méthode RunSearch dessine automatiquement les mêmes barres dans le visualiseur lié grâce à la propriété HighlightInView, activée par défaut

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

La partie subtile, et la raison de préférer la méthode intégrée plutôt que de développer la vôtre à partir des résultats de recherche bruts, réside dans la conversion du système de coordonnées. Le moteur de texte renvoie des rectangles de correspondance dans des coordonnées dépendant du paramètre Origin de la bibliothèque : avec Origin 0 ou 3, l'axe Y va du bas vers le haut comme dans l'espace utilisateur PDF natif, et avec Origin 2 ou 3, l'axe X est mesuré depuis le bord droit. La superposition du visualiseur fonctionnant dans un espace de page orienté du haut vers le bas et basé sur la gauche, chaque rectangle de correspondance doit être inversé (Y = PageHeight - Bottom, et le cas échéant X = PageWidth - Right) avant d'être stocké — utiliser directement le bord supérieur signalé sous l'origine par défaut dessinerait la coloration à la position verticalement inversée sur la page. TPDFlibViewer effectue cette conversion selon l'origine active lors de l'enregistrement de chaque correspondance, puis applique la projection de rotation directe lors du rendu, ce qui permet d'obtenir un positionnement correct des barres quel que soit l'angle. La translucidité utilise l'API Windows AlphaBlend avec une valeur alpha source constante, le canevas VCL n'ayant pas de composition alpha native. Les API de recherche de texte et de géométrie sous-jacentes sont traitées en détail dans l'article sur la recherche de texte et l'énumération des éléments de page

Clics sur les annotations, info-bulles et export des pièces jointes

TPDFlibViewer traite les annotations comme des objets interactifs de premier ordre. Un clic sur une annotation qui n'est pas un lien déclenche OnAnnotClick avec le numéro de page, l'index d'annotation basé sur 1 et la chaîne du sous-type. AnnotAtPagePoint expose the same hit test programmatically and skips Popup companion annotations so you always get the annotation the user actually sees. Le survol d'une annotation affiche son texte Contents comme info-bulle du composant grâce au commutateur AnnotHints, activé par défaut et fonctionnant en synergie avec la propriété standard VCL ShowHint. Pour les annotations de type FileAttachment, SaveAnnotAttachmentToFile écrit le fichier intégré sur le disque et renvoie 1 en cas de succès, ce qui finalise un workflow de clic pour enregistrer en quelques lignes

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

L'impression et les limites à anticiper

L'impression ne requiert aucune intégration distincte. PrintDoc affiche la boîte de dialogue d'impression standard de Windows — choix de l'imprimante, plage de pages, copies, assemblage — et confie la tâche au moteur d'impression de la bibliothèque, tandis que PrintDoc(False) envoie l'intégralité du document à l'imprimante par défaut sans aucune interface utilisateur. PrintDocRange s'exécute de manière entièrement programmatique avec un nom d'imprimante explicite, les pages de début et de fin, le nombre de copies et un indicateur d'assemblage, ce qui constitue le point d'entrée idéal pour les tâches d'impression en lot ou déclenchées par le serveur à partir de la même instance de visualiseur

Deux limites méritent d'être mentionnées en toute honnêteté. Premièrement, TPDFlibViewer est un composant Windows VCL ; la couche de visualisation est exclue des builds NOVCL. Ainsi, les services en console et les cibles non VCL doivent utiliser directement l'API de rendu sous-jacente TPDFlib. Deuxièmement, l'utilisation sans tête (headless) est délibérément prise en charge mais limitée : CaptureRegion effectue le rendu de n'importe quelle région client — pages et colorations comprises — dans un bitmap appartenant à l'appelant et fonctionne sur un contrôle sans parent et sans descripteur de fenêtre (handle), car le processus de dessin évite les appels VCL qui forceraient la création d'un handle. Cela rend l'évaluation au niveau du pixel pratique dans les tests automatisés, mais tout ce qui requiert véritablement une boucle de messages, comme les outils de souris interactifs, relève toujours d'une véritable session d'interface utilisateur. Côté performances, le composant conserve un cache mémoire de pages avec un budget configurable via SetCacheLimit, et peut conserver les pages rendues d'une session à l'autre ; l'article sur le cache disque de pages du visualiseur traite en détail de cette couche et de son comportement DPI par moniteur

Cinq étapes, donc : placer le composant et appeler LoadFromFile, associer les actions prêtes à l'emploi de PDFlibViewerActions, appeler HighlightSearchHits pour la recherche intégrée à la page, gérer OnAnnotClick pour les workflows d'annotation, et exposer PrintDoc. Tous les éléments présentés ici sont inclus dans la version standard de losLab PDF Library pour Delphi, où le composant de visualisation, les classes d'actions et le moteur de rendu s'installent ensemble