Technical Article

Variantes stylistiques OpenType GSUB en Delphi pur

Un concepteur choisit une police dotée d'un a à un seul étage pour les en-têtes, d'un zéro barré pour les tableaux ou d'un jeu de capitales ornées pour une couverture. Ces glyphes existent déjà dans la police. Ils ne constituent simplement pas le choix par défaut. Le a par défaut est mappé du caractère au glyphe via la table cmap, tandis que la variante se situe à quelques identifiants de glyphes de là, accessible uniquement par une règle de substitution. Produire cette variante dans un PDF implique de lire la règle et d'émettre le glyphe de substitution dans le flux de contenu. Cet article traite de la lecture de ces règles de type substitution unique, en Object Pascal sans bibliothèque de mise en forme (shaping) native sous-jacente.

La portée est volontairement restreinte. Les jeux et variantes stylistiques sont des substitutions d'un glyphe par un autre glyphe. C'est la partie de la mise en page OpenType que vous pouvez résoudre par un parcours de table court et déterministe, ce qui en fait une solution idéale pour un moteur Pascal qui souhaite rester exempt de dépendances en C.

Pourquoi du Delphi pur plutôt que HarfBuzz

HarfBuzz est la réponse évidente pour la mise en forme du texte, et pour une mise en forme bidirectionnelle complète, indienne ou arabe, c'est la bonne réponse. C'est également une bibliothèque C. L'intégrer dans un produit Delphi ou C++Builder implique de livrer un objet natif pour chaque plateforme et architecture cible, de respecter sa convention d'appel, de suivre ses versions et de vérifier ses conditions de licence par rapport aux vôtres. Rien de tout cela n'est difficile individuellement. Mais c'est une friction permanente qui n'apporte rien lorsque le besoin réel est simplement de type : « donnez-moi la forme ss01 de cette lettre ».

Une substitution unique n'a pas besoin d'un moteur de mise en forme. Elle nécessite un analyseur pour quelques formats de sous-tables GSUB et une recherche binaire ou deux. Écrire cela en Pascal permet de conserver l'ensemble de la chaîne d'outils au sein d'un seul compilateur. La limite objective est que cette approche gère uniquement les recherches de substitution de glyphes. Il ne s'agit pas de résolution bidirectionnelle, ni de réorganisation des scripts indiens, ni de mise en forme contextuelle automatique. Là où ces fonctionnalités sont requises, elles sont nécessaires, et une requête de substitution unique ne saurait les remplacer.

La hiérarchie GSUB, de haut en bas

La table de substitution de glyphes (GSUB) est organisée comme une chaîne d'indirections, et une requête de substitution parcourt cette chaîne depuis le sommet. Au sommet se trouve la liste ScriptList. Une étiquette de script telle que latn sélectionne une entrée, et l'étiquette spéciale DFLT est le script par défaut appliqué lorsqu'un script plus spécifique ne correspond. L'entrée de script pointe vers un système de langue LangSys, avec un LangSys par défaut pour le cas général et des variantes nommées optionnelles pour les langues nécessitant un comportement différent. Le turc en est l'exemple classique, où les i avec et sans point exigent un traitement propre.

Le LangSys désigne un ensemble d'index de fonctionnalités. Chaque index pointe dans la FeatureList, où un enregistrement de fonctionnalité porte une étiquette de quatre octets, ss01 par exemple, et une liste d'index de recherche (lookups). Ces index pointent enfin vers la LookupList, où résident les sous-tables de substitution réelles. Résoudre ss01 signifie donc : trouver le script, trouver son LangSys, trouver la fonctionnalité dont l'étiquette est ss01, collecter les recherches qu'elle nomme et les appliquer. HotPDF cible par défaut le script DFLT et le LangSys par défaut, ce qui correspond à ce que livre la grande majorité des conceptions de textes latins, et il permet de surcharger l'étiquette de script lorsqu'une police organise ses fonctionnalités sous un script spécifique.

