Article technique

RtLTextOut dans HotPDF : texte PDF de droite à gauche en Delphi

Envoyez la phrase arabe يوضح ملف PDF هذا à un TextOut ordinaire et la page qui en résulte est incorrecte de deux façons à la fois. Les mots courent de gauche à droite au lieu de droite à gauche, et les lettres restent séparées dans leurs formes isolées au lieu de se joindre en mots connectés. Rien ne génère d'erreur. Le code Delphi compile, le fichier s'ouvre, et un relecteur lisant l'arabe vous dit que le résultat est inutilisable. Le correctif est un seul appel, pas un changement de bibliothèque : HotPDF achemine le texte de droite à gauche via une méthode distincte, RtLTextOut, qui gère le réordonnancement que TextOut ordinaire ne fait pas. Quatre aspects de cette méthode déterminent si la sortie est exploitable : ce qu'elle fait à la chaîne, comment son argument charset sélectionne le script, le changement au niveau du document qu'elle effectue comme effet de bord, et le travail sur la police qui doit précéder.

Pourquoi le texte de droite à gauche nécessite son propre appel

Un flux de contenu PDF ne stocke pas de texte éditable. Il stocke des glyphes à des positions fixes, ce qui signifie que ce qui émet le flux est responsable de décider dans quel ordre ces glyphes apparaissent. Sur l'écran, le système d'exploitation s'en chargeait pour vous : déposez de l'arabe dans un TEdit et la couche texte du système réordonne et joint les caractères avant que vous ne voyiez le moindre pixel. C'est précisément pourquoi la chaîne semble parfaite dans votre formulaire et se casse dans le PDF. Le bureau faisait le travail silencieusement, et dès que vous écrivez votre propre flux de contenu, le travail est de nouveau de votre côté.

TextOut vous prend au mot. Il dessine les points de code dans l'ordre où vous les passez, de gauche à droite, ce qui est correct pour le latin, le cyrillique et le CJK, et faux pour l'arabe et l'hébreu. RtLTextOut est l'appel qui réordonne la ligne en ordre visuel de droite à gauche d'abord, puis dessine. HotPDF garde délibérément les deux méthodes séparées plutôt que de deviner la direction depuis les caractères, de sorte que le choix de laquelle appeler est le choix du comportement de script que vous obtenez. La mécanique plus profonde du réordonnancement bidirectionnel et de la jonction contextuelle arabe est un sujet en soi, couvert dans l'article sur la mise en forme de texte arabe et RTL avec HotPDF ; ici le point pratique est plus étroit. Utilisez RtLTextOut pour les passages de droite à gauche, TextOut pour tout le reste, et ne faites jamais passer l'un par l'autre.

Schéma montrant comment RtLTextOut réordonne une ligne mixte arabe et latin en ordre visuel de droite à gauche avant de la dessiner dans un PDF
RtLTextOut réordonne chaque ligne en ordre visuel avant de dessiner : les passages de droite à gauche conservent leur séquence tandis que les mots latins et chiffres intégrés se lisent de gauche à droite à l'intérieur de la ligne

L'argument charset détermine le script

Ce qui indique à RtLTextOut s'il dispose de l'arabe ou de l'hébreu, ce n'est pas la méthode, c'est la police. SetFont prend un charset Windows comme quatrième argument, et cette valeur transporte les règles de script dans l'appel de droite à gauche : 178 sélectionne l'arabe, 177 sélectionne l'hébreu. Définissez le charset, puis dessinez, et les deux lignes ci-dessous sortent dans le bon ordre de lecture sans aucune configuration supplémentaire.

// Arabe : charset 178 indique à RtLTextOut d'appliquer les règles arabes
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hébreu : charset 177 bascule les règles vers l'hébreu
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Deux détails sur ces coordonnées sont faciles à manquer. La position que vous passez est toujours le début du passage dans le système de coordonnées propre à la page, mesuré depuis le coin inférieur gauche avec Y croissant vers le haut, la même origine que tout TextOut utilise ; RtLTextOut change l'ordre des glyphes, pas le point de mesure de la page. Et comme pour tout appel de dessin, le SetFont doit venir en premier et doit être répété après chaque AddPage, car la police courante ne survit pas à un saut de page. Oubliez la répétition et la seconde page revient à la police active à ce moment-là, ce qui pour l'arabe signifie généralement des cases vides.

Il ne renverse pas un texte que vous avez déjà retourné

L'erreur la plus fréquente qui engloutit le plus de temps de débogage est de fournir à RtLTextOut une chaîne déjà retournée manuellement. On arrive à cette méthode après une première tentative avec TextOut ordinaire qui donnait un résultat à l'envers, et un recours courant est d'inverser les caractères dans le code avant de dessiner. RtLTextOut inverse en interne de son propre chef, donc une chaîne pré-inversée est inversée une deuxième fois et atterrit là où elle était au départ. Passez le texte dans l'ordre logique, l'ordre dans lequel vous le tapez et le liriez à voix haute, et laissez l'appel faire le réordonnancement.

Le piège est plus vicieux qu'une simple inversion parce qu'une chaîne doublement inversée peut sembler correcte pour une phrase de test entièrement arabe, puis se casser dès qu'une ligne porte un mot latin ou un chiffre. À l'intérieur d'une ligne de droite à gauche, ces passages intégrés sont censés se lire de gauche à droite, et l'inversion manuelle détruit cet imbrication tandis que le cas purement arabe survit par hasard. Le bogue passe donc votre premier test minimal et ne se manifeste que plus tard sur une vraie facture avec un numéro de compte. Supprimez toute inversion manuelle dès que vous passez à RtLTextOut.

