Article technique

Créer un lecteur PDF en Delphi avec PDFium Component

Un lecteur PDF en Delphi se ramène à deux composants et au câblage entre eux. TPdf possède le document : il ouvre le fichier, le déchiffre 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, le zoom et la page que l'utilisateur regarde. PDFium Component enveloppe le moteur de rendu livré à l'intérieur de Chrome : les glyphes, l'anticrénelage et les couleurs obtenus sur le canevas correspondent donc à ce que vos utilisateurs voient déjà dans leur navigateur. Le travail n'est pas dans le rendu. Il est dans le raccordement de l'objet document à la vue, dans le chargement sans plantage d'un fichier endommagé ou protégé par mot de passe, et dans la poignée de commandes qui donne au lecteur un air fini : tourner la page, changer le zoom, ajuster la page à la fenêtre

Cet article parcourt cet assemblage dans l'ordre où vous le construisez réellement. Tout ce qui suit affiche une seule page à la fois, ce que veulent la plupart des flux documentaires. Si vous avez besoin de pages empilées dans une colonne à défilement continu, c'est une autre décision de mise en page et ce n'est pas le chemin suivi ici

Câbler TPdf à TPdfView

Déposez un TPdf et un TPdfView sur la fiche, puis indiquez à la vue quel document afficher. Cette unique affectation constitue tout le lien entre le document non visuel et le contrôle qui le peint

Architecture de lecteur PDF Delphi où TPdf possède le document, TPdfView le peint, et une seule affectation de propriété les relie au-dessus de la DLL PDFium
TPdf possède le document tandis que TPdfView le peint, et une seule affectation relie les deux au-dessus du moteur PDFium partagé
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf et PdfView ont été déposés à la conception.
  PdfView.Pdf := Pdf;                 // la vue peint ce que contient ce document
  PdfView.FitMode := pfmFitWidth;     // place d'emblée l'utilisateur sur un zoom sensé
end;

Avant que tout cela ne s'exécute, la bibliothèque native PDFium doit être présente sur la machine. PDFium Component appelle pdfium32.dll ou pdfium64.dll selon votre plateforme cible, et le document refuse simplement de s'ouvrir si la DLL est introuvable. Livrez la DLL correspondante à côté de votre exécutable, ou placez-la là où le chargeur du système la trouvera. Les versions avec V8 n'existent que pour les PDF portant du JavaScript que vous voulez exécuter, ce qu'un simple lecteur ne fait pas : prenez donc la DLL standard, sauf raison concrète de faire autrement

Charger un document sans faire confiance à l'entrée

L'instinct pousse à envelopper le chargement dans un try/except et à traiter une exception levée comme un échec. Cet instinct est faux ici, et s'y fier produit un lecteur qui semble correct jusqu'au jour où quelqu'un lui tend un fichier cassé. Poser Active := True ne lève rien en cas d'échec de chargement. PDFium Component intercepte l'erreur interne et laisse Active à False : la seule façon honnête de savoir si le document s'est ouvert est donc de relire la propriété après l'avoir écrite

Flux de décision de chargement pour un lecteur PDFium Delphi où poser Active ne lève jamais, un faux silencieux signale un mauvais mot de passe ou un fichier endommagé, et une seule nouvelle tentative de mot de passe suit
L'activation ne lève jamais en cas d'échec, si bien que le lecteur relit Active et répond à un faux silencieux par une unique nouvelle demande de mot de passe
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // ne lève jamais ; en cas d'échec Active reste à False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // la vue suit sa propre page courante
  UpdatePageLabel;
end;

Deux points méritent l'attention. Le premier est que PageNumber existe sur les deux objets et que les deux sont indépendants. Pdf.PageNumber est la notion de page courante du document ; PdfView.PageNumber est la page que le contrôle affiche réellement, et c'est celle que vous posez pour déplacer l'utilisateur dans le fichier. Poser l'une ne bouge pas l'autre : un lecteur pilote donc toujours la propriété de la vue. Le second est l'indexation à partir de 1 : les pages vont de 1 à Pdf.PageCount, pas de 0, ce qui piège quiconque a l'habitude des tableaux indexés à zéro

Traiter un fichier chiffré