Les tables de couverture décident de la participation

Chaque sous-table de substitution commence par la même question : ce glyphe d'entrée participe-t-il à cette règle, et si oui, où se situe-t-il dans l'indexation propre de la règle. Cette question trouve sa réponse dans une table de couverture (Coverage table), et la réponse est un index de couverture, une petite valeur ordinale que le reste de la sous-table utilise pour rechercher ce que devient le glyphe.

La couverture se présente sous deux formats. Le format 1 est une liste d'identifiants de glyphes triés par ordre croissant. Vous trouvez un glyphe par recherche binaire, et sa position dans la liste constitue son index de couverture. Le format 2 est une liste d'enregistrements de plages, chacun comprenant un glyphe de début, un glyphe de fin et l'index de couverture auquel correspond le glyphe de début. Un glyphe situé dans une plage obtient son index de couverture en se décalant par rapport au début de la plage. Le format 1 est compact lorsque les glyphes participants sont dispersés, le format 2 lorsqu'ils forment des plages contiguës. Les deux formats sont triés, de sorte que les recherches s'effectuent en temps logarithmique, et renvoient soit un index de couverture, soit une indication d'absence de couverture qui permet au moteur de laisser le glyphe intact.

Substitution unique, les deux formats

La substitution unique est le type de recherche 1 (LookupType 1), et elle mappe un glyphe vers un unique remplaçant. Elle dispose également de deux formats, cette distinction étant une optimisation d'espace. Le format 1 stocke un unique delta signé. L'identifiant du glyphe de sortie est celui du glyphe d'entrée plus ce delta, modulo 65536. C'est ainsi qu'une police encode une substitution où chaque glyphe participant se situe à un décalage fixe constant par rapport à sa variante, par exemple un bloc de chiffres alignés placé à distance constante des chiffres elzéviriens correspondants. La table de couverture indique quels glyphes sont éligibles, et le delta unique sert pour tous.

Le format 2 stocke un tableau explicite d'identifiants de glyphes de substitution. L'index de couverture obtenu de la table de couverture sert d'index dans ce tableau, de sorte que le glyphe à l'index de couverture 0 devient le premier élément du tableau, l'index de couverture 1 le second, et ainsi de suite. Le format 2 est utilisé lorsque les variantes ne présentent pas un décalage uniforme, ce qui est le cas général pour les jeux stylistiques construits manuellement. La requête reste identique du côté de l'appelant dans les deux cas. On prend le glyphe d'entrée, on le passe dans la table de couverture et, s'il est couvert, on applique le delta ou on lit l'emplacement du tableau.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

Le comportement par défaut mérite d'être noté. GetSingleSubstituteGlyph renvoie l'identifiant du glyphe d'entrée inchangé à chaque échec : pas de police, pas de table GSUB, pas de fonctionnalité correspondante, aucun résultat de couverture. Cela signifie que l'appel peut être effectué de manière inconditionnelle en toute sécurité. Vous demandez la variante et, s'il n'y en a pas, vous obtenez exactement ce que vous avez fourni, de sorte que le code appelant n'a jamais besoin de traiter à part une police dépourvue de cette fonctionnalité.

Signification des étiquettes de fonctionnalités stylistiques

L'étiquette de fonctionnalité (tag) constitue l'ensemble du vocabulaire pour désigner la variante demandée, et la liste des étiquettes associées au travail stylistique est courte. La paire principale est salt, variantes stylistiques, l'accès universel aux formes alternatives d'un glyphe, et ss01 à ss20, les vingt jeux stylistiques numérotés qu'une police peut définir, chacun représentant un groupe nommé de substitutions assemblé par le concepteur. Une police peut placer un a à un seul étage et un R à jambe droite sous ss03, par exemple ; ainsi, activer ce seul jeu applique ce style aux deux.

