Un designer choisit une police avec un a à un seul niveau pour les titres, ou un zéro barré pour les tableaux, ou un ensemble de majuscules ornées (swash) pour une couverture. Ces glyphes se trouvent déjà dans la police. Ce ne sont tout simplement pas ceux par défaut. Le a par défaut passe du caractère au glyphe via la table cmap, et la variante se trouve à quelques identifiants de glyphe de distance, accessible uniquement par une règle de substitution. Produire cette variante dans un PDF signifie lire la règle et émettre le glyphe de substitution dans le flux de contenu. Cet article traite de la lecture de ces règles, celles du type à substitution simple, en Object Pascal sans aucune bibliothèque de mise en forme (shaping) native sous-jacente
La portée est délibérément restreinte. Les jeux stylistiques et les variantes sont des substitutions avec une entrée d'un seul glyphe et une sortie d'un seul glyphe. Il s'agit de la partie de la disposition OpenType que vous pouvez résoudre avec un petit parcours de table déterministe, ce qui en fait un bon choix pour un moteur Pascal qui souhaite rester exempt de dépendances C
Pourquoi du Delphi pur plutôt que HarfBuzz
HarfBuzz est la réponse évidente à "mets en forme ce texte", et pour la mise en forme bidirectionnelle complète, indienne ou arabe, c'est la bonne réponse. C'est également une bibliothèque C. La lier à un produit Delphi ou C++Builder signifie livrer un objet natif pour chaque plateforme et architecture cible, respecter sa convention d'appel, suivre son rythme de publication et lire ses conditions de licence par rapport aux vôtres. Rien de tout cela n'est difficile isolément. Mais tout cela constitue une friction qui ne disparaît jamais, et qui n'apporte rien lorsque l'exigence réelle est "donne-moi la forme ss01 de cette lettre"
La substitution simple ne nécessite pas de moteur de mise en forme. Elle nécessite un analyseur (parser) pour une poignée de formats de sous-tables GSUB et une recherche binaire ou deux. L'écrire en Pascal maintient l'ensemble de la chaîne d'outils à l'intérieur d'un seul compilateur. La limite honnête est que cette approche gère les recherches de substitution de glyphes et rien d'autre. Il ne s'agit pas d'une résolution bidirectionnelle, ni d'une réorganisation indienne, ni d'une mise en forme contextuelle automatique. Là où celles-ci sont nécessaires, elles sont nécessaires, et une requête de substitution simple ne pourra pas les remplacer
La hiérarchie GSUB, de haut en bas
La table de substitution de glyphes (Glyph Substitution) est organisée comme une chaîne d'indirections, et une requête de substitution parcourt la chaîne à partir du haut. Au sommet se trouve la ScriptList. Une balise de script (script tag) telle que latn sélectionne une entrée, et la balise spéciale DFLT est le script par défaut qui s'applique lorsqu'aucun script plus spécifique ne correspond. L'entrée de script pointe vers un LangSys, le système linguistique, avec un LangSys par défaut pour le cas courant et d'autres nommés facultatifs pour les langues qui nécessitent un comportement différent. Le turc en est l'exemple habituel, où les i avec et sans point exigent leur propre traitement
Le LangSys nomme un ensemble d'index de fonctionnalités. Chaque index pointe dans la FeatureList, où un enregistrement de fonctionnalité porte une balise de quatre octets, ss01 parmi elles, et une liste d'index de recherche (lookup indices). Ces index pointent finalement vers la LookupList, où se trouvent les sous-tables de substitution proprement dites. Ainsi, résoudre ss01 signifie : trouver le script, trouver son LangSys, trouver la fonctionnalité dont la balise est ss01, collecter les recherches qu'elle nomme, et les appliquer. HotPDF utilise par défaut le script DFLT et le LangSys par défaut, ce qui correspond à ce que livre la grande majorité des conceptions de texte latin, et il offre un moyen de remplacer la balise de script lorsqu'une police relie plutôt ses fonctionnalités à un script spécifique
Les tables de couverture (Coverage) décident qui participe
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), et la réponse est un index de couverture, un petit nombre ordinal 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 avec une recherche binaire, et sa position dans la liste constitue son index de couverture. Le format 2 est une liste d'enregistrements de plages (range records), chacun comprenant un glyphe de début, un glyphe de fin, et l'index de couverture vers lequel le glyphe de début pointe. Un glyphe à l'intérieur d'une plage obtient son index de couverture par décalage (offset) depuis le début de la plage. Le format 1 est compact lorsque les glyphes participants sont dispersés, le format 2 l'est lorsqu'ils se trouvent dans des séries contiguës. Les deux sont triés, donc les deux sont recherchés en temps logarithmique, et les deux renvoient soit un index de couverture, soit un résultat clair "non couvert" qui permet au moteur de laisser le glyphe tranquille
La substitution simple, les deux formats
La substitution simple (Single Substitution) correspond au LookupType 1, et elle mappe un glyphe vers un seul remplacement exactement. Elle possède également deux formats, et cette division est une optimisation de l'espace. Le format 1 stocke un delta signé unique. L'identifiant du glyphe de sortie est l'identifiant 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 au même décalage fixe par rapport à sa variante, par exemple un bloc de chiffres alignés placés à une distance constante des chiffres elzéviriens correspondants. La table de couverture indique quels glyphes sont qualifiés, et ce seul delta sert à tous
Le format 2 stocke un tableau explicite d'identifiants de glyphes de substitution. L'index de couverture issu de la table de couverture est l'index dans ce tableau, de sorte que le glyphe à l'index de couverture 0 devient la première entrée du tableau, l'index de couverture 1 la deuxième, et ainsi de suite. Le format 2 est utilisé lorsque les variantes ne sont pas à un décalage uniforme, ce qui est le cas courant pour les jeux stylistiques construits à la main. La requête est la même du côté de l'appelant dans les deux cas. Prenez le glyphe d'entrée, passez-le dans la couverture (Coverage), et s'il est couvert, appliquez le delta ou lisez la case 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, []);
// Glyphe par défaut pour 'a' via la table cmap de la police.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Jeu stylistique 1 (Stylistic Set 1) : résoudre la variante via GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID signifie que la fonctionnalité n'a pas touché à ce glyphe.
if AltGID <> BaseGID then
{ émettre AltGID dans le flux de contenu } ;
finally
Pdf.Free;
end;
end;
Le contrat qui mérite d'être noté est la transmission telle quelle (pass-through). GetSingleSubstituteGlyph renvoie l'identifiant du glyphe d'entrée inchangé à chaque échec : pas de police, pas de table GSUB, pas de fonctionnalité correspondante, pas d'occurrence 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 récupérez exactement ce que vous avez mis, de sorte que le code appelant n'a jamais besoin de gérer de cas particulier pour une police dépourvue de la fonctionnalité
La signification des balises de fonctionnalités stylistiques
La balise de fonctionnalité (feature tag) constitue le vocabulaire complet de la variante que vous demandez, et les balises pertinentes pour le travail stylistique forment une liste courte. La paire principale est salt, les variantes stylistiques (stylistic alternates), l'accès global aux formes variantes d'un glyphe, et ss01 à ss20, les vingt jeux stylistiques numérotés qu'une police peut définir, chacun étant un groupe nommé de substitutions que le designer rassemble. Une police pourrait mettre un a à un seul niveau et un R à jambe droite sous ss03, par exemple, de sorte que l'activation de ce seul jeu modifie le style des deux
Autour d'elles se trouvent plusieurs autres balises de substitution simple. aalt permet d'accéder à toutes les variantes (access-all-alternates), l'union de chaque variante d'un glyphe, généralement présentée sous forme de palette de glyphes. titl sélectionne les majuscules de titrage taillées pour les grandes tailles. subs et sups substituent de vrais chiffres en indice et en exposant plutôt que des chiffres par défaut mis à l'échelle. ordn produit des formes ordinales, les lettres en exposant dans 1st et 2nd. frac construit des fractions, bien que les fractions diagonales complètes s'appuient également sur une logique de ligature et contextuelle qui va au-delà de la substitution simple de base. Pour les cas à glyphe unique, le mécanisme est identique à ss01 : passez la balise à la requête de substitution et lisez le glyphe de substitution en retour
// Essayer une fonctionnalité de jeu stylistique, puis se rabattre sur les variantes simples.
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');
// Reste BaseGID si aucune fonctionnalité ne couvre ce glyphe.
end;
Le format 12 de la cmap et les plans supplémentaires
Avant que toute substitution puisse s'exécuter, un caractère doit devenir un glyphe, et c'est le travail de la table cmap. La requête de substitution part d'un identifiant de glyphe, donc le chemin va toujours du caractère au glyphe via cmap, puis du glyphe à la variante via GSUB. La partie intéressante de cmap est sa portée. Une sous-table de format 4 couvre le plan multilingue de base (Basic Multilingual Plane - BMP), les 65536 premiers points de code, et c'est suffisant pour la plupart des textes latins. Cela ne l'est pas pour les points de code à partir de U+10000, les plans supplémentaires, où se trouvent désormais les caractères alphanumériques mathématiques, de nombreux symboles et plusieurs écritures vivantes
Le format 12 est la sous-table qui couvre la totalité de la plage U+0000 à U+10FFFF. Il s'agit d'une liste triée de groupes, chaque groupe étant composé d'un point de code de début, d'un point de code de fin et d'un identifiant de glyphe de début, de sorte qu'une suite contiguë de points de code est mappée sur une suite contiguë de glyphes. HotPDF résout les points de code avec une stratégie hybride qui correspond à la forme des données. Les points de code dans le BMP sont servis à partir d'un tableau direct indexé par le point de code, une simple recherche sans parcours. Les points de code dans les plans supplémentaires sont servis à partir d'une table creuse triée par point de code et recherchée avec une recherche binaire. Le résultat est que GetUnicodeGlyphForCodepoint prend un Cardinal complet et répond correctement sur toute la plage, renvoyant l'identifiant de glyphe 0, le glyphe .notdef, pour tout point de code que la police ne mappe pas
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// Un point de code de plan supplémentaire : U+1D49C A MAJUSCULE MATHÉMATIQUE EN ÉCRITURE CURSIVE.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // recherche au format 12
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // la police n'a pas de glyphe pour ce point de code
end;
Là où ces requêtes s'arrêtent
Les API de substitution simple répondent à un type de question, et il est utile d'être clair sur ce à quoi elles ne répondent pas. LookupType 1 est l'un des huit types de substitution. La requête ne gère pas la substitution multiple de LookupType 2, où un glyphe en devient plusieurs, ni la substitution de ligature de LookupType 4, où plusieurs glyphes en deviennent un. Elle ne gère pas les types contextuels et contextuels enchaînés, LookupTypes 5 et 6, qui ne se déclenchent que lorsqu'un glyphe apparaît dans un voisinage particulier, ni les types d'extension et de chaînage inverse (reverse-chaining). Une fraction diagonale, un ensemble Devanagari ou une cascade initiale-médiane-finale arabe constituent un problème de séquence, et une recherche de substitution simple par glyphe ne peut pas l'exprimer
Elle n'effectue pas non plus de mise en forme (shaping) automatique. Rien ici n'inspecte une suite de texte, ne décide quelles fonctionnalités activer, ni ne les applique dans l'ordre requis par le script. L'appelant choisit la balise de fonctionnalité et l'applique glyphe par glyphe. C'est exactement le bon outil pour les jeux stylistiques et les variantes, qui sont optionnels et locaux, et c'est exactement le mauvais outil pour un script nécessitant une réorganisation. Garder une frontière nette permet au chemin de substitution de rester petit et prévisible
Pour les cas qui nécessitent effectivement un travail au niveau de la séquence, le sujet des écritures complexes est abordé dans notre article sur la mise en forme de textes à écritures complexes dans Delphi. Si vos substitutions font partie d'une tâche de génération de rapports plus vaste qui place également des images et d'autres polices sur la page, le guide de sortie de rapports avec polices et images explique comment ces éléments s'articulent. Tous fonctionnent sur le même moteur, le Composant HotPDF pour Delphi et C++Builder, qui intègre les requêtes de substitution GSUB aux côtés des API d'intégration de polices, de création de sous-ensembles et de texte traitées ailleurs sur ce blog