Article technique

Comparaison PDF côte à côte en Delphi avec PDFium

Deux documents ouverts en même temps, au même numéro de page, chacun dans son propre panneau défilant : voilà le cœur d'un visualiseur de comparaison. PDFium Component fournit cela par un modèle objet direct où TPdf possède le fichier et TPdfView possède l'affichage. Un document, un TPdf, un TPdfView. Si vous voulez trois panneaux, vous avez trois paires. Le difficile n'est pas dans les appels d'API ; c'est dans l'arithmétique de mise en page au redimensionnement de la fenêtre et dans la logique de synchronisation des pages quand vous décidez quelle vue doit suivre laquelle

Disposition de la fiche

La fiche VCL contient trois conteneurs TScrollBox côte à côte, chacun avec un TPdfView à l'intérieur, aligné en alClient pour qu'il remplisse la boîte. Deux composants TSplitter se placent entre les boîtes afin que l'utilisateur puisse ajuster la largeur des colonnes à l'exécution. Une barre d'outils au-dessus des panneaux porte les boutons d'ouverture, les commandes de zoom et le basculement entre deux et trois vues

Le mode trois vues est un booléen que la fiche suit en interne. Quand il bascule, vous recalculez les largeurs et affichez ou masquez la troisième colonne. L'approche la plus simple consiste à effacer toutes les propriétés Align, masquer les séparateurs, puis fixer des positions absolues :

Schéma de disposition de la fiche d'un visualiseur de comparaison PDF côte à côte en Delphi construit avec PDFium Component, montrant une barre d'outils, trois boîtes de défilement avec des panneaux TPdfView et des séparateurs en modes deux vues et trois vues
Chaque panneau est une boîte de défilement contenant un TPdfView, et passer de deux vues à trois vues se réduit à un autre jeu d'affectations de largeur
procedure TFormMain.UpdateLayout;
var
  TotalWidth: Integer;
begin
  TotalWidth := ClientWidth;

  if ThreeViewMode then
  begin
    ScrollBox3.Visible := True;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 3;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth div 3;
    ScrollBox3.Left   := ScrollBox2.Left + ScrollBox2.Width;
    ScrollBox3.Width  := TotalWidth - ScrollBox3.Left;
    // Appliquer la même valeur (ClientHeight - hauteur de la barre) aux trois Height
  end
  else
  begin
    ScrollBox3.Visible := False;
    ScrollBox1.Left   := 0;
    ScrollBox1.Width  := TotalWidth div 2;
    ScrollBox2.Left   := ScrollBox1.Width;
    ScrollBox2.Width  := TotalWidth - ScrollBox2.Left;
  end;
end;

Mettre Align := alNone sur les trois boîtes avant l'arithmétique entière évite que le moteur de contraintes de la VCL ne combatte vos affectations. Rétablissez la visibilité des séparateurs après le positionnement si vous voulez le redimensionnement par glisser en mode deux vues

La hauteur de chaque boîte de défilement est la zone cliente moins la hauteur du panneau de barre d'outils. Comme la barre d'outils est ancrée en haut avec alTop, ClientHeight - PanelButtons.Height vous donne l'espace vertical utilisable. Affectez cette valeur aux trois boîtes dans le même appel à UpdateLayout, pour qu'il n'existe jamais une trame où une boîte est plus haute que les autres et provoque un scintillement de mise en page

Ouverture d'un document

Chaque paire de panneaux a besoin de sa propre procédure d'ouverture. Le motif est court : désactiver le composant, définir le nom de fichier, activer, puis vérifier Active ; si la propriété est restée à False, demander un mot de passe et réessayer. Notez que TPdfView.Active est ce qui commande le rendu, alors que TPdf.Active est ce qui ouvre réellement le fichier ; les deux sont indépendantes. Mettre PdfView.Active := True alors que son TPdf lié n'est pas encore actif est sans danger mais n'affiche rien

Organigramme de l'ouverture d'un document PDF avec PDFium Component en Delphi, montrant la vérification silencieuse de Active, une nouvelle tentative avec mot de passe et une boîte d'erreur pour les fichiers endommagés ou protégés
Un chargement échoué laisse Active à False sans lever d'exception, donc le flux la vérifie, réessaie une fois avec un mot de passe, puis signale le problème au lieu d'afficher un panneau vide
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
  PdfViewComponent: TPdfView);