Les documents chiffrés se replient sur le même chemin de chargement. Si le mot de passe d'ouverture est défini avant l'activation, le document se déchiffre en s'ouvrant ; s'il est faux ou absent, Active reste à False exactement comme pour un fichier corrompu. La récupération consiste donc à demander un mot de passe et à retenter 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('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // doit être défini avant Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

Parce que l'échec est silencieux aussi bien pour un mauvais mot de passe que pour un fichier endommagé, vous ne pouvez pas distinguer les deux à partir du seul Active. En pratique c'est acceptable pour un lecteur : l'utilisateur fournit le bon mot de passe ou apprend que le fichier ne s'ouvrira pas, et le message se lit de la même façon dans les deux cas

Feuilleter le document

Une fois le document ouvert, la navigation est de l'arithmétique sur PdfView.PageNumber bornée par Pdf.PageCount. Le seul vrai travail est le bornage, pour que les boutons ne poussent jamais la page hors de la plage 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 saisie « aller à la page N » est le même appel GoToPage alimenté par un entier analysé, et le bornage couvre le cas où l'utilisateur tape 9999 dans un fichier de dix pages. Gardez UpdatePageLabel comme unique endroit qui écrit « Page 3 sur 12 », afin que l'affichage ne dérive jamais par rapport à ce que montre la vue

Zoom : pourcentages explicites et modes d'ajustement

Le zoom sur TPdfView arrive en deux saveurs qui interagissent, et comprendre cette interaction fait la différence entre une commande de zoom qui se tient et une qui lutte contre l'utilisateur. La voie directe est la propriété Zoom, un pourcentage où 100 signifie la taille réelle. L'autre voie est FitMode, qui demande à la vue de calculer le zoom à votre place et de le recalculer au fil des redimensionnements de la fenêtre

Interaction entre Zoom et FitMode dans un lecteur PDFium Delphi où affecter un Zoom exact remet FitMode à pfmNone et choisir un mode d'ajustement rend le calcul du zoom à la vue
Affecter un zoom exact efface le mode d'ajustement, et choisir un mode d'ajustement redonne le calcul du zoom à la vue
// grossissements fixes
PdfView.Zoom := 100;     // taille réelle
PdfView.Zoom := 50;      // moitié
PdfView.Zoom := 200;     // double

// laisse la vue dimensionner la page sur la fenêtre, et la garder dimensionnée au redimensionnement
PdfView.FitMode := pfmFitWidth;   // la largeur de 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. Affecter Zoom directement remet FitMode à pfmNone. C'est le comportement correct, pas un bug : dès que l'utilisateur choisit un 150 % exact, la vue ne peut plus honorer en même temps un « ajuster à la largeur », car les deux demandes se contredisent. La conséquence pour votre interface 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 visible le mode actif. Quand l'utilisateur clique sur ajuster à la page, posez FitMode ; quand il clique sur un zoom numérique, posez 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, par exemple pour amorcer un curseur de zoom avec le pourcentage d'ajustement courant, les aides par page vous donnent les nombres sans changer le mode. PageWidthZoom[N], PageZoom[N] et ActualSizeZoom[N] renvoient le pourcentage qui ajusterait la page N à la largeur, l'afficherait entière ou la rendrait à la taille réelle

// amorce un affichage de zoom depuis 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 un lecteur fini a réellement besoin

Le lecteur ci-dessus tient en quelques dizaines de lignes, et il fait déjà le travail dont un flux documentaire a besoin : ouvrir un fichier, survivre à un mauvais fichier, afficher une page, se déplacer entre les pages et changer le grossissement à la main ou par ajustement. PDFium fait silencieusement les parties difficiles. Les polices incorporées se résolvent, les annotations et les champs de formulaire se peignent là où le document les place, et la page que vous voyez correspond à celle que verrait un utilisateur de Chrome, puisque c'est le même moteur qui dessine les deux

À partir de cette base, les ajouts sont incrémentaux plutôt que structurels. La sélection et la recherche de texte lisent la même couche de texte que PDFium construit déjà ; des métadonnées comme Pdf.Title et Pdf.Author sont à une lecture de propriété ; la rotation et les niveaux de gris sont des options de rendu que vous passez en dessinant une page vers un bitmap. Aucune de ces additions ne change la colonne vertébrale que vous avez ici, à savoir l'objet document, la vue et le flux charger-puis-naviguer qui les relie. Réussissez cette colonne vertébrale et le reste est de la décoration

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