Article technique

Mise en forme de texte arabe et de droite à gauche (RTL) dans les PDF Delphi avec HotPDF

Passez la phrase arabe يوضح ملف PDF à TextOut et ouvrez le résultat. Les lettres vont dans le mauvais sens, et chacune d'elles se trouve dans sa forme isolée avec un espace visible avant la suivante, comme si quelqu'un avait tapé de l'anglais à l'envers et avait appuyé sur la barre d'espace entre chaque caractère. Aucune exception n'a été déclenchée. Aucun avertissement n'a été imprimé. La sortie est tout simplement erronée, et elle est erronée parce que deux transformations distinctes dont dépend l'arabe ne se sont jamais produites. Savoir quelles sont ces deux transformations, et quel appel les exécute, résume l'essentiel de la sortie PDF à écriture complexe

HotPDF est un composant PDF VCL natif pour Delphi et C++Builder, et il effectue le travail de droite à gauche (right-to-left) pour vous via un appel distinct. Il s'arrête également à quelques endroits spécifiques que vous devez connaître avant de valider un paramètre régional (locale), de sorte que cet article cartographie les concepts et les limites honnêtes ; la configuration pratique de l'appel lui-même se trouve dans l'article de référence RtLTextOut

Pourquoi une chaîne correcte s'imprime toujours de manière erronée

Unicode conserve le texte dans l'ordre logique, l'ordre dans lequel vous le tapez et le lisez à haute voix. Un moteur de rendu doit placer les glyphes dans l'ordre visuel. Pour les écritures de gauche à droite, ces ordres coïncident et personne n'y pense. Pour l'arabe et l'hébreu, ce n'est pas le cas, et lorsqu'une seule ligne mélange les directions, par exemple une phrase arabe contenant le jeton latin "PDF" ou un prix écrit en chiffres, l'algorithme bidirectionnel Unicode (UAX #9) décide exactement comment les fragments de gauche à droite s'imbriquent dans la ligne de droite à gauche. C'est la première transformation, la réorganisation (reordering), et la sauter est ce qui retourne la ligne

La seconde est la mise en forme contextuelle (contextual shaping). Une lettre arabe est dessinée différemment selon l'endroit où elle se trouve dans un mot : initiale, médiane, finale ou isolée. Le point de code (codepoint) reste le même tout du long ; seul le glyphe change. Un pipeline qui transmet chaque point de code directement à son glyphe par défaut produit exactement la sortie déconnectée en forme isolée du paragraphe d'ouverture. L'hébreu saute cette étape, car ses lettres ne se joignent pas, mais il nécessite toujours la réorganisation. L'arabe a besoin des deux, et c'est pourquoi c'est avec l'arabe, et non l'hébreu, que vous effectuez vos tests

Sur le bureau, rien de tout cela n'est votre problème. Lorsqu'un formulaire VCL peint de l'arabe dans un TEdit, la pile de texte du système d'exploitation le réorganise et le met en forme discrètement, ce qui est précisément la raison pour laquelle la chaîne qui semble parfaite à l'écran ressort cassée dans un PDF naïf. Un flux de contenu ne stocke pas de texte modifiable. Il stocke des glyphes positionnés, donc quiconque émet le flux hérite du travail de mise en forme (shaping) que le système d'exploitation avait l'habitude de gérer. RtLTextOut est l'appel qui reprend ce travail

Ce que RtLTextOut met en forme pour vous

HotPDF conserve le chemin latin et le chemin d'écriture complexe comme deux méthodes différentes. TextOut imprime ce que vous lui donnez dans l'ordre où vous le donnez. RtLTextOut effectue d'abord les deux transformations — réorganisation bidirectionnelle sur toute la ligne, analyse contextuelle pour les écritures de jointure — puis l'imprime. Les règles de l'écriture qui s'appliquent transitent par le jeu de caractères (charset) de la police plutôt que par l'appel lui-même, de sorte que la direction est un choix explicite sur chaque site d'appel au lieu d'une supposition faite à partir des caractères. La configuration paramètre par paramètre, les valeurs de jeu de caractères, les étapes d'enregistrement de la police et un exemple complet compilable se trouvent tous dans l'article de référence RtLTextOut ; cet article s'en tient à la signification des transformations, à l'endroit où elles s'arrêtent et à la manière de prouver qu'elles ont fonctionné

