Le PDF n'a aucune notion de police variable. Une police intégrée dans un fichier PDF est un ensemble fixe de contours avec des métriques fixes, donc une police variable doit être réduite à une seule instance statique avant de pouvoir entrer dans un document. HotPDF réalise cette instanciation en interne : vous inspectez les axes d'une police variable, vous sélectionnez des coordonnées telles qu'une graisse de 620 ou une largeur de 87,5, et la bibliothèque fige ces valeurs dans un programme de police complet et autonome que tout lecteur PDF conforme peut restituer
La raison en est pratique plutôt que théorique. Les fonderies de caractères livrent de plus en plus un seul fichier variable au lieu d'une douzaine de graisses statiques, et les équipes de design choisissent des valeurs qu'aucune instance nommée ne fournit. Sans instanciation, un générateur de rapports revient soit à l'instance par défaut, ce qui annule la décision de design, soit intègre toute la police variable en espérant que la visionneuse respecte des coordonnées d'axes qu'elle n'a aucun moyen de connaître, ce qu'aucun lecteur n'est tenu de faire
Que doit réellement reconstruire l'instanciation ?
Une police variable OpenType stocke un contour par défaut par glyphe, plus un ensemble de deltas indexés par position dans l'espace de conception. Appliquer une coordonnée d'axe ne consiste pas à écrire un nombre dans un en-tête ; cela signifie parcourir la table gvar, interpoler les deltas pour l'emplacement demandé, déplacer les points, puis recalculer tout ce qui découlait de ces points. HotPDF reconstruit les contours des glyphes, la table loca longue, les métriques horizontales et verticales complètes, la boîte englobante globale de la police et l'ajustement de somme de contrôle sfnt
Ce qui est retiré compte tout autant. Une instance statique ne doit pas conserver fvar, avar, gvar, HVAR, VVAR, MVAR, STAT ni cvar, et une DSIG devenue obsolète doit également disparaître, puisque les octets signés n'existent plus. Laisser subsister l'une de ces tables produit une police qui prétend être variable tout en portant des contours déjà déplacés, et les lecteurs qui appliquent effectivement les variations les appliqueront alors une seconde fois
Points fantômes, et le piège de la double application
La règle la plus subtile de tout le processus concerne les métriques. Dans gvar, le nombre de points d'un glyphe couvre les points de contour, ou les points de composant pour un glyphe composite, plus quatre points fantômes qui codent l'approche gauche, la largeur d'avance et leurs équivalents verticaux. Ces points fantômes sont eux-mêmes soumis à des deltas
Ainsi, lorsqu'une police possède une table gvar, HotPDF dérive les métriques horizontales et verticales à partir des points fantômes interpolés, sans appliquer en plus HVAR ou VVAR. Appliquer les deux est l'erreur classique : la même variation est appliquée deux fois et chaque largeur d'avance ressort légèrement trop large, ce qui se traduit par un texte qui dérive progressivement vers la droite sur une ligne justifiée. Ce n'est que lorsqu'une police n'a pas de gvar que la bibliothèque fige le magasin de variation des métriques directement dans hmtx ou vmtx
Deux autres détails garantissent l'exactitude de la géométrie. Les points fantômes ne participent jamais à l'interpolation de contour, donc les points non explicitement listés pour un glyphe simple sont déduits par IUP par contour, points fantômes exclus. Et les glyphes composites voient leurs deltas appliqués aux décalages de composants qui utilisent des paramètres XY, après quoi les limites des enfants sont recalculées récursivement. Cette récursion est bornée en profondeur et vérifiée contre les cycles, car un graphe de composants malveillant ou simplement défectueux pourrait sinon récurer indéfiniment
Inspecter l'espace de conception avant de choisir
Le premier appel de tout flux d'instanciation est InspectVariableFont, qui renvoie les axes et les instances nommées définies par la fonderie. Les enregistrements d'axe portent le tag de quatre octets, les valeurs minimale, par défaut et maximale, des indicateurs et un ID de nom ; les instances nommées portent un ID de nom de sous-famille, des indicateurs, un ID de nom PostScript optionnel et une coordonnée par axe :
var
Pdf: THotPDF;
Axes: THPDFVariableFontAxisArray;
Instances: THPDFVariableFontNamedInstanceArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
begin
for I := 0 to High(Axes) do
Writeln(Format('%s min=%.1f default=%.1f max=%.1f',
[string(Axes[I].Tag), Axes[I].MinimumValue,
Axes[I].DefaultValue, Axes[I].MaximumValue]));
Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
end
else
Writeln('not a variable font - embed it as an ordinary TrueType face');
finally
Pdf.Free;
end;
end;
Signaler la plage d'un axe importe car les valeurs d'axe sont bornées à la plage déclarée par la police, et non à la plage proposée par votre interface. Un curseur qui permet à un utilisateur de demander une graisse de 1000 sur une police dont l'axe wght s'arrête à 900 doit être corrigé dans l'interface, pas silencieusement au niveau de la police, sous peine que la sortie imprimée diverge de l'aperçu
Sélectionner des coordonnées et générer le document
La sélection d'axe est à état et s'applique aux polices enregistrées par la suite. SetVariableFontAxis prend un tag ASCII imprimable de quatre octets et une valeur finie, et rejette tout le reste par une exception plutôt que de l'ignorer silencieusement. ClearVariableFontAxes réinitialise la sélection, et GetVariableFontAxisSelections indique ce qui est actuellement en attente, ce qui vaut la peine d'être journalisé dans les moteurs de rapports où plusieurs chemins de code peuvent avoir touché le même objet document. La famille elle-même est sélectionnée par son nom via SetFont, exactement comme n'importe quelle autre police TrueType intégrée :
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.SetVariableFontAxis('wght', 620); // demi-gras, pas une instance nommée
Pdf.SetVariableFontAxis('wdth', 87.5); // légèrement condensée
Pdf.CurrentPage.SetFont('Inter', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');
Pdf.ClearVariableFontAxes; // retour à l'instance par défaut
Pdf.CurrentPage.SetFont('Inter', [], 10);
Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
L'événement OnVariableFontInstance se déclenche à chaque instance produite et indique les valeurs d'axe utilisées, ce qui est le moyen le plus simple de prouver dans un journal ce qu'un PDF donné contient réellement. Comme chaque ensemble de coordonnées distinct produit un programme de police distinct, traitez les sélections d'axes comme faisant partie de votre clé de cache de polices ; le fonctionnement du cache est décrit dans le cache persistant de sous-ensembles de polices
Comment l'instanciation interagit avec le sous-ensemblage et la mise en forme
L'instanciation s'exécute avant le sous-ensemblage, et cet ordre est le bon. La police instanciée est une police TrueType statique normale, donc le sous-ensembleur ordinaire la traite comme n'importe quelle autre : il calcule la fermeture des glyphes, conserve les glyphes réellement utilisés par le document et écarte le reste. L'interaction à connaître est que deux sélections d'axes différentes de la même famille correspondent à deux programmes de police différents, donc un document qui mélange la graisse 400 et la graisse 620 intègre deux sous-ensembles, et non une police partagée avec deux instances
La mise en forme n'est en principe pas affectée, mais mérite d'être vérifiée en pratique. Les fonctionnalités de mise en page résident dans GSUB et GPOS, que l'instanciation préserve, de sorte que les ligatures et les variantes stylistiques continuent de fonctionner comme décrit dans les variantes stylistiques OpenType GSUB. Ce qui change, c'est le positionnement : une instance condensée a des avances plus étroites que la valeur par défaut, donc toute mise en page qui mesure le texte avant l'instanciation mesure de mauvaises largeurs. Mesurez avec la même sélection d'axes que celle utilisée pour le rendu, et l'écart disparaît
Une dernière remarque défensive tirée de l'implémentation, utile à quiconque étend ce chemin de code. Les polices sans métriques verticales évaluent quand même les arguments de tableau dynamique au point d'appel Delphi, de sorte que les tableaux du moment de l'analyse sont toujours alloués plutôt que de s'appuyer sur une vérification HasVerticalMetrics pour court-circuiter un index vide. C'est le genre de détail au niveau du langage qui transforme une branche apparemment protégée en violation d'accès, précisément sur les polices que vous n'avez pas testées
La prise en charge des polices variables s'intègre au même pipeline de polices que l'intégration, le sous-ensemblage et la fermeture des glyphes, décrit plus en détail dans la fermeture de sous-ensemble de polices et les glyphes mis en forme. L'ensemble complet des fonctionnalités typographiques pour Delphi et C++Builder est répertorié sur la page du composant PDF Delphi HotPDF