Article technique

Créer une visionneuse PDF dans Delphi avec le composant PDFium

Une visionneuse PDF dans Delphi se résume à deux composants et au câblage (wiring) entre eux. TPdf possède le document : il ouvre le fichier, le décrypte et répond aux questions sur le nombre de pages et les métadonnées. TPdfView est le contrôle visuel qui peint les pages à l'écran et gère le défilement (scrolling), le zoom et la page que l'utilisateur regarde actuellement. Le composant PDFium enveloppe le même moteur de rendu que celui qui est fourni dans Chrome, de sorte que les glyphes, l'anticrénelage (anti-aliasing) et la couleur que vous obtenez sur la zone de dessin (canvas) correspondent à ce que vos utilisateurs voient déjà dans leur navigateur. Le travail n'est pas dans le rendu. Il réside dans la connexion de l'objet document à la vue, le chargement sans plantage sur un fichier endommagé ou protégé par mot de passe, et le fait de donner à l'utilisateur la poignée de contrôles qui donnent à une visionneuse un aspect fini : tourner la page, changer le zoom, ajuster la page à la fenêtre

Ceci parcourt cet assemblage dans l'ordre où vous le construisez réellement. Tout ici rend une seule page à la fois, ce qui est ce que souhaitent la plupart des flux de travail de documents. Si vous avez besoin de pages empilées dans une colonne à défilement continu (continuously scrolling column), il s'agit d'une décision de disposition différente et ce n'est pas la voie à suivre ici

Câblage de TPdf à TPdfView

Déposez un TPdf et un TPdfView sur la fiche (form), puis indiquez à la vue quel document afficher. Cette seule affectation (assignment) est le lien complet entre le document non visuel et le contrôle qui le peint

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf et PdfView ont été déposés au moment de la conception.
  PdfView.Pdf := Pdf;                 // la vue peint tout ce que ce document contient
  PdfView.FitMode := pfmFitWidth;     // démarrer l'utilisateur à un zoom raisonnable
end;

Avant que tout cela ne s'exécute, la bibliothèque native PDFium doit être sur la machine. Le composant PDFium appelle pdfium32.dll ou pdfium64.dll en fonction de votre plate-forme cible, et le document refuse tout simplement de s'ouvrir si la DLL est introuvable. Livrez (Ship) la DLL correspondante à côté de votre exécutable, ou placez-la là où le chargeur système (system loader) la trouvera. Les versions compatibles V8 n'existent que pour les PDF contenant du JavaScript que vous souhaitez exécuter, ce qu'une simple visionneuse ne fait pas, alors optez pour la DLL standard à moins que vous n'ayez une raison concrète de ne pas le faire

Chargement d'un document sans faire confiance à l'entrée

L'instinct est d'envelopper le chargement dans un try/except et de traiter une exception levée comme un échec. Cet instinct est faux ici, et se tromper produit une visionneuse qui a l'air bien jusqu'à ce que quelqu'un lui remette un fichier cassé. Définir Active := True ne lève pas d'exception en cas d'échec de chargement. Le composant PDFium intercepte l'erreur interne et laisse Active à False, de sorte que la seule façon honnête de savoir si le document s'est ouvert est de relire la propriété après l'avoir définie

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // ne lève jamais d'exception ; l'échec laisse Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Impossible d'ouvrir ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // la vue suit sa propre page courante
  UpdatePageLabel;
end;

Deux choses méritent l'attention. La première est que PageNumber existe sur les deux objets et les deux sont indépendants. Pdf.PageNumber est la notion d'une page courante du document ; PdfView.PageNumber est la page que le contrôle affiche réellement, et c'est celle que vous définissez pour déplacer l'utilisateur dans le fichier. Définir l'un ne déplace pas l'autre, donc une visionneuse pilote toujours la propriété de la vue. La seconde est l'indexation basée sur 1 : les pages vont de 1 à Pdf.PageCount, et non de 0, ce qui surprend tous ceux qui sont habitués aux tableaux basés sur zéro

Gestion d'un fichier crypté

Les documents cryptés se plient au même chemin de chargement. Si le mot de passe d'ouverture est défini avant l'activation, le document se décrypte lors de son ouverture ; s'il est erroné ou manquant, Active reste False exactement comme il le fait pour un fichier corrompu. La récupération (recovery) consiste donc à demander un mot de passe et à réessayer l'activation

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Mot de passe requis', 'Mot de passe :', Password) then
    begin
      Pdf.Password := Password;       // doit être défini avant Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Impossible d'ouvrir le document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Étant donné que l'échec est silencieux à la fois pour un mauvais mot de passe et un fichier endommagé, vous ne pouvez pas les distinguer à partir d'Active seul. En pratique, cela est acceptable pour une visionneuse : l'utilisateur fournit le bon mot de passe ou apprend que le fichier ne s'ouvrira pas, et le message est le même de toute façon