Une règle d'utilisation est importante même à cette altitude : l'entrée doit être dans l'ordre logique, car RtLTextOut effectue l'inversion lui-même, et une chaîne que vous avez déjà retournée à la main ressort doublement inversée — l'article de référence passe en revue ce piège et son nettoyage. Ce qui vaut à ce piège d'être mentionné ici, c'est la raison pour laquelle il survit aux tests. Une chaîne en arabe pur doublement inversée peut sembler parfaitement correcte et ne s'effondre que lorsqu'une ligne porte un mot latin ou un nombre, car ces séquences intégrées (embedded runs) ne s'imbriquent plus de la manière dictée par UAX #9. Le bogue ne se trouve pas dans le rendu ; il s'agit de fournir à l'algorithme un texte qui était déjà à moitié traité

Ce même comportement de direction mixte fait trébucher les réviseurs plus qu'il ne fait trébucher le code. Dans une ligne de droite à gauche, les chiffres et les mots latins intégrés se lisent toujours de gauche à droite. Quelqu'un qui n'a pas travaillé avec une mise en page bidirectionnelle regardera une facture rendue, verra le numéro de compte lu dans le "mauvais" sens par rapport à l'arabe qui l'entoure, et le signalera comme un bogue. C'est le résultat correct selon les spécifications. Une brève note dans vos critères d'acceptation, rédigée avant le premier passage (pass) d'un locuteur natif, permet d'éviter cet aller-retour

Quand la réorganisation et la jointure suffisent, et quand elles ne suffisent pas

Pour le texte courant en arabe et en hébreu — rapports, factures, contrats, lettres — la réorganisation (reordering) et la jointure contextuelle représentent l'intégralité du travail, et RtLTextOut s'en charge à lui seul. La limite apparaît lorsque la typographie demande plus qu'une simple jointure. La réponse de HotPDF du côté arabe est un façonneur côté producteur (producer-side shaper) optionnel (opt-in) : définissez AutoShapeArabic := True et le composant réécrit la séquence en ordre logique en formulaires de présentation Unicode (Unicode Presentation Forms) avant le passage bidirectionnel, de sorte que les formes de jointure sont calculées par rapport aux voisins logiques et que les pliures de ligature sont intégrées dans les points de code que le PDF porte réellement, plutôt que d'être laissées à la résolution d'une visionneuse. Le commutateur (switch) est désactivé par défaut et la sortie est stable au niveau des octets (byte-stable) lorsqu'il reste désactivé, donc son activation est une décision délibérée par pipeline de documents, et non une mise à niveau globale. Le même modèle opt-in s'étend aux autres écritures de jointure de droite à gauche mises en forme par HotPDF : le syriaque, le n'ko, l'adlam et le rohingya hanifi ont chacun leur propre drapeau d'auto-mise en forme (auto-shape flag) qui reflète celui de l'arabe

Les fonctionnalités OpenType facultatives sont un autre mécanisme. Les ligatures discrétionnaires et les fonctionnalités de substitution unique similaires passent par GetSingleSubstituteGlyph(GID, 'liga'), qui résout une substitution à la fois — l'ID du glyphe d'entrée d'abord, la balise de fonctionnalité (feature tag) ensuite — et renvoie le glyphe d'entrée inchangé lorsque la fonctionnalité ne s'applique pas. C'est suffisant pour piloter une liste de ligatures finie et connue que vous gérez vous-même. Ce n'est pas un moteur GSUB complet, et la différence réside exactement là où les plans de paramètres régionaux ambitieux échouent : un pipeline de mise en forme qui gère parfaitement l'arabe a démontré la réorganisation et la jointure, rien de plus

