HotPDF rend correctement les lettres accentuées des polices embarquées en gérant deux mécanismes de police qui construisent un glyphe à partir d'autres : les glyphes composites TrueType, dont les décalages de composantes doivent être appliqués en unités de glyphe avant la matrice glyphe-vers-périphérique, et la forme seac de endchar en CFF, qui assemble une lettre de base et un accent par code StandardEncoding. Se tromper sur l'une ou l'autre, et é, Å, ü ou un guillemet anglais courbé sort auquel il manque un morceau, pendant que les lettres simples autour ont l'air parfaites
Ce dernier détail est ce qui rend ces bugs coûteux. Un rapport se rend, le corps de texte est bon, et seul "München" a un trou, ou une citation s'ouvre avec une apostrophe et un vide. Personne ne suspecte le moteur de rendu. La première théorie est toujours une police endommagée ou un mauvais producteur, et le ticket rebondit pendant une semaine. Cet article parcourt ce qui se passe réellement dans chaque mécanisme, comment le composant HotPDF Delphi le gère depuis les v2.768.10 et v2.768.11, et le correctif de subsetting lié en v2.770.83 qui garde les glyphes composites intacts quand HotPDF écrit des polices au lieu d'en lire
Pourquoi seul l'accent disparaît-il d'un glyphe PDF ?
L'accent disparaît parce que c'est la seule partie du glyphe qui porte un décalage non nul. Les polices TrueType comme CFF économisent de l'espace en réutilisant des contours : é n'est pas dessiné comme une nouvelle forme mais comme "le glyphe e, plus le glyphe accent aigu décalé vers le haut et la droite". La lettre de base se trouve presque toujours au décalage (0, 0), donc toute erreur d'application des décalages la laisse intacte. L'accent est la composante qui bouge, donc c'est elle qui atterrit au mauvais endroit ou n'est jamais dessinée
Le motif du symptôme est un diagnostic à lui seul. Si les lettres de base des caractères accentués se rendent et pas les diacritiques, ou si une ponctuation appariée comme “ et ” se montre comme une seule marque au lieu de deux, arrêtez de regarder le fichier de police. Une police cassée casse en général des glyphes entiers, et les casse pour toute visionneuse. Des morceaux manquants de glyphes composés dans un seul moteur, pendant qu'Acrobat affiche la page bien, pointent vers le code qui assemble les composantes
Comment les glyphes composites TrueType placent-ils leurs composantes ?
Un glyphe composite TrueType place chaque composante avec sa propre petite transformée affine, exprimée en unités de police, et cette transformée doit agir sur le contour de la composante avant la matrice glyphe-vers-périphérique. Dans la table glyf OpenType, un glyphe composite a un compte de contours négatif, suivi d'une liste d'enregistrements de composantes. Chaque enregistrement porte un mot de drapeaux, l'indice de glyphe de la composante, deux arguments qui sont soit des octets soit des mots (ARG_1_AND_2_ARE_WORDS, 0x0001), et une échelle optionnelle : une valeur F2Dot14 pour WE_HAVE_A_SCALE (0x0008), deux pour WE_HAVE_AN_X_AND_Y_SCALE (0x0040), ou une matrice 2x2 complète pour WE_HAVE_A_TWO_BY_TWO (0x0080). Quand ARGS_ARE_XY_VALUES (0x0002) est posé, les deux arguments sont un décalage dx/dy en unités de police. MORE_COMPONENTS (0x0020) dit qu'un autre enregistrement suit
Donc pour chaque composante on construit une matrice de composante CM depuis la partie échelle et le décalage, puis on la combine avec M, la matrice qui mappe les unités de police vers les pixels périphériques. L'ordre de cette combinaison est le bug entier. La multiplication de matrices interne de HotPDF suit la convention vecteur-colonne, donc le produit de A et B applique B d'abord puis A. La composition correcte applique CM d'abord, en espace glyphe, et M ensuite. Écrite dans l'autre sens, M tourne d'abord et le décalage de CM s'ajoute après, déjà en pixels périphériques et jamais mis à l'échelle
// Illustration seulement : un enregistrement affine 2D avec la convention vecteur-colonne,
// où Mul(A, B) mappe un point à travers B d'abord puis à travers A
type
TAffine = record
a, b, c, d, e, f: Double;
end;
function Mul(const A, B: TAffine): TAffine;
begin
Result.a := A.a * B.a + A.c * B.b;
Result.b := A.b * B.a + A.d * B.b;
Result.c := A.a * B.c + A.c * B.d;
Result.d := A.b * B.c + A.d * B.d;
Result.e := A.a * B.e + A.c * B.f + A.e;
Result.f := A.b * B.e + A.d * B.f + A.f;
end;
// M : unités de police -> pixels périphériques (échelle minuscule, y inversé)
// CM : transformée de composante ; e/f portent dx/dy en unités de police
Sub := Mul(M, CM); // correct : décalage mis à l'échelle avec le contour
// Sub := Mul(CM, M); // faux : dx/dy atterrissent en pixels périphériques bruts
Les nombres rendent la panne évidente une fois qu'on les voit. Arial utilise un em de 2048 unités, et la composante accent de é porte son décalage dans ces unités de police. Avec le bon ordre, ce décalage rétrécit avec le contour jusqu'à quelques pixels au zoom normal. Avec l'ordre inversé, la valeur brute en unités de police s'ajoute comme pixels périphériques, et dans le rendu de test HotPDF, l'accent du é d'Arial s'est retrouvé à 243 pixels de sa lettre, bien hors du glyphe et en général hors de tout ce qui est visible. Le guillemet double gauche d'Arial est deux copies de la forme d'apostrophe, la seconde décalée, donc il se rendait comme un ‘ isolé suivi de vide
La partie linéaire expose rarement le bug. Les composantes accent ne portent en général aucune échelle, et une échelle uniforme commute avec M de toute façon, donc seule la translation révèle le produit inversé. C'est aussi pourquoi un test rapide avec du texte latin simple passe : l'inversion avait siégé dans le code pendant plusieurs versions avant que la v2.768.10 ne la corrige, et seules les composantes à décalage la montraient
Récursion et cycles
Les glyphes composites peuvent référencer d'autres glyphes composites, donc un moteur les parcourt récursivement et doit se défendre. HotPDF plafonne la profondeur d'imbrication à 5 niveaux et le nombre total de composantes par glyphe à 1024, saute une composante qui nomme son propre indice de glyphe, et traite le mode d'appariement de points rarement utilisé (des arguments qui sont des numéros de points plutôt que des décalages) comme un décalage nul. Une police hostile ou corrompue peut donc perdre une composante, mais elle ne peut pas bloquer le rendu de page
Qu'est-ce que la forme seac de endchar CFF ?
La forme seac de endchar est une façon dépréciée pour une charstring CFF Type 2 de dire "ce glyphe est une lettre de base plus un accent" : quand endchar a quatre arguments, ce sont adx ady bchar achar. L'Adobe Technical Note #5177 la décrit dans son annexe sur la compatibilité avec le Type 1. Le nom vient de l'opérateur seac du Type 1 (standard encoding accented character), et les sous-ensembles CFF convertis depuis des polices Type 1 l'utilisent encore beaucoup pour les lettres latines accentuées
Trois détails font trébucher les implémenteurs. Un, bchar et achar ne sont pas des ID de glyphes. Ce sont des codes de l'Adobe StandardEncoding, donc le moteur mappe chaque code vers un nom de glyphe puis cherche ce nom à travers le charset de la police pour trouver le GID. Deux, le Type 2 n'a pas d'argument asb, contrairement au seac Type 1 qui prend asb adx ady bchar achar. En Type 2, l'origine de l'accent va directement à (adx, ady) relatif à l'origine du glyphe de base, sans correction de chasse latérale. Trois, les polices CFF à clé CID n'ont pas le droit d'utiliser seac du tout, parce qu'il n'y a pas de charset de noms de glyphes par lequel résoudre les codes StandardEncoding
Il y a ensuite la largeur. Chaque charstring Type 2 peut commencer son premier opérateur vidant la pile avec un argument de chasse optionnel, et endchar est l'un de ces opérateurs. Un endchar simple à un argument porte une largeur ; un endchar seac à cinq arguments porte une largeur suivie des quatre valeurs seac. La règle est de compter les arguments contre le compte fixe attendu, qui est quatre pour seac. Avant la v2.768.11, HotPDF traitait tout endchar comme "prends la largeur et arrête", si bien que les glyphes seac produisaient un contour vide : pas de base, pas d'accent, juste un écart de la bonne taille dans la ligne
// Illustration de la décision endchar dans un interpréteur Type 2.
// La pile porte les opérandes qui précèdent endchar ; Count est leur nombre.
if (Count >= 4) and not IsCIDFont then
begin
// 5 opérandes : le premier est la chasse, les autres sont seac
if Count = 5 then
TakeWidthAndShift; // jetez la chasse, gardez 4 opérandes
if StackDepth = 4 then
begin
BaseGID := GIDForName(StdEncName(Round(Stack[2]))); // bchar
AccentGID := GIDForName(StdEncName(Round(Stack[3]))); // achar
DrawGlyph(BaseGID, Identity);
DrawGlyph(AccentGID, Translate(Stack[0], Stack[1])); // adx, ady
Exit;
end;
end;
// sinon : endchar ordinaire, chasse optionnelle, fermez le chemin
Dans le vrai interpréteur, les deux glyphes composantes sont exécutés avec le même moteur de charstring, la matrice de l'accent est la matrice de glyphe courante composée avec une translation vers (adx, ady), et la matrice est restaurée ensuite pour que les glyphes suivants de la ligne ne soient pas touchés. Si l'un des noms ne se résout pas en GID, cette composante est sautée plutôt que de faire échouer le glyphe entier, ce qui est le comportement indulgent qu'on veut quand un sous-ensemble a été taillé un peu trop agressivement
Vérifier le correctif depuis votre propre code Delphi
La façon la plus rapide de vérifier la gestion des glyphes composites et de seac est de rendre une page avec du texte accentué provenant de polices embarquées à travers les deux chemins de sortie publics : THotPDF.RenderLoadedPageToBitmap pour la sortie raster et THotPDF.BuildLoadedPageSVG avec EmbedFonts à True pour la sortie vectorielle. Tous deux partagent le même code de contour de glyphe, donc tous deux ont reçu les correctifs v2.768.10 et v2.768.11. Le pipeline dans son ensemble est couvert dans comment HotPDF rend les glyphes TrueType, CFF et Type 3 embarqués, et les options bitmap dans le rendu de pages PDF en bitmaps en Delphi
uses
System.Classes, Vcl.Graphics, HPDFDoc;
procedure RenderAccentProbe(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
Svg: AnsiString;
Lines: TStringList;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
// Chemin raster : 144 DPI rend les accents mal placés faciles à repérer
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('accent-probe.bmp');
finally
Bmp.Free;
end;
// Chemin vectoriel : EmbedFonts = True écrit les contours des glyphes, donc
// l'assemblage composite et seac est visible dans les chemins SVG eux-mêmes
Svg := Pdf.BuildLoadedPageSVG(0, True);
if Svg <> '' then
begin
Lines := TStringList.Create;
try
Lines.Text := string(Svg);
Lines.SaveToFile('accent-probe.svg');
finally
Lines.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Choisissez les documents de test délibérément. Il vous faut un PDF avec un sous-ensemble TrueType embarqué contenant é, Å et des guillemets anglais, et un avec un sous-ensemble CFF embarqué (FontFile3, Type1C) produit depuis une police Type 1, qui est là où seac vit encore, typiquement dans de vieilles sorties de composition et certains flux print. Le texte composé dans des polices que le producteur n'a pas embarquées ne vous apprend rien ici, parce que ces glyphes viennent des polices Windows installées plutôt que du programme de police embarqué décrit dans ISO 32000-1 §9.9
Pourquoi les glyphes composites cassaient-ils pendant le subsetting ?
Les glyphes composites cassaient pendant le subsetting parce que le sous-ensembleur avançait dans les enregistrements de composantes avec un mauvais pas dès qu'une composante portait une échelle ou une transformée 2x2, puis lisait mal l'indice de glyphe de la composante suivante. HotPDF a corrigé cela dans la v2.770.83. Le subsetting est le jumeau côté écriture du bug de rendu : quand HotPDF embarque un sous-ensemble d'une police TrueType installée, il doit garder chaque glyphe qu'un composite référence et renuméroter ces références vers les nouveaux ID de glyphes compacts
Le vieux marcheur ne regardait que le drapeau de taille des arguments et le drapeau encore-des-composantes, donc il avançait de 6 ou 8 octets par enregistrement. Il ignorait les 2, 4 ou 8 octets de données de transformée que les drapeaux d'échelle ajoutent. Après une composante à échelle, le prochain "indice de glyphe" qu'il lisait était en réalité des octets de transformée, donc la vraie composante pouvait être laissée dehors du sous-ensemble, et l'étape de renumérotation pouvait écrire un nouvel ID de glyphe directement dans la transformée. Le résultat était une lettre accentuée qui se rendait dans certaines visionneuses avec le mauvais accent, un accent déformé, ou pas du tout, et seulement pour les polices qui mettent leurs composantes à l'échelle, voilà pourquoi la plupart des tests ne l'avaient jamais attrapé
Deux problèmes de plus siégeaient à côté du bug de pas. La renumérotation d'un indice de glyphe n'est pas idempotente, et les composites imbriqués étaient renumérotés deux fois : une fois par la passe sur chaque composite marqué, encore une par une descente récursive en lui. HotPDF renumérote désormais chaque composite exactement une fois et pilote l'étape "quels glyphes ce composite demande-t-il" depuis une file de travail, pour qu'un cycle de composites qui se référencent termine, que les indices de glyphes au-delà du compte de glyphes maxp de la police soient rejetés, et que les glyphes vides ne soient jamais parsés comme composites
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'accents-subset.pdf';
Pdf.FontEmbedding := True; // embarquez les glyphes utilisés
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 14);
// é, Å, ü et les guillemets anglais sont tous des glyphes composites dans Arial
Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
'Café, Ångström, München, “quoted”');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Faites faire un aller-retour à ce fichier à travers la sonde ci-dessus et à travers une seconde visionneuse. Depuis la v2.770.83, chaque composante de chaque glyphe composite survit au subsetting, composantes derrière une échelle ou une transformée 2x2 comprises, donc les accents se rendent pareil dans HotPDF, Acrobat et les visionneuses navigateur
Ce que la gestion des glyphes composites ne couvre pas
Le correctif des glyphes composites couvre le placement par décalage, ce qu'utilisent presque toutes les vraies polices, et approxime le reste avec prudence. Les composites TrueType par appariement de points, où les deux arguments nomment des points à aligner au lieu de donner un décalage, sont dessinés avec un décalage nul. Le seac dans les polices CFF à clé CID est ignoré par conception parce que la spécification y interdit. Un seac dont le code de base ou d'accent n'a pas de glyphe dans un sous-ensemble taillé dessine la composante qu'il trouve. Aucun de ces cas ne fait planter le moteur, mais si vous produisez des PDF pour l'archivage ou le PDF/A, le geste plus sûr est d'embarquer des polices qui utilisent des composites par décalage et des charstrings Type 2 sans seac, pour que chaque consommateur, pas un seul moteur, voie les mêmes lettres
FAQ
Pourquoi les lettres accentuées se rendent-elles sans leurs accents dans mon moteur PDF ?
Les lettres accentuées sont en général des glyphes composites ou des glyphes seac CFF, et l'accent est la seule composante à décalage. Un moteur qui applique le décalage de composante après la matrice périphérique, ou qui ignore seac dans endchar, perd ou déplace l'accent pendant que la lettre de base se rend normalement
HotPDF prend-il en charge la forme seac CFF de endchar ?
Oui. Depuis la v2.768.11, HotPDF dessine un endchar à quatre arguments, ou cinq avec la chasse, comme un glyphe de base plus un accent décalé à (adx, ady), en résolvant les deux via les noms StandardEncoding et le charset de la police, pour le rendu bitmap comme pour l'export SVG avec polices embarquées
Les glyphes composites sont-ils préservés quand HotPDF fait du subsetting de polices TrueType ?
Oui. Depuis la v2.770.83, HotPDF avance correctement sur les transformées de composantes de 2, 4 ou 8 octets, garde chaque composante référencée dans le sous-ensemble, renumérote les composites imbriqués une seule fois, et s'arrête sans danger sur des références de glyphes cycliques ou hors plage
Le rendu des glyphes composites, la prise en charge seac CFF et le subsetting TrueType sûr sont livrés au sein du HotPDF Delphi PDF Component, une bibliothèque VCL native pour Delphi et C++Builder qui crée, édite, extrait et rend des documents PDF sans DLL externe