Article technique

Sous-ensemblage de polices TrueType via fontsub.dll en Delphi

HotXLS, le composant Excel pour Delphi et C++Builder, réduit la taille des polices PDF intégrées grâce au sous-ensemblage de polices TrueType : au moment de l'export PDF, il appelle la fonction CreateFontPackage de la bibliothèque système Windows fontsub.dll pour reconstruire une police TrueType intégrée autour des seuls points de code Unicode réellement utilisés par une feuille de calcul, plutôt que d'expédier le fichier de police complet. Un rapport avec deux cents lignes de noms de produits chinois peut n'avoir besoin que de quelques centaines de caractères han distincts, alors que les polices CJK fournies par Windows pèsent couramment 5 à 20 Mo chacune. Intégrez-en une entière, et la police à elle seule peut peser plus lourd que tous les autres objets du PDF réunis

fontsub.dll n'est pas une bibliothèque dont la plupart des développeurs Delphi ont jamais entendu parler, et cela s'explique : Microsoft la distribue comme une petite DLL utilitaire peu documentée plutôt que comme une API Win32 phare. HotXLS la traite comme une capacité optionnelle, pas comme une dépendance stricte, si bien que la façon dont l'exportateur la charge, l'appelle, et se replie lorsqu'elle est absente en dit autant sur la programmation Windows défensive que sur les formats de police, et les deux moitiés de cette histoire méritent d'être parcourues

Pourquoi le texte Unicode fait-il gonfler un export PDF HotXLS ?

L'exportateur PDF de HotXLS ne recourt à une police TrueType intégrée que lorsque le texte de la feuille de calcul sort de WinAnsi, et reste sur la famille Helvetica intégrée le reste du temps, le chemin par défaut que couvre en détail le guide d'export feuille de calcul vers PDF. WinAnsi couvre suffisamment bien le texte d'Europe occidentale pour que de nombreux classeurs ne déclenchent jamais d'intégration de police : le PDF se contente de référencer Helvetica par son nom et le lecteur la fournit localement, si bien que le fichier reste petit. Dès qu'une cellule contient quelque chose que WinAnsi ne peut pas représenter, un nom de produit chinois, une note en coréen, un symbole égaré dans un commentaire, l'exportateur doit intégrer un véritable programme de police, car un lecteur PDF n'a aucune source de glyphe de repli pour les caractères en dehors des 14 polices standard

HotXLS localise cette police automatiquement, en scannant le dossier des polices Windows à la recherche d'une courte liste de candidats installés, y compris les polices compatibles CJK que Windows fournit pour le rendu chinois et coréen, à moins que la propriété UnicodeFontFile de l'exportateur ne pointe déjà vers un fichier spécifique, et quelle que soit la police sur laquelle il tombe, elle est intégrée entièrement avant même que le sous-ensemblage ne s'exécute. Cette exigence d'intégration est propre au PDF : les chemins d'export RTF et HTML de HotXLS conservent le texte Unicode intact en échappant les points de code dans le flux d'octets plutôt qu'en expédiant un programme de police, ce qui explique pourquoi le problème de taille couvert dans cet article n'a pas d'équivalent sur ces deux formats

uses
  lxHandle, lxPDF;

var
  Book: TXLSWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('catalog-cn.xlsx');
    Exporter := TXLSPDFExport.Create;
    try
      // Optional: pin a specific CJK-capable font instead of the
      // exporter's automatic Windows\Fonts scan.
      Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
      Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Qu'est-ce que fontsub.dll, et pourquoi ne pas écrire un sous-ensembleur à partir de zéro ?

fontsub.dll est une petite bibliothèque système Windows, fournie depuis Windows XP, qui expose une seule fonction pertinente ici : CreateFontPackage. Donnez-lui les octets d'une police TrueType source et une liste de points de code Unicode à conserver, et elle vous renvoie une police minimale qui satisfait pourtant toutes les contraintes du format de police : indices de glyphes renumérotés, glyf et loca reconstruits autour des seuls contours conservés, hmtx et cmap réécrits en conséquence. HotXLS déclare le type de pointeur de fonction directement selon ce contrat

const
  TTFCFP_FLAGS_SUBSET = 1;
  TTFMFP_SUBSET = 0;
  TTFCFP_MS_PLATFORMID = 3;
  TTFCFP_UNICODE_CHAR_SET = 1;

type
  TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
    var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
    var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
    usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
    pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
    lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;

Écrire à la main le travail de CreateFontPackage plutôt que de l'appeler signifierait implémenter un véritable sous-ensembleur TrueType correct : parcourir les glyphes composites pour intégrer chaque glyphe composant référencé par un glyphe conservé, reconstruire les décalages loca après suppression de contours, respecter les bits de permission d'intégration dans la table OS/2 d'une police, et faire tout cela correctement sur toutes les polices exotiques qu'une machine de client peut avoir installées. Microsoft a déjà résolu ce problème et fournit la solution dans le cadre de Windows lui-même, si bien qu'appeler une DLL système que Microsoft maintient, teste contre sa propre pile de rendu de polices, et distribue gratuitement sur chaque machine coûte à HotXLS un chargement dynamique et un pointeur de fonction ; réimplémenter la même logique signifierait posséder un analyseur pour un format binaire truffé de décennies de cas particuliers, pour une fonctionnalité qui n'importe que lorsqu'une police se trouve être volumineuse

Construire la liste de conservation à partir des glyphes réellement rendus

