Article technique

Rendre les polices PDF non embarquées par polices système

Quand un PDF n'embarque pas une police, le composant HotPDF rend ce texte avec une police Windows installée choisie par HPDFMapBaseFontToSystem : il décode le nom /BaseFont, coupe la partie style, essaie plusieurs graphies jusqu'à ce que GDI confirme que la famille est installée, mesure les largeurs manquantes des standard 14 sur des polices métriquement compatibles, et convertit les codes un octet en Unicode avant de dessiner. Chacune de ces étapes existe parce que la version naïve échouait sur de vrais fichiers. Le renderer de pages RenderLoadedPageToBitmap se débrouille bien avec les programmes embarqués ; ceci est l'histoire des polices qui ne sont pas dans le fichier du tout

Pourquoi GDI dessine-t-il en silence le mauvais caractère pour une police non embarquée ?

GDI ne signale jamais une police manquante : passez à CreateFontIndirect un nom de face qu'il ne connaît pas et il sélectionne tranquillement un substitut, souvent une autre serif sans graisse bold. Le premier renderer passait le nom PDF presque verbatim, donc TimesNewRoman,Bold, TimesNewRomanPS-BoldMT et SegoeUI-Semibold n'appariaient rien et sortaient dans ce que GDI avait choisi. Les noms peuvent être pires encore. ISO 32000-1 §7.3.5 laisse un nom écrire n'importe quel octet en #xx, et les producteurs CJK épellent routinièrement les noms de police en UTF-8 échappé ou en octets de page de code héritée ; avant la v2.766.69, les échappements eux-mêmes devenaient le nom de face. HPDFMapBaseFontToSystem décode maintenant les échappements d'abord, renvoie une séquence d'octets UTF-8 valide comme caractères, et lit tout autre octet haut dans la page de code système

Couper le style, c'est là que les heuristiques mordent. Une virgule termine toujours la famille (Arial,Bold donne Arial), mais un trait d'union seulement quand le mot qui suit est un style : Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra ou Condensed. Cette règle garde MS-Mincho entier tout en transformant Calibri-Light en Calibri. HotPDF essaie ensuite des graphies de la famille, suivies des graphies de la famille privées d'un suffixe PSMT, MT ou PS. Depuis la v2.768.18, ces graphies couvrent chaque choix d'espaces aux endroits où un mot peut commencer : devant une capitale qui suit une minuscule (MyriadPro devient Myriad Pro), à la dernière capitale d'une suite suivie d'une minuscule (UIGothic), et après un MS initial (MSPGothic), depuis la forme entièrement espacée jusqu'au nom tel qu'écrit ; au-delà de quatre tels endroits, seules la forme entièrement espacée et le nom collé sont essayés. L'espacement ne peut pas être appliqué à l'aveugle, parce que Windows garde certains mots collés : SimSun est installé sous exactement cette graphie, tandis que MicrosoftYaHei, MicrosoftJhengHei et MSPGothic appartiennent à Microsoft YaHei, Microsoft JhengHei et MS PGothic. Avant la v2.768.18, le mapper mettait une espace devant chaque capitale interne, donc MicrosoftYaHei était cherché comme Microsoft Ya Hei et jamais trouvé. Un candidat compte comme installé quand CreateFontIndirect suivi de GetTextFace renvoie le nom demandé ou, depuis la v2.768.18, quand la table name de la police sélectionnée le liste comme famille, nom de famille complet ou typographique, dans n'importe quelle langue ; la réponse est mise en cache par nom, donc les documents à nombreuses polices non installées ne sondent plus Windows pour chaque nom à chaque page

Le pipeline HotPDF qui rend les polices PDF non embarquées avec des polices système en Delphi : HPDFMapBaseFontToSystem décode les octets échappés #xx du nom /BaseFont, coupe les suffixes de style Bold, Italic et Light tout en gardant MS-Mincho entier, bâtit des graphies candidates comme Myriad Pro et Microsoft YaHei, et n'en accepte une que quand GetTextFace ou la table de noms de la police confirme le nom installé
GDI ne signale jamais une police manquante, il substitue en silence — le mapping essaie ses candidats dans l'ordre et ne fait confiance qu'à un nom que GDI rend ou que la table de noms de la police sélectionnée liste