var
  Password: string;
begin
  if not OpenDialog.Execute then
    Exit;

  PdfComponent.Active   := False;
  PdfComponent.FileName := OpenDialog.FileName;
  PdfComponent.Password := '';
  PdfComponent.Active   := True;

  // Les échecs de chargement sont silencieux : Active reste False au lieu de lever une exception.
  if not PdfComponent.Active then
  begin
    // Très probablement un fichier protégé par mot de passe ; laissez une seule nouvelle tentative.
    if InputQuery('Password', 'Enter document password:', Password) then
    begin
      PdfComponent.Password := Password;
      PdfComponent.Active   := True;
    end;
  end;

  if not PdfComponent.Active then
  begin
    ShowMessage('Could not open ' + OpenDialog.FileName +
      ' (damaged file or wrong password)');
    Exit;
  end;

  PdfViewComponent.PageNumber := 1;
  SetActivePdfView(PdfViewComponent);
end;

Vérifiez toujours PdfComponent.Active après l'affectation ; un fichier endommagé ou un mot de passe erroné fait échouer le chargement en silence, sans lever d'exception dans le chemin par défaut. Définir explicitement PdfViewComponent.PageNumber := 1 après une ouverture réussie évite de garder un numéro de page périmé venu du document précédent

La boîte de message à la fin est intentionnelle : vous voulez que les fichiers corrompus ou non pris en charge apparaissent immédiatement plutôt que d'être avalés sous la forme d'un panneau vide et muet. Un utilisateur qui ne voit rien ne sait pas si le fichier a été chargé et se trouve simplement vide, ou si le composant l'a rejeté. Signaler l'échec garde l'erreur visible

Suivi du panneau actif

Quand l'utilisateur clique dans un panneau, ce panneau devient actif. La fiche suit un champ privé FActivePdfView: TPdfView. Le retour visuel est un changement de couleur de bordure sur le TScrollBox conteneur : mettez-la à clHighlight pour le panneau actif et à clWindow pour les autres. Reliez cela à chaque TPdfView.OnClick ainsi qu'à la procédure d'ouverture, pour que le focus suive le document que vous venez d'ouvrir

Certaines opérations s'appliquent à tous les panneaux visibles plutôt qu'au seul panneau actif. Un booléen FAllViewsMode sur la fiche pilote cette branche. Quand il vaut true, les changements de zoom et la navigation de page se diffusent à tous les panneaux qui ont un document actif :

procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
  if PdfView1.Active then PdfView1.Zoom := NewZoom;
  if PdfView2.Active then PdfView2.Zoom := NewZoom;
  if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;

Navigation de page synchronisée

La navigation synchronisée est facultative mais utile pour les flux de révision de documents où les deux fichiers couvrent la même plage de pages. La logique se place dans un gestionnaire d'événement déclenché après que l'utilisateur a navigué dans une vue. Quand une vue source change son PageNumber, le gestionnaire propage ce numéro aux autres vues, sous une seule réserve : la vue cible doit compter au moins autant de pages, sinon on passe

Les propriétés PageNumber de TPdfView et de TPdf sont indépendantes. TPdf.PageNumber indique la page que le composant document considère comme courante ; TPdfView.PageNumber indique ce qui est affiché à l'écran. Pour la navigation, c'est la propriété de la vue que vous voulez, pas celle du document

Une case à cocher intitulée par exemple « Synchroniser les pages » donne le contrôle à l'utilisateur. Quand elle est décochée, chaque panneau navigue indépendamment et le gestionnaire sort immédiatement. Cette indépendance compte pour les cas où les deux documents ont un nombre de pages différent, ou lorsque l'utilisateur veut retrouver le passage équivalent dans une traduction qui commence à une autre page. Imposer toujours la synchronisation rendrait l'outil plus pénible à utiliser qu'un simple agencement de deux fenêtres sur le bureau

Un point de vigilance : définir PdfView.PageNumber par programme à l'intérieur du gestionnaire de synchronisation déclenchera lui-même l'événement de changement sur cette vue. Protégez-vous de la récursion infinie avec un indicateur booléen que vous positionnez avant l'affectation et effacez immédiatement après. Cet indicateur appartient à la fiche, pas à chaque vue, car les trois vues partagent le même gestionnaire