Pagination dans le document

Avec le document ouvert, la navigation est arithmétique sur PdfView.PageNumber délimité (bounded) par Pdf.PageCount. Le seul vrai travail est le serrage (clamping), de sorte que les boutons ne poussent jamais la page hors de portée et que les boutons premier et dernier restent désactivés aux extrémités du fichier

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// les quatre boutons de navigation se réduisent à un appel chacun
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Une zone de texte "aller à la page N" est le même appel GoToPage alimenté à partir d'un entier analysé (parsed integer), et la limite (clamp) couvre le cas où l'utilisateur tape 9999 dans un fichier de dix pages. Conservez UpdatePageLabel comme le seul endroit qui écrit "Page 3 sur 12" afin que l'affichage ne se désynchronise (drifts out of sync) jamais avec ce que la vue montre

Zoom : pourcentages explicites et modes d'ajustement

Le zoom sur TPdfView se décline en deux saveurs qui interagissent, et comprendre l'interaction fait la différence entre un contrôle de zoom qui se comporte bien et un qui se bat avec l'utilisateur. L'itinéraire direct est la propriété Zoom, un pourcentage où 100 signifie la taille réelle. L'autre itinéraire est FitMode, qui demande à la vue de calculer le zoom pour vous et de continuer à le recalculer au fur et à mesure que la fenêtre se redimensionne

// agrandissements fixes
PdfView.Zoom := 100;     // taille réelle
PdfView.Zoom := 50;      // moitié
PdfView.Zoom := 200;     // double

// laisser la vue dimensionner la page à la fenêtre, et la garder dimensionnée lors du redimensionnement
PdfView.FitMode := pfmFitWidth;   // la largeur de la page remplit le contrôle
PdfView.FitMode := pfmFitPage;    // page entière visible
PdfView.FitMode := pfmActualSize; // 1:1 avec les points du document

Voici la partie qui fait trébucher (trips people up) les gens. L'attribution directe de Zoom réinitialise FitMode à pfmNone. C'est un comportement correct, pas un bogue : au moment où l'utilisateur choisit un 150 % exact, la vue ne peut plus honorer "ajuster à la largeur", car les deux demandes sont en conflit. La conséquence pour votre interface utilisateur (UI) est qu'un bouton de zoom avant et un bouton d'ajustement à la page sont des états mutuellement exclusifs, et la barre d'outils doit rendre le mode actif visible. Lorsque l'utilisateur clique sur ajuster à la page, définissez FitMode ; lorsqu'il clique sur un zoom numérique, définissez Zoom et laissez-le effacer le mode d'ajustement de lui-même

Si vous préférez calculer vous-même la valeur d'ajustement, peut-être pour amorcer (seed) un curseur de zoom (zoom slider) avec le pourcentage d'ajustement actuel, les aides (helpers) par page vous donnent les chiffres sans changer de mode. PageWidthZoom[N], PageZoom[N] et ActualSizeZoom[N] renvoient le pourcentage qui ajusterait la page N à la largeur, l'ajusterait en entier ou la rendrait à sa taille réelle

// amorcer un affichage de zoom à partir de la valeur d'ajustement à la largeur de la page courante
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Ce dont une visionneuse terminée a réellement besoin

La visionneuse ci-dessus fait quelques dizaines de lignes et elle fait déjà le travail dont un flux de travail de document a besoin : ouvrir un fichier, survivre à un mauvais fichier, afficher une page, se déplacer entre les pages et modifier l'agrandissement (magnification) à la main ou par ajustement. PDFium fait les parties difficiles en silence. Les polices intégrées se résolvent, les annotations et les champs de formulaire (form fields) se peignent là où le document les place, et la page que vous voyez correspond à celle qu'un utilisateur de Chrome verrait, car c'est le même moteur qui dessine les deux

À partir de cette base, les ajouts sont incrémentiels plutôt que structurels. La sélection de texte et la recherche lisent à partir de la même couche de texte que PDFium construit déjà ; les métadonnées telles que Pdf.Title et Pdf.Author sont à une lecture de propriété près ; la rotation et les niveaux de gris (grayscale) sont des options de rendu que vous transmettez lorsque vous dessinez une page sur un bitmap. Aucun de ces éléments ne modifie la colonne vertébrale (spine) que vous avez ici, qui est l'objet document, la vue et le flux charge-puis-navigue (load-then-navigate flow) qui les relie. Obtenez cette colonne vertébrale correctement et le reste n'est que décoration

Les composants TPdf et TPdfView utilisés tout au long font partie du Composant PDFium pour Delphi et C++Builder, qui porte la référence complète de la visionneuse sur sa page de produit