L'effet de bord Direction à connaître

Appeler RtLTextOut modifie plus que la ligne que vous dessinez. Cela bascule également la préférence de direction de lecture du document vers droite à gauche, la même chose que vous définiriez autrement via la propriété Direction. Ce setter ajoute vpDirection au ViewerPreferences du document, ce qui indique à un visualiseur comment disposer les planches en double page et de quel côté commence un agencement en pages en vis-à-vis. Quand le document entier est en arabe ou en hébreu, c'est exactement ce que vous voulez, et vous l'obtenez gratuitement.

Cela vaut la peine d'être connu précisément parce que c'est invisible sur une seule page. Si le document est principalement de gauche à droite avec un seul bloc de droite à gauche, le premier appel RtLTextOut basculera quand même la préférence de tout le fichier, et rien dans votre preuve d'une seule page ne le montrera. Le symptôme apparaît des semaines plus tard quand quelqu'un imprime un livret recto-verso et que les planches sortent en miroir. Si ce n'est pas ce que vous voulez, redéfinissez Direction explicitement après le passage de droite à gauche :

// RtLTextOut a déjà mis la direction du document sur RightToLeft ;
// restaure gauche à droite si le document est principalement LTR
Pdf.Direction := LeftToRight;

Pour un document qui se lit réellement de droite à gauche, laissez-le tel quel. Le point est de savoir que l'appel a un effet au niveau du document entier afin que la surprise de la brochure n'arrive jamais.

Enregistrez la police que vous livrez, pas celle que vous espérez installée

Rien du réordonnancement n'a d'importance si la police n'a pas de glyphes à dessiner. L'échec classique est un rapport qui s'affiche parfaitement sur la machine du développeur, où Arial Unicode MS se trouve être présente, et sort en rangées de cases vides sur le serveur d'un client où Windows a substitué silencieusement une police sans couverture arabe. Le remède est d'arrêter de faire confiance aux polices système installées et d'en enregistrer une que vous livrez avec l'application.

// Livrez une police arabe connue et enregistrez-la avant de dessiner
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Deux limites accompagnent l'enregistrement. Une police introduite via RegisterUnicodeTTF est incorporée, et la gestion Unicode incorporée de HotPDF nécessite un document en PDF 1.5 ou ultérieur ; cela ne pose problème que si quelque chose en aval insiste sur PDF 1.4, mais quand c'est le cas l'échec est silencieux. L'autre est juridique plutôt que technique : les fichiers TrueType portent des bits de permission d'incorporation, et une police qui semble correcte à l'écran peut être sous licence d'une façon qui interdit de la livrer à l'intérieur de documents clients. Confirmez la licence avant d'incorporer, pas après une plainte.

Un exemple console complet

Pour assembler les pièces, voici un programme autonome qui écrit une page avec une ligne arabe, une ligne hébraïque et une ligne mixte portant un nom de produit latin. Chaque bloc définit son charset, puis dessine dans l'ordre logique.

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // unité principale HotPDF

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // Un en-tête latin passe par le chemin TextOut ordinaire
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabe : charset 178, ordre logique, RtLTextOut fait le réordonnancement
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hébreu : charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Ligne mixte : le mot latin intégré se lit quand même de gauche à droite
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

Exécutez-le et ouvrez le résultat. Les lignes arabes et hébraïques se lisent de droite à gauche, les lettres se joignent là où le script les joint, et dans la dernière ligne le token HotPDF se trouve de gauche à droite à l'intérieur du passage arabe, ce qui est le résultat conforme à la spécification même si cela surprend quiconque voit la mise en page bidirectionnelle pour la première fois. Ce dernier point vaut la peine d'être inscrit dans vos critères d'acceptation avant qu'un lecteur natif examine la sortie, car le passage intégré se lisant dans le « mauvais » sens par rapport au script environnant est ce qui est le plus souvent signalé comme un bogue alors que ce n'en est pas un.

Vérifier la sortie

Une page qui semble correcte n'est pas la même chose qu'une page qui est correcte ; vérifiez-la donc comme le ferait un système en aval. Copiez le texte depuis le visualiseur et comparez les points de code avec votre chaîne source ; un ordre visuel correct avec un ordre logique brouillé est un vrai mode d'échec. Lancez la recherche dans le document du visualiseur pour un mot que vous pouvez voir sur la page. Ensuite, ouvrez le fichier sur une machine qui n'a pas vos polices de développement, celle la plus susceptible d'exposer une substitution silencieuse. Rien de tout cela ne remplace un locuteur natif lisant un vrai document, ce qui détecte des problèmes qu'aucune chaîne de test synthétique ne détectera, donc planifiez cette relecture avant que le format ne soit mis en production.

RtLTextOut gère le réordonnancement bidirectionnel et la jonction contextuelle arabe, ce qui couvre la grande majorité du travail sur les rapports et documents de droite à gauche. Là où il s'arrête, les scripts nécessitant plus que le réordonnancement et la jonction comme les familles indiques, et les fonctionnalités OpenType optionnelles qui passent par la substitution de glyphes uniques, est cartographié avec les détails de couverture de glyphes et de mise en forme dans l'article complémentaire sur la mise en forme de texte arabe et RTL avec HotPDF.

Les appels RtLTextOut, SetFont et RegisterUnicodeTTF présentés ici font partie du HotPDF Component pour Delphi et C++Builder.