Article technique

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

Envoyez la phrase arabe يوضح ملف PDF هذا à un simple TextOut et la page qui revient est fausse de deux manières à la fois. Les mots s'écrivent de gauche à droite au lieu de droite à gauche, et les lettres se séparent dans leurs formes isolées au lieu de se joindre en mots connectés. Aucune erreur. Le code Delphi est compilé, le fichier s'ouvre et un réviseur qui lit l'arabe vous dit que la sortie est inutilisable. Le correctif est un appel, pas un échange de bibliothèque (library swap) : HotPDF achemine (routes) le texte de droite à gauche via une méthode distincte, RtLTextOut, qui gère le réordonnancement (reordering) que le simple TextOut ne fera pas. Cette page est la référence de travail pour cette méthode : la signature et ses paramètres, l'argument de jeu de caractères (charset) qui sélectionne le script, l'effet secondaire (side effect) au niveau du document, la configuration de la police qui doit venir en premier et les échecs qui parviennent réellement au support, chacun avec son correctif

Signature et paramètres

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X et Y ancrent la séquence (run) dans le propre système de coordonnées de la page, mesuré à partir du coin inférieur gauche avec Y augmentant vers le haut, la même origine que chaque appel TextOut utilise ; RtLTextOut modifie l'ordre des glyphes, et non l'endroit à partir duquel la page mesure. angle fait pivoter la ligne de base (baseline) exactement comme dans TextOut, donc 0 dessine une ligne horizontale. Text est la chaîne dans l'ordre logique, l'ordre dans lequel vous la taperiez, et la deuxième surcharge (overload) prend les mêmes données UTF-16 qu'un tampon (buffer) PWORD brut avec un nombre d'unités de code explicite, ce qui est la forme à utiliser lorsque le texte arrive d'une API plutôt que d'une chaîne Delphi. Sur les anciennes versions de Delphi antérieures à la résolution de surcharge pour ces types, la forme de chaîne est exposée sous le nom RtLTextOutStr avec la liste de paramètres identique

La division du travail entre les deux appels de sortie est stricte. TextOut dessine les points de code (codepoints) dans l'ordre dans lequel vous les passez, ce qui est correct pour le latin, le cyrillique et le CJK et faux pour l'arabe et l'hébreu. RtLTextOut réorganise d'abord chaque ligne dans l'ordre visuel de droite à gauche, puis dessine, en gardant les mots latins et les chiffres intégrés (embedded) lus de gauche à droite à l'intérieur de la ligne. HotPDF garde les deux méthodes délibérément séparées plutôt que de deviner la direction à partir des caractères, de sorte que le choix de celle à appeler est le choix du comportement de script que vous obtenez ; utilisez RtLTextOut pour les séquences de droite à gauche, TextOut pour tout le reste, et ne jamais acheminer (route) l'une via l'autre. Pourquoi le réordonnancement existe, ce que font réellement l'algorithme bidirectionnel Unicode (Unicode Bidirectional Algorithm) et la jointure contextuelle arabe (Arabic contextual joining), et où s'arrête la mise en forme (shaping) de HotPDF font l'objet de l'article d'accompagnement sur La mise en forme de textes arabes et RTL avec HotPDF ; tout ce qui suit est la configuration pratique

Schéma de la façon dont RtLTextOut réorganise une ligne mixte arabe et latin dans l'ordre visuel de droite à gauche avant de la dessiner dans un PDF
RtLTextOut réorganise chaque ligne dans l'ordre visuel avant le dessin : les séquences de droite à gauche conservent leur séquence tandis que les mots latins et les chiffres intégrés se lisent de gauche à droite à l'intérieur de la ligne.

L'argument charset décide du script