Comme la fonction de mapping est publique dans l'unité HPDFRenderFontMetrics, un rapport de préflight peut montrer avec quelle famille installée chaque police non embarquée va se rendre, en utilisant l'énumération de polices que THotPDF expose déjà pour les documents chargés :

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

Comment HotPDF mesure-t-il les polices standard 14 sans /Widths ?

HotPDF mesure les avances manquantes sur la police installée aux métriques identiques, parce qu'ISO 32000-1 §9.6.2.2 permet aux polices standard 14 d'omettre /Widths et que la bibliothèque n'embarque aucune table AFM. Arial porte les métriques de Helvetica, Times New Roman porte celles de Times et Courier New celles de Courier, donc HPDFMeasureBaseFontWidths crée la face correspondante à lfHeight = -1000 et appelle GetCharWidth32W ; à cette hauteur, le résultat est déjà dans les unités 1/1000 em qu'emploient les largeurs PDF. Le renderer convertit d'abord chaque code en Unicode via /Encoding, /BaseEncoding et /Differences, à défaut StandardEncoding. L'export SVG et l'extraction de texte butent sur un piège de plus : une police Type 1 standard sans /Encoding du tout produisait un décodeur sans information d'encodage, l'export SVG ne l'enregistrait jamais, et chaque largeur mesurée partait en pure perte. Fournir la StandardEncoding implicite a corrigé ça, à condition de la marquer comme encodage prédéfini ; la router par le chemin des noms de CMap décode chaque code en 0 et chaque largeur suit

Bold, italic et un décalage de un dans les drapeaux du descripteur de police

L'entrée /Flags d'un descripteur de police numérote ses bits à partir de 1, pas de 0, donc ForceBold est le bit 19 ($40000) et Italic le bit 7 ($40), selon ISO 32000-1 Table 123. L'ancien code testait $20000, qui est le bit 18, SmallCap. L'erreur a survécu de la v2.345.0 à la v2.766.53 parce que /FontDescriptor est presque toujours une référence indirecte et que le constructeur de polices ne lisait que les objets directs, donc toute la branche des drapeaux ne tournait jamais, et la même cécité ignorait /Widths 12 0 R et composait le texte avec une avance de repli de 500 unités. Dès que la v2.766.53 a commencé à résoudre les références indirectes à travers le renderer, le bit a dû être corrigé dans le même changement, sinon chaque face en petites capitales se serait mise à rendre en bold d'un coup :

const
  // ISO 32000-1 Table 123 compte les positions de bits depuis 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, pas un poids
  FD_FORCEBOLD  = $40000;  // bit 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Numérotation des bits de l'entrée /Flags du descripteur de police PDF : ISO 32000-1 Table 123 compte depuis le bit 1, ce qui fait Italic $40 au bit 7, SmallCap $20000 au bit 18 et ForceBold $40000 au bit 19, si bien que le test du descripteur $20000 de HotPDF visait SmallCap et est resté inoffensif tant que les références /FontDescriptor indirectes n'étaient jamais résolues
La branche des drapeaux est restée du code mort pendant quarante versions parce que le descripteur était indirect — dès que les références se sont résolues, le bit décalé de un est devenu du texte en petites capitales visible

Pourquoi les polices CJK à CMap UCS2 reçoivent-elles les mauvaises largeurs ?

