Article technique

Visionneuse PDF pour Lazarus et Free Pascal avec PDFium

Delphi et Lazarus compilent le même Object Pascal, et cette similitude de surface est exactement ce qui rend le portage d'une visionneuse entre eux trompeur. Les deux chaînes d'outils divergent à trois endroits qui comptent pour le travail sur les PDF : le type string natif est UTF-16 dans Delphi et UTF-8 dans une application LCL ; la VCL et la LCL sont des frameworks visuels différents avec leurs propres contrôles, dialogues et formats de sérialisation de formulaires (form-streaming formats) ; et un binaire Delphi cible Windows tandis qu'un binaire FPC peut être destiné à Linux ou macOS. Aucune de ces différences n'apparaît au moment de la compilation. Une visionneuse construite sur le Composant PDFium, qui propose des éditions VCL et LCL à partir d'une seule arborescence source, compilera proprement sous Lazarus après une poignée d'échanges de noms d'unités et quelques blocs {$IFDEF FPC}. Les échecs arrivent plus tard, lorsque de vraies données et un vrai déploiement exposent les hypothèses que la compilation Delphi faisait discrètement

Quatre de ces hypothèses expliquent la majeure partie du temps perdu : l'encodage du texte à la limite de l'interface utilisateur, la tentation de maintenir deux copies du formulaire, la façon dont un binaire de moteur natif est résolu au moment de l'exécution, et le moment où la synthèse vocale n'a plus de plate-forme une fois SAPI disparue. Chacune est facile à gérer si vous savez qu'elle arrive, et coûteuse à traquer si vous l'ignorez

Le même Pascal, des charges utiles de chaînes (string payloads) différentes

La string native de Delphi est en UTF-16 depuis 2009. Lazarus et Free Pascal utilisent par défaut l'UTF-8 dans les applications LCL. Les API orientées texte du composant parlent UTF-16 via le type WString, dont la compilation FPC fait un alias vers WideString, de sorte que chaque frontière où le texte passe entre votre interface utilisateur LCL et le moteur PDF est un point de conversion

Les conversions se produisent automatiquement dans les affectations simples, et la plupart du code n'a jamais besoin d'y penser. Deux habitudes permettent d'éviter les bogues d'encodage. Faites passer le texte directement sans manipulation au niveau de l'octet : un code qui découpe un terme de recherche par décalage d'octet fonctionne dans Delphi, où un Char est une unité UTF-16, et corrompt l'UTF-8 multi-octets dans la LCL. Et testez avec des données non-ASCII dès la première exécution. Un nom de fichier allemand, un terme de recherche cyrillique, un nom d'auteur accentué dans les métadonnées du document : des données de test purement ASCII masquent tous les défauts d'encodage, car l'ASCII est la seule plage où l'UTF-8 et l'UTF-16 s'accordent octet pour caractère. Le bogue est réel depuis le début ; l'ASCII le maintient simplement invisible jusqu'à ce qu'un client à Munich ouvre un fichier que vous n'avez jamais essayé

Un seul bloc conditionnel, pas un embranchement (fork) par IDE

Après la première douzaine d'IFDEF, la base de code commence à ressembler à deux projets partageant un seul dépôt, et l'idée de la diviser (fork) par IDE semble tentante. C'est la mauvaise approche. Les véritables différences se réduisent à un seul bloc de déclaration partagé, et un embranchement double le coût de chaque correction de bogue à partir de ce moment. Gardez la couche conditionnelle aussi petite que ceci :

{$IFDEF FPC}
uses
  LCLType, Forms, Graphics, Controls;

type
  WString = WideString;   // les API de texte du composant sont en UTF-16
  TBytes  = array of Byte;
{$ELSE}
uses
  Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}

Tout ce qui se trouve sous ce bloc compile de manière identique dans les deux IDE. La gestion des documents, la navigation entre les pages, les appels de rendu : TPdf et TPdfView exposent la même surface dans les éditions VCL et LCL, de sorte que la majeure partie de la visionneuse ne voit jamais de condition de compilateur. Maintenir cet état de fait relève de la discipline structurelle plutôt que de l'astuce technique. La logique PDF partagée vit dans des unités qui n'intègrent aucun dialogue ou panneau spécifique à un framework. La poignée de choses qui diffèrent véritablement, comme les dialogues d'impression et les sélecteurs de fichiers avec leurs conventions de plate-forme, se cachent derrière une interface fine implémentée une fois par framework. Le bloc IFDEF devient le seul endroit où la divergence de plate-forme future est autorisée à atterrir, au lieu de disperser des directives de compilateur dans quarante unités