Ce qui indique à RtLTextOut s'il met en page de l'arabe ou de l'hébreu n'est pas la méthode, c'est la police. SetFont prend un jeu de caractères (charset) Windows comme quatrième argument, et cette valeur porte 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 l'ordre de lecture correct sans aucune autre configuration

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Un détail de séquençage est facile à manquer : le SetFont doit venir en premier et doit être répété après chaque AddPage, car la police actuelle, y compris le jeu de caractères (charset), ne survit pas à un saut de page (page break). Oubliez la répétition et la deuxième page se rabat sur la police active, ce qui pour l'arabe signifie généralement des cases vides (empty boxes)

Il n'inverse pas le texte que vous avez déjà inversé

La seule erreur qui engloutit le plus de temps de débogage ici est de fournir (feeding) à RtLTextOut une chaîne que vous avez déjà inversée à la main. Les gens atteignent cette méthode après une première tentative avec un simple TextOut qui est sortie à l'envers, et un pis-aller (stopgap) courant consiste à inverser les caractères dans le code avant de dessiner. RtLTextOut s'inverse en interne par lui-même, de sorte qu'une chaîne pré-inversée s'inverse une seconde fois et atterrit exactement là où elle a commencé. Passez le texte dans l'ordre logique, l'ordre dans lequel vous le taperiez et le liriez à haute voix, et laissez l'appel effectuer le réordonnancement

Le piège est plus méchant (nastier) qu'un simple basculement (flip) car une chaîne doublement inversée peut sembler correcte pour une phrase de test entièrement arabe, puis se briser (break) à l'instant où une ligne porte un mot latin ou un chiffre. À l'intérieur d'une ligne de droite à gauche, ces séquences intégrées (embedded runs) sont censées se lire de gauche à droite, et l'inversion manuelle ruine (wrecks) cette imbrication (nesting) tandis que le cas purement arabe survit (survives it). Ainsi, le bogue passe votre premier test de fumée (smoke test) et fait surface plus tard sur une vraie facture contenant un numéro de compte. Supprimez (Strip out) chaque inversion manuelle au moment où vous passez à RtLTextOut

L'effet secondaire de Direction (Direction side effect) à connaître

L'appel de RtLTextOut modifie plus que la ligne que vous dessinez. Il fait également basculer (flips) la préférence de sens de lecture (reading-direction preference) du document de droite à gauche, la même chose que vous définiriez vous-même via la propriété Direction. Ce setter ajoute vpDirection aux ViewerPreferences du document, ce qui indique à une visionneuse comment organiser les planches de deux pages (two-up spreads) et de quel côté commence une mise en page en vis-à-vis (facing-page layout). Lorsque le document entier est en arabe ou en hébreu, c'est exactement ce que vous voulez, et vous l'obtenez gratuitement

Il vaut la peine de le savoir précisément parce qu'il est invisible sur une seule page. Si le document est principalement de gauche à droite avec un bloc de droite à gauche, le premier appel RtLTextOut fera toujours basculer (tip) la préférence de l'ensemble du fichier, et rien dans votre épreuve (proof) d'une page ne le montrera. Le symptôme apparaît des semaines plus tard lorsque quelqu'un imprime un livret recto-verso (duplex booklet) et que les doubles pages (spreads) sortent en miroir (mirrored). Si ce n'est pas ce que vous voulez, remettez Direction explicitement après la séquence de droite à gauche :

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

Pour un document qui se lit véritablement (genuinely) de droite à gauche, n'y touchez pas. L'important est de savoir que l'appel a un effet à l'échelle du document afin que la surprise du livret ne se produise jamais

Enregistrez la police que vous livrez (ship), pas celle que vous espérez être installée