Le texte CJK avec une CMap UCS2 prédéfinie dessine les bons glyphes mais le mauvais espacement quand un renderer traite le code comme le CID, parce que /W est indexé par CID, pas par code de caractère. Avec STSong-Light et UniGB-UCS2-H, le code arrive à égaler la valeur Unicode, donc GDI dessine les bons caractères et le bug se cache dans les avances : les minuscules arrivent en codes 97 et au-delà, tombent hors d'une entrée /W telle que [1 95 500], et reçoivent toutes la largeur par défaut /DW de 1000. Depuis la v2.766.56, le renderer HotPDF lit les codes à travers les plages codespace de la CMap (ISO 32000-1 §9.7.6.2) et les mappe vers des CIDs avant de chercher les largeurs. Seules les tables UCS2 et UTF16 intégrées et les flux CMap embarqués sont employés ; une approximation identité pour quelque chose comme GBK-EUC-H aurait seulement fait semblant d'être prise en charge tout en produisant une sortie fausse, donc le renderer ne fait pas semblant

Pourquoi du texte CJK avec une CMap UCS2 dessine les bons glyphes aux mauvaises avances : /W est indexé par CID tandis que les codes sont des valeurs Unicode, donc avec STSong-Light et UniGB-UCS2-H les codes minuscules 97 et au-delà ratent l'entrée /W [1 95 500] et prennent le défaut /DW, corrigé dans HotPDF en mappant les codes vers des CIDs via les plages codespace de la CMap
Le bug se cache parce que le code égale l'Unicode ici — les glyphes ont l'air bons tandis que chaque avance se met par défaut en silence, donc jugez le rendu CJK à l'espacement, pas aux formes

Pourquoi les caractères accentués deviennent-ils des points d'interrogation sur un Windows chinois ?

Les codes un octet ne doivent jamais atteindre les fonctions GDI ANSI (« A »), parce que GetGlyphOutlineA et GetGlyphIndicesA interprètent les octets dans la page de code système tandis que TextOutA emploie le jeu de caractères de la police sélectionnée. Sur un système chinois, l'octet $A9 d'Arial (le signe copyright en Windows-1252) devenait un octet conducteur GBK et se rendait en « ? », un piège dans lequel la voie de contours sans hinting ajoutée en v2.766.83 est tombée en plein. La v2.767.3 demande à la police réalisée son jeu de caractères avec GetTextCharset, le convertit en page de code via TranslateCharsetInfo, passe l'octet par MultiByteToWideChar et appelle les fonctions W ; les polices symboliques emploient U+F000 plus le code à la place. Les encodages en désaccord avec Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — sont mappés vers Unicode avant que toute police système ne les voie

Quelles sont les limites du dessin avec des polices système ?

Le rendu par polices système est une approximation, et le composant HotPDF est honnête sur là où il s'arrête. Avant la v2.768.18, le contrôle d'installation ne comparait que le nom que GetTextFace renvoie, et sur un Windows localisé cette fonction rapporte le nom de famille dans la langue du système, donc Microsoft YaHei sur un Windows chinois ou Yu Mincho sur un Windows japonais était jugé absent et dessiné dans un substitut GDI ; depuis la v2.768.18, une face qui revient sous un autre nom est aussi cherchée dans la table name de la police, et de telles polices se trouvent. La compatibilité métrique n'est garantie que pour les familles Helvetica, Times et Courier ; Symbol se mappe à Symbol et ZapfDingbats à Wingdings, ce qui est un expédient plutôt qu'un appariement. Le préflight ci-dessus ne voit aussi que les polices du dictionnaire /Resources de chaque page, pas celles référencées depuis l'intérieur de Form XObjects. Quand un code ne peut toujours pas être dessiné, le suivi des glyphes non résolus au moment du dessin le rapporte, ce qui est un meilleur signal que l'examen à l'œil de vignettes

La correction durable loge du côté de la composition. HotPDF lui-même écrit avec FontEmbedding réglé à True par défaut, en substituant un Arial embarqué même quand le code appelle SetFont avec Helvetica, et le texte embarqué passe par le renderer de glyphes de polices embarquées au lieu de n'importe laquelle des devinettes ci-dessus. Une garde pas chère pour les fichiers entrants est d'avertir avant le rendu quand une famille mappée n'est pas dans la liste des polices écran :

// VCL : Screen.Fonts liste les noms de familles installées (unité Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Pour le composant complet, y compris rendu de pages, extraction de texte et font subsetting côté écriture, voir la page produit du HotPDF Delphi PDF component