// Try a stylistic-set feature, then fall back to plain alternates. function ResolveAlternate(Pdf: THotPDF; BaseGID: Word; const PreferredTag: AnsiString): Word; begin Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag); if Result = BaseGID then Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt'); // Still BaseGID if neither feature covers this glyph. end;
// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

Le format cmap 12 et les plans supplémentaires

Avant qu'une substitution ne puisse s'exécuter, un caractère doit devenir un glyphe, tâche qui incombe à la table cmap. La requête de substitution démarre d'un identifiant de glyphe, le chemin étant toujours caractère-vers-glyphe via cmap, puis glyphe-vers-variante via GSUB. L'aspect intéressant de cmap réside dans sa portée. Une sous-table au format 4 couvre le plan multilingue de base (Basic Multilingual Plane), soit les 65536 premiers points de code, ce qui suffit pour la plupart des textes latins. Cela ne suffit pas pour les points de code à partir de U+10000, les plans supplémentaires, où résident désormais les caractères alphanumériques mathématiques, de nombreux symboles et plusieurs écritures vivantes.

Le format 12 est la sous-table couvrant l'ensemble de la plage U+0000 à U+10FFFF. Il s'agit d'une liste triée de groupes, chaque groupe comprenant un point de code de début, un point de code de fin et un identifiant de glyphe de début, de sorte qu'une suite contiguë de points de code correspond à une suite contiguë de glyphes. HotPDF résout les points de code à l'aide d'une stratégie hybride adaptée à la structure des données. Les points de code situés dans le BMP sont servis depuis un tableau direct indexé par le point de code (une recherche unique directe). Les points de code des plans supplémentaires sont servis depuis une table creuse triée par point de code et parcourue par recherche binaire. Le résultat est que GetUnicodeGlyphForCodepoint prend un type Cardinal complet et répond correctement sur toute la plage, renvoyant l'identifiant de glyphe 0, le glyphe .notdef, pour tout point de code non mappé par la police.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

Où s'arrêtent ces requêtes

Les API de substitution unique répondent à une forme de question spécifique, et il convient de préciser ce qu'elles ne traitent pas. Le type LookupType 1 est l'un des huit types de substitution. La requête ne gère pas la substitution multiple (LookupType 2), où un glyphe se transforme en plusieurs, ni la substitution de ligature (LookupType 4), où plusieurs glyphes s'assemblent en un seul. Elle ne gère pas non plus les types contextuels et contextuels en chaîne (LookupTypes 5 et 6), qui ne s'activent que lorsqu'un glyphe apparaît dans un voisinage particulier, ni les types d'extension et de chaîne inverse. Une fraction diagonale, un groupe dévanagari ou une cascade arabe initiale-médiale-finale sont des problèmes de séquence qu'une recherche de substitution unique par glyphe ne saurait exprimer.

Elle n'effectue pas non plus de mise en forme automatique. Aucun élément ici n'inspecte un bloc de texte pour décider des fonctionnalités à activer et les appliquer dans l'ordre requis par le script. L'appelant choisit l'étiquette de fonctionnalité et l'applique glyphe par glyphe. C'est précisément l'outil idéal pour les jeux et variantes stylistiques, qui sont locaux et activés sur option, et précisément le mauvais outil pour un script nécessitant une réorganisation. Maintenir cette frontière nette permet au chemin de substitution de rester court et prévisible.

Pour les cas nécessitant un travail au niveau de la séquence, l'aspect des écritures complexes est traité dans notre article sur la mise en forme de textes à écritures complexes dans Delphi. Si vos substitutions s'inscrivent dans un travail d'édition plus large plaçant également des images et d'autres polices sur la page, le guide sur la sortie de rapports avec polices et images détaille l'assemblage de ces éléments. Tous s'exécutent sur le même moteur, le composant HotPDF pour Delphi et C++Builder, qui prend en charge les requêtes de substitution GSUB en plus des API d'intégration de polices, de sous-ensembles (subsetting) et de texte traitées par ailleurs sur ce blog.