Rien du réordonnancement (reordering) n'a d'importance si la police n'a aucun glyphe à dessiner. L'échec classique est un rapport qui s'affiche parfaitement (flawlessly) sur la machine du développeur, où Arial Unicode MS est présent, et sort sous forme de rangées de cases vides (empty boxes) sur le serveur d'un client où Windows a discrètement (quietly) substitué une police sans aucune couverture arabe. Le remède consiste à cesser de faire confiance aux polices système installées et à enregistrer celle que vous livrez avec l'application

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Deux limites (boundaries) accompagnent (ride along with) l'enregistrement. Une police introduite via RegisterUnicodeTTF est intégrée (embedded), et la gestion Unicode intégrée de HotPDF a besoin du document au format PDF 1.5 ou ultérieur ; cela ne pose problème (bites) que si quelque chose en aval (downstream) insiste sur le PDF 1.4, mais lorsque c'est le cas, l'échec est silencieux. L'autre est légal plutôt que technique : les fichiers TrueType portent des bits d'autorisation d'intégration (embedding-permission bits), et une police qui semble correcte à l'écran peut faire l'objet d'une licence qui interdit de la livrer dans les documents clients. Confirmez la licence avant de l'intégrer, pas après une plainte

Un exemple de console complet

En assemblant les éléments (pieces), 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;   // HotPDF main unit

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

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

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

    // Mixed line: the embedded Latin word still reads left to right
    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 arabe et hébraïque se lisent de droite à gauche, les lettres se rejoignent là où le script les rejoint, et dans la dernière ligne, le jeton (token) HotPDF se trouve de gauche à droite à l'intérieur de la séquence arabe. Cette imbrication (nesting) est le résultat bidirectionnel correct, pas un bogue, même si les réviseurs débutants (first-time reviewers) le signalent (file it) régulièrement comme tel ; l'article sur la mise en forme lié ci-dessus explique pourquoi les règles Unicode l'exigent et comment formuler vos critères d'acceptation afin que le rapport ne soit jamais déposé (filed)

Erreurs courantes et leurs correctifs

Chaque échec ci-dessous est apparu dans un vrai fil de discussion de support, et chacun remonte à l'une des sections ci-dessus

  • La sortie se lit à l'envers ou s'embrouille (scrambles) sur des lignes mixtes — la chaîne a été inversée à la main avant l'appel, généralement une solution de contournement (workaround) restante d'une tentative de TextOut. Supprimez toute inversion manuelle et passez l'ordre logique ; RtLTextOut s'inverse en interne
  • Les lettres s'impriment déconnectées sous des formes isolées — le texte est passé par un simple TextOut, ou SetFont a été appelé sans un charset de droite à gauche. Dessinez avec RtLTextOut et passez 178 pour l'arabe ou 177 pour l'hébreu comme quatrième argument SetFont
  • Cases vides (Empty boxes) sur la machine du client — Windows a substitué une police sans aucune couverture arabe ou hébraïque. Arrêtez de nommer les polices installées ; enregistrez une police que vous livrez via RegisterUnicodeTTF et appelez SetFont avec ce nom
  • La deuxième page s'affiche dans la mauvaise police — la police actuelle ne survit pas à AddPage. Répétez l'appel SetFont, jeu de caractères inclus, après chaque saut de page
  • Les doubles pages recto-verso (Duplex spreads) s'impriment en miroir sur un document principalement LTR — le premier appel RtLTextOut a inversé (flipped) la Direction du document comme effet secondaire. Réglez Pdf.Direction := LeftToRight après la séquence de droite à gauche
  • Le texte Unicode intégré se dégrade (degrades) silencieusement en aval — quelque chose dans le pipeline force le PDF 1.4, et la gestion Unicode intégrée de HotPDF nécessite la version 1.5 ou ultérieure. Augmentez la version du document ou supprimez la contrainte en aval (downstream constraint)

Avant l'expédition (ships) du format, vérifiez au-delà de l'inspection visuelle (past eyeballing) : copiez le texte depuis la visionneuse, exécutez la recherche dans le document, ouvrez le fichier sur une machine sans vos polices de développement, et mettez un document authentique devant un lecteur natif. La liste de vérification complète (full verification checklist), la carte de couverture par script et le corpus de chaînes de test qui valent la peine d'être construits se trouvent tous dans l'article d'accompagnement sur La mise en forme de textes arabes et RTL avec HotPDF

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