Construisez le formulaire dans le code, pas dans deux concepteurs (designers)

La sérialisation de formulaires (form streaming) est l'endroit où les projets multi-IDE pourrissent silencieusement. Un fichier .dfm et un fichier .lfm qui prétendent décrire le même formulaire s'écartent propriété par propriété jusqu'à ce que les deux compilations se comportent différemment pour des raisons que personne ne peut comparer (diff), car les deux fichiers ne sont même pas dans le même format. Construire la visionneuse au moment de l'exécution contourne tout le problème. Il y a une séquence de constructeur, versionnée comme du code ordinaire, et elle se lit de la même manière sur les deux plates-formes :

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  Pdf := TPdf.Create(Self);

  PdfView := TPdfView.Create(Self);
  PdfView.Parent := Self;
  PdfView.Align := alClient;
  PdfView.Pdf := Pdf;
  PdfView.FitMode := pfmFitWidth;

  if ParamCount > 0 then
  begin
    Pdf.FileName := ParamStr(1);
    Pdf.Active := True;   // ouvre le document ; PageCount valide après cela
  end;
end;

L'ordre exact de ces affectations importe moins que l'unique ligne qui fait le vrai travail. PdfView.Pdf := Pdf lie le contrôle visuel au composant de document, et à partir de là, la navigation dans les pages via PageNumber et le comportement d'ajustement via FitMode répondent de manière identique sous VCL et LCL. Il est utile de connaître une particularité inter-frameworks avant qu'un utilisateur ne la signale comme un bogue : affecter Zoom manuellement ramène FitMode à pfmNone sur les deux frameworks. Donc, si votre barre d'outils traite "ajuster à la largeur" comme une préférence persistante (sticky preference), vous devez réaffecter le mode d'ajustement après tout zoom programmatique, sinon la préférence cesse discrètement de persister la première fois que le code touche au niveau de zoom

Le binaire dont l'IDE ne vous a jamais averti

Le composant enveloppe le moteur PDFium, qui est livré sous forme de binaire natif de plate-forme, et ce binaire est la source de presque tous les rapports du type "fonctionne dans l'IDE, échoue à partir du raccourci installé". Trois règles expliquent la plupart d'entre eux. L'architecture (Bitness) doit correspondre exactement. Un exécutable 32 bits ne peut pas charger une bibliothèque pdfium 64 bits, et le message que le système d'exploitation renvoie ("module introuvable" sur certaines versions de Windows) induit activement en erreur, car le fichier se trouve juste là, à côté de l'exécutable. Résolvez le chemin de la bibliothèque par rapport à l'exécutable, jamais au répertoire de travail ; un lancement depuis l'IDE et un lancement depuis le shell diffèrent précisément sur ce point, c'est pourquoi le bogue se cache pendant le développement. Et interceptez un échec de chargement avant l'ouverture du premier document, puis signalez-le en précisant le chemin attendu et l'architecture. Un ticket d'assistance qui indique "Binaire PDFium 64 bits manquant sous <chemin>" se ferme en quelques minutes. Celui qui indique "la visionneuse plante au démarrage" se transforme en une semaine d'échanges

Profitez-en pour versionner le binaire du moteur en même temps que l'exécutable. PDFium évolue rapidement, et un installateur qui met à jour l'application mais laisse une bibliothèque obsolète sur le disque produit des plantages que personne dans votre bureau ne peut reproduire, pour la simple raison que chaque machine de votre bureau possède par hasard la paire correspondante. Traitez la bibliothèque comme une partie de l'artefact de compilation, avec le même installateur, le même numéro de version et le même chemin de restauration (rollback path) que l'exécutable qu'elle charge

Enregistrement des composants dans l'IDE Lazarus