Couverture sur l'ensemble des écritures (scripts)

L'arabe exerce les deux transformations, c'est pourquoi c'est avec cette chaîne qu'il faut tester, et pourquoi un passage en arabe est la preuve unique la plus solide du fonctionnement du pipeline. L'hébreu nécessite la réorganisation mais pas la jointure, car ses lettres sont isolées ; si le rendu de l'hébreu est correct mais que l'arabe sort déconnecté, la moitié bidirectionnelle va bien et la moitié contextuelle n'a jamais été exécutée. Le persan et l'ourdou utilisent l'écriture arabe et héritent de son comportement, bien que la préférence de l'ourdou pour le style nastaliq soit une décision de police avec des conséquences sur la lisibilité qu'un lecteur natif devrait juger

Le thaï se trouve entièrement de l'autre côté de la ligne. Il s'écrit de gauche à droite, il ne nécessite donc aucun travail bidirectionnel, et ses lettres ne se rejoignent pas, il n'a donc pas besoin d'analyse contextuelle ; les chaînes thaïlandaises passent par le chemin TextOut ordinaire comme le latin. Ce que le thaï possède, ce sont des marques empilées (stacked marks) — des voyelles et des marques de ton au-dessus et en dessous de la consonne de base — et le fait qu'elles soient correctement placées dépend de la capacité de la police à construire ses marques de combinaison pour qu'elles s'empilent sans l'aide du moteur de mise en forme. La plupart des polices thaïlandaises dédiées le font. Testez avec la police exacte que vous allez intégrer, pas avec un sosie

Le devanagari et le reste de la famille indienne (Indic) constituent un arrêt brutal et honnête. Leurs signes vocaliques se réorganisent autour des groupes de consonnes et leurs conjonctions (conjuncts) se forment à travers des chaînes de substitutions dépendantes du contexte, ce qui relève du territoire GSUB complet, au-delà de la réorganisation et de la jointure. Si un paramètre régional indien est sur la feuille de route, exécutez un véritable projet pilote (pilot) sur des chaînes client authentiques avant de le promettre — le fait que l'arabe fonctionne ne prouve pas que le devanagari fonctionnera. Les chaînes CJK (chinois, japonais, coréen), le vietnamien avec ses signes diacritiques empilés et le texte européen mixte empruntent tous le chemin ordinaire sans analyse bidirectionnelle, et il est payant de garder les deux chemins physiquement séparés dans le code du rapport, une routine pour les exécutions RTL (RTL runs) et une autre pour tout le reste, de sorte que la logique des paramètres régionaux soit visible sur le site d'appel au lieu d'être cachée derrière un drapeau (flag) que quelqu'un oublie de définir

La couverture des glyphes est décidée avant même que la mise en forme ne s'exécute

La mise en forme (shaping) sélectionne des glyphes dans une police. Si la police ne les contient pas, il n'y a rien à sélectionner, c'est pourquoi l'échec de déploiement classique — sans faille sur la machine du développeur, boîtes vides sur le serveur du client après une substitution de police silencieuse — est un problème de couverture, pas un problème de mise en forme. Le remède pratique, consistant à enregistrer une police que vous livrez au lieu de faire confiance à ce qu'une machine a installé, est expliqué étape par étape dans l'article de référence. Le point conceptuel est que la couverture doit être établie avant même que toute question de mise en forme ne soit significative, et qu'elle peut être établie par programmation au lieu d'être évaluée visuellement sur la sortie

// After RegisterUnicodeTTF, audit coverage for the
// codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

