HotPDF effectue le rendu des polices PDF intégrées dans Delphi sans rien installer sur la machine : le pipeline de rendu de glyphes intégrés de HPDFGlyphRender.pas analyse les programmes de polices stockés dans le PDF lui-même — les contours TrueType glyf de FontFile2, les charstrings CFF Type 2 de FontFile3, ainsi que les flux de contenu de glyphes Type 3 — et les restitue sous forme de tracés vectoriels GDI remplis. Cet article constitue l'analyse approfondie de la fidélité des polices liée au rendu de pages PDF vers un TBitmap avec HotPDF : cet autre article traite du moteur de rendu dans son ensemble, celui-ci explique comment le texte de ces pages obtient ses formes exactes
Pourquoi un PDF s'affiche-t-il avec des carrés à la place du texte ?
Des carrés (tofu), des vides ou des caractères légèrement erronés dans le rendu PDF signifient presque toujours que le moteur de rendu a demandé une police au système d'exploitation au lieu d'utiliser celle intégrée dans le fichier. Le problème survient de la même manière à chaque fois : le document semble parfait sur la machine qui l'a produit, puis un client l'ouvre sur un serveur vierge ou un poste restreint, et la facture japonaise affiche des carrés vides, ou encore une police de substitution décale chaque retour à la ligne. Les polices n'ont jamais été présentes sur cette machine — uniquement dans le PDF — et un moteur de rendu qui se limite à la substitution par des polices système ne peut pas les afficher. Les polices partielles (subset fonts) aggravent la situation : un sous-ensemble peut comporter quarante glyphes sous des codes de caractères attribués de manière privée à ce seul fichier, une attribution qu'aucune police installée ne partage
La norme ISO 32000-1 §9.9 définit trois conteneurs pour un programme de police intégrée dans le descripteur de police : FontFile contient un programme Type 1 d'origine, FontFile2 un programme TrueType, et FontFile3 un programme CFF brut (Type1C ou CIDFontType0C) ou une enveloppe OpenType. Une quatrième variante, la police Type 3 de la norme ISO 32000-1 §9.6.5, n'intègre aucun élément binaire — chaque glyphe est un petit flux de contenu PDF exécuté sur place. Les trois conteneurs diffèrent par les mathématiques de leurs contours (B-splines quadratiques, charstrings cubiques ou opérateurs de page arbitraires). Un moteur de rendu fidèle requiert donc un interpréteur distinct pour chacun d'eux, complété par une couche d'encodage qui associe les codes de caractères au bon index de glyphe avant tout tracé
Comment HotPDF convertit-il les contours glyf TrueType en tracés GDI ?
THPDFEmbeddedTTF dans HPDFGlyphRender.pas lit la table loca pour localiser chaque enregistrement de glyphe, parcourt les contours glyf point par point et émet un tracé GDI. Deux conventions TrueType requièrent un traitement explicite. Premièrement, des points hors-courbe consécutifs impliquent un point sur la courbe à leur milieu, et un contour dont tous les points sont hors-courbe commence au milieu de ses derniers et premiers points — omettez l'une de ces règles et les glyphes arrondis développeront des facettes plates ou s'effondreront. Deuxièmement, les courbes TrueType sont des courbes de Bézier quadratiques alors que la fonction GDI PolyBezierTo requiert des courbes cubiques. Chaque segment quadratique fait donc l'objet d'une élévation de degré exacte plutôt que d'être découpé en segments de droite
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
L'élévation de degré est sans perte : la courbe cubique trace la même courbe, de sorte que le contour rendu correspond à ce qu'un lecteur conforme dessine à partir de la même table, à n'importe quel niveau de zoom. Le reste du travail concerne le positionnement. Chaque glyphe est défini dans des unités de police (généralement une grille de 1000 ou 2048 unités par em), et le moteur de rendu compose la matrice d'échelle, la matrice de texte et la matrice de transformation actuelle en une seule transformation glyphe-vers-périphérique avant que le tracé ne soit rempli. L'ordre importe plus qu'il n'y paraît : composez ces trois matrices dans l'ordre inverse et chaque glyphe s'effondrera vers l'origine — une page déformée dont le bug réel réside dans une seule ligne d'algèbre matricielle
Comment l'interpréteur de charstrings Type 2 gère-t-il les polices CFF ?
THPDFEmbeddedCFF fournit aux programmes FontFile3 un véritable interpréteur de charstrings Type 2 : il analyse les structures CFF INDEX, le Top DICT et le Private DICT, puis exécute chaque charstring et émet les segments de tracé directement vers GDI. Une enveloppe OpenType (le conteneur OTTO) est d'abord dépouillée pour atteindre la table CFF brute ; les flux bruts CIDFontType0C et Type1C sont consommés directement. Les charstrings constituent un langage de pile compact, et trois de ses conventions déterminent si l'interpréteur reste synchronisé avec le flux d'octets. Le préfixe de largeur facultatif signifie que le premier opérateur de vidage de pile peut comporter un opérande d'en-tête supplémentaire. L'opérateur hintmask implique un vstemhm lorsque des opérandes se trouvent encore sur la pile, et le nombre d'octets de masque à ignorer dépend du nombre de tiges (stems) accumulées — trompez-vous une seule fois dans ce calcul et chaque opcode suivant sera mal lu. De plus, les appels de sous-programmes ajoutent un biais à leur index (107, 1131 ou 32768 selon le nombre de sous-programmes) avant la recherche, de sorte qu'un appel non biaisé ciblerait un sous-programme erroné
Le CFF à clé CID (CID-keyed CFF) ajoute une indirection qui piège les implémentations naïves : le code de caractère sélectionne un CID, mais l'index de charstring est un GID, et le jeu de caractères (charset) de la police associe le GID au CID — le moteur de rendu construit donc la correspondance inverse CID-vers-GID avant le dessin, et sélectionne le Private DICT par glyphe via FDSelect pour les polices qui en comportent plusieurs. Les programmes Type1C à clés de nom, le conteneur habituel des polices Type 1 simples, résolvent quant à eux les codes sur un octet via l'encodage intégré du programme CFF ou via le mécanisme d'encodage au niveau du PDF décrit ensuite. Un avertissement honnête : l'interpréteur lit les opérateurs d'indices (hints) pour maintenir le flux synchronisé mais n'exécute pas le hinting, une limite abordée à la fin
Qu'est-ce qu'une police Type 3 et comment est-elle dessinée ?
Un glyphe Type 3 n'est pas un contour du tout — la norme ISO 32000-1 §9.6.5 le définit comme un flux de contenu. HotPDF effectue donc son rendu en empilant l'état graphique, en composant la matrice de la police, sa taille et la matrice de texte sur la CTM, puis en exécutant la procédure de glyphe via le même interpréteur d'opérateurs qui dessine les pages, avec le dictionnaire /Resources propre à la polices à portée. Deux détails de la spécification importent pour la correction. Les largeurs /Widths Type 3 sont exprimées dans l'espace des glyphes plutôt que dans l'espace de texte au 1/1000 utilisé par tous les autres types de polices. Les décalages doivent donc passer par la matrice /FontMatrix — sinon, les polices de codes-barres avec une matrice à 0.01 se décalent avec un ordre de grandeur erroné. De plus, une procédure de glyphe débutant par l'opérateur d1 émet deux promesses que le moteur de rendu applique : le dessin est limité à la boîte englobante déclarée et, selon la norme ISO 32000-1 §9.6.5.2, le glyphe ignore ses propres opérateurs de couleur et dessine avec la couleur de remplissage actuelle de l'appelant. Ainsi, rg, g, k et leurs équivalents de contour dans la procédure sont supprimés pendant la durée de ce glyphe. Omettez cette règle de couleur et une police de codes-barres d1 marquée en bleu par la page ressortira en noir ; omettez le détourage et un glyphe mal formé dessinera en dehors de sa cellule
Comment les codes de caractères deviennent des identifiants de glyphes (glyph IDs)
Les interpréteurs de contours ne représentent que la moitié du travail, car les octets d'une chaîne de texte PDF constituent des codes de caractères et non des index de glyphes, et la norme ISO 32000-1 y consacre deux sous-clauses. Pour les polices simples, le paragraphe §9.6.6 prescrit une priorité stricte : un tableau /Differences l'emporte sur l'encodage de base (WinAnsiEncoding, MacRomanEncoding ou StandardEncoding), qui l'emporte sur la table propre au programme de police. HotPDF résout cette chaîne en une table de correspondance code-vers-GID à 256 entrées, traduisant les noms de glyphes en index de glyphes par trois méthodes : correspondance exacte du jeu de caractères dans un programme CFF, noms numériques littéraux comme gNN/glyphNN pris comme index, et traduction de nom vers Unicode selon la table Adobe Glyph List suivie d'une recherche cmap pour les programmes TrueType. Pour les polices composites, le paragraphe §9.7 attribue le contrôle à CIDToGIDMap : le cas courant est /Identity, mais l'entrée peut être un flux de paires gros-boutiennes (big-endian) indexées par CID — et la sortie Unicode de HotPDF utilise précisément cette forme de flux pour ses sous-ensembles compacts, le chemin du flux n'est donc pas un cas exotique
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
Lorsqu'une recherche cmap TrueType est requise, HotPDF parcourt une chaîne de repli plutôt que de faire confiance à une seule sous-table : les sous-tables Unicode Windows (format 4, puis format 12 pour les plans supplémentaires) viennent en premier, puis la sous-table de symboles (3,0) avec sa convention de zone d'utilisation privée F000 projetée sur l'octet de poids faible — la raison pour laquelle une police de symboles comme Wingdings répond aux codes ASCII de base — puis les formats hérités 6 et 0. Les sous-tables de format 2 ne sont délibérément pas interprétées : elles correspondent à des pages de codes multi-octets héritées comme Shift-JIS et Big5, non Unicode, et les polices CJK modernes comportent de toute façon invariablement une sous-table de format 4 ou 12. Tout code qui survit à aucune de ces méthodes se rabat sur le dessin système GDI pour ce seul glyphe, de sorte qu'un caractère impossible à mapper dégrade un seul glyphe, et non tout le bloc de texte
Ce que le pipeline intégré ne fait pas
Il convient de préciser clairement les limites. Le hinting n'est pas exécuté — les contours sont remplis tels qu'ils ont été conçus, ce qui est indiscernable d'un résultat avec hinting à 150 DPI et plus, mais peut différer d'un pixel par rapport à un rastériseur avec hinting à de très petites tailles. Les programmes Type 1 d'origine dans FontFile (charstrings cryptées par eexec) ne sont pas interprétés, et les axes de polices variables OpenType ne sont pas appliqués. Dans ces situations, comme dans le cas d'un programme de police endommagé ou d'une table glyf sans table cmap exploitable, le système se rabat sur le dessin par police système plutôt que de bloquer l'affichage de la page. Cette approche axée sur la fidélité s'étend à d'autres aspects du moteur de rendu — les motifs de dégradé axiaux et radiaux reçoivent le même traitement de qualité — et la partie génération possède sa propre logique concernant l'ordre des sous-ensembles de polices dans EndDoc
L'utilisation de ce pipeline ne requiert aucun code spécifique aux polices — chaque mécanisme décrit ci-dessus s'active automatiquement lors de l'appel de rendu de la page
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
La conséquence pratique est celle qui importe le plus pour votre support : un PDF qui intègre ses polices s'affiche avec celles-ci, sur un serveur de build, dans un conteneur Windows ou sur un poste client qui n'a jamais disposé de cette police de caractères. Le pipeline de rendu de glyphes intégrés est inclus dans HotPDF Component pour Delphi et C++Builder — une bibliothèque VCL native couvrant la création, l'édition, l'extraction de texte et le rendu de pages PDF, sans aucune dépendance DLL externe