Schéma de la navigation de page synchronisée dans un visualiseur de comparaison PDF Delphi utilisant PDFium Component, avec la case de synchronisation, un contrôle du nombre de pages par vue cible et un indicateur anti-récursion
Le numéro de page voyage de la vue source vers toutes les autres vues seulement si la synchronisation est activée et si chaque vue cible contient bien cette page

Zoom par panneau

Chaque TPdfView porte sa propre propriété Zoom, un Double exprimé en pourcentage où Zoom := 100 signifie la taille réelle (100 %). La définir annule tout FitMode actif. Pour un bouton d'ajustement à la largeur sur le panneau actif, lisez le zoom d'ajustement dans PdfView.PageWidthZoom[PdfView.PageNumber] et affectez-le. Pour l'ajustement à la page, utilisez PageZoom[PageNumber]. Les deux sont des propriétés tableau indexées par un numéro de page à base 1, alors prémunissez-vous contre un numéro de page nul avant d'y accéder

Quand vous exportez la page courante en image, lisez la rotation depuis la vue mais appelez RenderPage sur le composant TPdf, pas sur la vue. La forme bitmap de TPdf.RenderPage prend des dimensions explicites en pixels plus une valeur TRotation et un ensemble TRenderOptions. La variante fonction renvoie un TBitmap possédé par l'appelant, que vous libérez vous-même après l'enregistrement :

procedure TFormMain.SaveActiveViewAsImage;
var
  Pdf: TPdf;
  Bmp: TBitmap;
  Jpeg: TJpegImage;
begin
  if not Assigned(FActivePdfView) or not FActivePdfView.Active then
    Exit;

  Pdf := FActivePdfView.Pdf;
  Pdf.PageNumber := FActivePdfView.PageNumber;

  Bmp := Pdf.RenderPage(
    0, 0,
    Round(Pdf.PageWidth * 2),
    Round(Pdf.PageHeight * 2),
    FActivePdfView.Rotation, [], clWhite);
  try
    if SavePictureDialog.Execute then
    begin
      Jpeg := TJpegImage.Create;
      try
        Jpeg.Assign(Bmp);
        Jpeg.CompressionQuality := 90;
        Jpeg.SaveToFile(SavePictureDialog.FileName);
      finally
        Jpeg.Free;
      end;
    end;
  finally
    Bmp.Free;
  end;
end;

Le multiplicateur 2x sur la largeur et la hauteur donne une sortie plus nette pour les documents à texte fin. Le try/finally autour de la libération du bitmap n'est pas facultatif ; une annulation de TSaveDialog passe quand même par le bloc finally, et vous voulez que le bitmap soit libéré quoi qu'ait fait l'utilisateur

Exigences de DLL

PDFium Component encapsule la bibliothèque native pdfium. Un processus hôte 32 bits a besoin de pdfium32.dll ; un hôte 64 bits a besoin de pdfium64.dll. Les variantes avec le moteur JavaScript V8 ajoutent le suffixe v8 et pèsent environ 23 à 27 Mo contre 5 à 6 Mo pour les versions standard. Pour un visualiseur de comparaison qui désactive le remplissage de formulaires (Pdf.FormFill := False), la version standard sans V8 suffit et garde la distribution plus légère

Placez la DLL dans le même répertoire que l'exécutable, ou dans n'importe quel répertoire du PATH système. Le composant la charge à la demande lors de l'activation du premier TPdf, si bien qu'une DLL manquante se manifeste à ce moment-là plutôt qu'au démarrage de l'application. Si vous livrez un installeur, l'approche la plus fiable consiste à copier la DLL dans le dossier de l'application pendant l'installation plutôt que de compter sur un répertoire système qu'un administrateur pourrait nettoyer plus tard

Les versions V8 servent surtout quand vous devez interagir avec les actions JavaScript des PDF, par exemple pour déclencher des champs de calcul ou des gestionnaires de soumission. Un visualiseur de comparaison passif n'a aucune raison d'exécuter du JavaScript ; définir Pdf.FormFill := False avant Active := True écarte entièrement l'environnement de remplissage de formulaires, ce qui signifie aussi qu'aucun moteur JS n'est initialisé même si la version standard est utilisée. C'est la valeur par défaut correcte pour un visualiseur en lecture seule, quelle que soit la variante de DLL que vous livrez

Pour plus de détails sur PDFium Component et son API complète, consultez la page produit Delphi PDFium Component