L'enregistrement lui-même comporte deux contraintes — un plancher PDF 1.5 pour la gestion d'Unicode intégrée et les bits d'autorisation d'intégration (embedding-permission bits) de la police — toutes deux couvertes aux côtés des étapes de configuration dans l'article de référence RtLTextOut. Ce qui a sa place ici, c'est l'habitude d'audit : GetUnicodeGlyphForCodepoint est votre système d'alerte précoce. Parcourez les plages de points de code (codepoint ranges) que vos données utilisent réellement au démarrage du service et consignez les ID de glyphes qui sont renvoyés. Un défaut de couverture apparaît alors comme une ligne dans un journal de démarrage lors du déploiement, plutôt que comme des caractères manquants dans une facture qui est déjà parvenue à un client

L'ordre de lecture appartient au document, pas aux glyphes

Même si chaque glyphe est correct, il reste une chose à faire. ISO 32000-1 §12.2 définit une préférence de visionneuse appelée /Direction qui indique l'ordre de lecture global du document. Elle ne touche aucun glyphe. Ce qu'elle fait, c'est dire à une visionneuse comment organiser les planches de deux pages (two-up spreads), de quel côté une disposition de pages en regard (facing-page layout) doit commencer, et de quel côté l'interface utilisateur de lecture (reading UI) doit pencher. Rien de tout cela ne s'affiche sur une seule page, c'est exactement la raison pour laquelle on l'oublie

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

Définir Direction est l'ensemble du travail : le définisseur de propriété (property setter) ajoute vpDirection à ViewerPreferences du document, de sorte qu'une seule ligne transporte la préférence dans le fichier. Si le texte sort par RtLTextOut, vous l'obtenez gratuitement, car l'appel inverse la direction du document comme effet secondaire — l'article de référence explique quand un document mixte nécessite l'annulation de cela. Le cas où vous devez le définir vous-même est un document de droite à gauche produit de toute autre manière, par exemple à partir d'une entrée que vous avez pré-façonnée en amont et dessinée via le chemin ordinaire. Omettez-le et l'épreuve d'une seule page que vous regardez est identique dans les deux cas ; puis quelqu'un imprime un livret recto verso, les planches sortent en miroir, et la cause est une ligne de code (one-liner) manquante datant de plusieurs semaines

Vérification de la sortie façonnée (shaped output)

Vérifiez de bout en bout, car une page peut sembler correcte tout en étant inutile pour tout ce qui se trouve en aval. Trois vérifications permettent de trouver la plupart des problèmes. Copiez le texte en dehors d'Acrobat et comparez les points de code à votre chaîne source. Exécutez la recherche dans le document de la visionneuse pour un mot que vous pouvez voir sur la page. Et ouvrez la sortie sur une machine qui n'a pas vos polices de développement, celle qui est la plus susceptible d'exposer une substitution. Rien de tout cela ne remplace un lecteur natif examinant un vrai document, ce qui permet de repérer des choses qu'aucun corpus synthétique ne fera. Programmez cet examen sur le calendrier avant la livraison (ships) du format

Choisissez délibérément les chaînes de test au lieu de recycler ce qu'un traducteur a envoyé l'année dernière. Un minimum viable par paramètre régional : une phrase en écriture pure (pure-script sentence), une phrase avec des noms de marques latines intégrés, une ligne portant des chiffres et de la monnaie, et des noms avec des signes diacritiques ou des marques de combinaison. Les noms de clients réels brisent les hypothèses que le texte de remplissage laisse intactes, alors laissez l'ensemble de régression s'agrandir d'une chaîne chaque fois qu'un cas d'assistance révèle un modèle que vous n'aviez pas vu

L'enregistrement des polices, le sous-ensemble (subsetting) et l'API de dessin de texte de tous les jours sont abordés dans l'article sur la sortie des rapports, les polices et les images avec HotPDF. Lorsque les mêmes documents doivent également répondre à des profils d'accessibilité, les règles de balisage de langue et de structure de l'article de validation PDF/A et PDF/UA s'ajoutent au travail de mise en forme détaillé ici

Les API de polices de droite à gauche et Unicode décrites ci-dessus sont fournies avec le Composant HotPDF pour Delphi et C++Builder ; la page du produit renvoie à la référence complète de la sortie de texte