HotXLS construit la liste de conservation du sous-ensemblage à partir d'une carte qu'il maintenait déjà pour une autre raison, si bien que la comptabilité ne coûte rien de plus. Chaque fois que le code de rendu de page dessine un caractère nécessitant la police Unicode intégrée, il recherche l'indice de glyphe de ce caractère et enregistre la paire dans FUnicodeGlyphMap, une table glyphe-vers-point-de-code qui pilote aussi la CMap PDF ToUnicode afin qu'un copier-coller depuis le document final retourne le texte d'origine plutôt que des identifiants de glyphes bruts. Au moment où les flux de contenu de page sont terminés, cette carte liste déjà exactement l'ensemble des points de code Unicode que le document a utilisés, ni plus ni moins

var
  keepList: array of Word;
  keepCount, i: Integer;
  codePoint: LongWord;
begin
  SetLength(keepList, FUnicodeGlyphMap.Count);
  keepCount := 0;
  for i := 0 to FUnicodeGlyphMap.Count - 1 do
  begin
    codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
    if codePoint > 0 then
    begin
      keepList[keepCount] := Word(codePoint);
      Inc(keepCount);
    end;
  end;
end;

Au moment de la finalisation, HotXLS parcourt cette même carte une seconde fois pour construire la liste de conservation attendue par CreateFontPackage, un simple tableau des points de code Unicode à retenir sous la forme 16 bits qu'exige l'argument de liste de conservation de l'API. Comme cet argument est un tableau de mots de 16 bits, il adresse proprement le plan multilingue de base, ce qui couvre le chinois, le japonais et le coréen ordinaires, le cyrillique, le grec et l'arabe sans complication ; une feuille de calcul qui s'appuie sur des caractères de plans supplémentaires, certains emoji ou des écritures historiques rares, se situe hors de ce qu'une seule entrée de liste de conservation peut nommer directement, ce qui est une limite qui mérite d'être connue plutôt qu'un défaut, puisque la grande majorité des feuilles de calcul professionnelles fortement unicodiennes ne s'approchent jamais de ce plan en premier lieu

Que se passe-t-il lorsque fontsub.dll est absent ?

HotXLS ne suppose jamais que fontsub.dll est présent, et l'export PDF n'échoue jamais parce qu'il ne l'est pas. La bibliothèque est chargée dynamiquement au moment où un sous-ensemble est nécessaire, avec SafeLoadLibrary et GetProcAddress plutôt qu'un import statique, précisément parce que fontsub.dll n'est pas une API publique documentée et garantie présente comme l'est kernel32.dll : c'est un outillage d'intégration de police fourni en bundle, et rien dans le contrat de Microsoft ne promet qu'elle survit sur chaque édition, chaque branche de maintenance, ou chaque couche de compatibilité qui tente d'émuler Windows

var
  hFontSub: HMODULE;
  CreateFontPackage: TCreateFontPackage;
begin
  hFontSub := SafeLoadLibrary('FontSub.dll');
  if hFontSub = 0 then
    Exit; // no subsetting available - keep the full embedded font
  try
    @CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
    if not Assigned(CreateFontPackage) then
      Exit;
    // ... call CreateFontPackage, check its return code ...
  finally
    FreeLibrary(hFontSub);
  end;
end;

Chaque chemin d'échec se replie sur le même résultat. Une DLL absente, un export absent, un code de retour non nul, ou une police dont la table OS/2 interdit le sous-ensemblage via ses bits de permission d'intégration, HotXLS se contente de conserver la police complète déjà intégrée et poursuit. Rien ne lève d'exception, rien n'interrompt l'export, et le code appelant n'a jamais à envelopper une optimisation de police dans sa propre gestion d'exception ; le PDF exporté est valide dans les deux cas, et la seule variable est de savoir s'il finit petit ou quelque peu plus volumineux

De combien le PDF devient-il réellement plus petit ?

Le sous-ensemblage de polices TrueType de HotXLS réduit typiquement le PDF exporté d'une feuille de calcul fortement unicodienne à quelque part entre un vingtième et un huitième de sa taille non sous-ensemblée, une réduction de 8 à 20 fois dont l'ampleur suit la part d'une police complète qu'un document donné touche réellement : un bon de commande construit autour de quelques centaines de caractères chinois distincts ne conserve que ces quelques centaines de glyphes parmi les dizaines de milliers qu'expédie une police CJK, tandis qu'une feuille couvrant un mélange plus large de caractères en conserve proportionnellement plus. HotXLS superpose une passe de compression Flate supplémentaire par-dessus les octets de police sous-ensemblée avant de les écrire dans le flux /FontFile2 du PDF, la même compression que subissent déjà les autres flux de contenu du document, et rien de tout cela ne demande quoi que ce soit de plus au code appelant : une feuille de calcul qui ne sort jamais de WinAnsi ne touche jamais ce chemin et continue d'exporter via Helvetica ordinaire, tandis qu'une feuille de calcul qui déclenche bien le chemin de police Unicode obtient le sous-ensemblage automatiquement, sans propriété à définir ni appel séparé à effectuer, et la seule propriété concernée, UnicodeFontFile, ne fait que choisir quelle police est intégrée et sous-ensemblée, pas si le sous-ensemblage se produit

Le sous-ensemblage de polices est un détail au sein de la surface d'export PDF plus large du composant Excel Delphi HotXLS, aux côtés de la pagination, des métadonnées d'impression de feuille de calcul, et des chemins d'export CSV, HTML et RTF fournis avec le composant