La construction à l'exécution ne nécessite aucune inscription au moment de la conception (design-time registration), ce qui constitue la configuration la plus propre pour une visionneuse qui construit sa propre interface utilisateur dans le code. Lorsque vous souhaitez effectivement avoir les composants sur la palette Lazarus pour le travail au moment de la conception, installez le paquet (package) et laissez son unité d'enregistrement dédiée, PDFiumLazReg dans Lib/FPC/PDFiumLaz.lpk, s'en charger. Cette unité est marquée "design-time" intentionnellement : elle référence les interfaces de l'éditeur de propriétés de l'IDE qui ne doivent jamais être liées dans votre exécutable final

Si vous vous trompez, le symptôme est une application qui dépend inexplicablement des paquets de l'IDE, ce qui se traduit par un échec de déploiement sur la première machine cliente qui n'a jamais eu Lazarus d'installé

Synthèse vocale (Speech) et lecteurs d'écran en dehors de Windows

La synthèse vocale (text-to-speech) est la seule fonctionnalité où la logique multiplateforme se brise, et elle se brise au niveau du système d'exploitation, pas du composant. SAPI, le backend TTS habituel sur Windows, n'existe que sur Windows. Une compilation Lazarus qui cible toujours Windows conserve la sortie SAPI complète et le même comportement compatible NVDA que l'original Delphi, donc un portage de Windows vers Windows ne perd rien ici, et un utilisateur NVDA ne peut pas distinguer les deux compilations

Une cible Linux ou macOS est une autre affaire. Il n'y a pas de SAPI à appeler, donc la sortie audio doit être recâblée vers un service vocal natif tandis que les API de lecture au-dessus restent en place. Cette séparation est l'argument en faveur du placement de la parole derrière une interface dès le premier commit : l'analyse de l'ordre de lecture et le curseur de suivi des mots sont neutres vis-à-vis de la plate-forme et sont conservés intacts, et seule la fine couche qui produit réellement le son doit changer selon la plate-forme. L'article sur le lecteur accessible couvre cette machinerie de lecture en profondeur

Une liste de contrôle de parité (parity checklist) avant de déclarer le portage terminé

Le passage en revue suivant a permis de détecter de véritables régressions, énumérées à peu près dans l'ordre d'apparition des défaillances. Ouvrez un document dont le chemin d'accès contient des caractères non ASCII. Recherchez un terme contenant des caractères non ASCII et confirmez que les résultats se mettent en évidence là où ils le devraient. Testez le défilement de la molette de la souris, la sélection par glissement et la navigation dans les pages au clavier sur chaque jeu de widgets que vous livrez, car la gestion du focus et le comportement de la molette sont les recoins de la LCL les plus dépendants du jeu de widgets. Vérifiez le rendu à une mise à l'échelle d'affichage de 100 %, 150 % et 200 %. Enfin, exécutez la compilation installée, et non la compilation de l'IDE, sur une machine qui n'a jamais eu l'IDE, car c'est le seul test qui vérifie honnêtement la résolution du binaire. Tout le reste peut réussir pendant que celui-ci échoue silencieusement

Le débit (throughput) de rendu se transfère entre les deux éditions sans changement, de sorte que l'approche de mise en cache de l'article sur le cache de rendu et les performances du zoom s'applique à la visionneuse LCL exactement comme elle a été écrite pour celle de la VCL

Rien de tout cela ne fait de l'édition LCL une édition inférieure. La surface principale est identique des deux côtés : TPdf, TPdfView, le rendu, les formulaires, l'extraction de texte et les API d'accessibilité se comportent de la même manière quel que soit l'IDE qui les a compilées. Chaque différence qui vaut la peine d'être suivie est liée à la plate-forme plutôt qu'à l'édition. La synthèse vocale SAPI est exclusive à Windows, les dialogues suivent les conventions de chaque framework, et le binaire doit correspondre à l'architecture dans laquelle il est chargé. Obtenez les limites d'encodage, le formulaire d'exécution et la résolution binaire corrects, et le reste du portage correspond au travail mécanique dont le compilateur s'est déjà chargé pour vous

Les éditions VCL et LCL décrites ici sont livrées ensemble sous le nom de Composant PDFium, avec le code source et des API publiques identiques pour Delphi, C++Builder et Lazarus/FPC