L'optimisation des fichiers de polices TrueType (TTF) est essentielle lors du développement d'applications web ou mobiles, en particulier pour les langues à grands jeux de caractères comme le chinois. Un fichier TTF standard contenant des milliers de glyphes peut atteindre plusieurs mégaoctets, ce qui impacte les performances. La solution consiste à n'inclure que les caractères effectivement utilisés, créant ainsi un sous-ensemble (subset) de la police originale.
L'outil open-source sfntly, développé par Google, permet d'éditer et de créer des fichiers de polices au format OpenType et TrueType. Son composant sfnttool est particulièrement utile pour cette tâche de découpage.
Préparation de l'environnement
- Installer un JDK version 1.8 ou supérieure.
- Installer l'outil de construction
ant. - Cloner le dépôt source de sfntly :
https://github.com/googlei18n/sfntly. - Construire le projet avec
antpour obtenir l'archivesfnttool.jar.
Procédure de découpage et points d'attention
L'outil s'utilise en ligne de commande pour extraire un sous-ensemble basé sur une chaîne de caractères fournie :
java -jar sfnttool.jar -s 'texte à extraire' police_source.ttf police_sortie.ttf
Exemple pour extraire les caractères «01中国人» :
java -jar sfnttool.jar -s '01中国人' source.ttf resultat.ttf
Point crucial concernant l'encodage : sfnttool convertit la chaîne d'entrée en Unicode en se basant sur la page de codes du système d'exploitation courant (par exemple, 936 pour GBK en Windows chinois simplifié, 950 pour BIG5 en chinois traditionnel). Une inadéquation entre l'encodage de la chaîne d'antrée et la page de codes système entraînera une extraction incorrecte.
Contournement pour le chinois traditionnel : Si votre système utilise une page de codes GBK (comme Windows chinois simplifié), entrer une chaîne encodée en BIG5 produira un résultat erroné. Les solutions sont :
- Exécuter l'outil sur un système dont la page de codes est 950 (BIG5).
- Modifier le code source de
sfnttoolpour accepter un paramètre d'encodage d'entrée et effectuer une conversion explicite vers Unicode.
Notes supplémentaires sur les formats de polices :
- Les fichiers
.ttc(TrueType Collection) peuvent être convertis en fichiers.ttfindividuels à l'aide d'outils comme FontCreator. - La taille du fichier de sortie peut varier considérablement en fonction de la police source originale, même pour un même sous-ensemble de caractères. Cette différence peut être due à des métadonnées ou à des techniques d'optimisation intégrées dans la police de base.
Amélioration : Lecture de la liste de caractères depuis un fichier
L'implémentation par défaut de sfnttool accepte la chaîne de caractères en argument de ligne de commande, ce qui est peu pratique pour un grand nombre de caractères. Le code suivant modifie la classe principale pour qu'elle lise la liste des caractères à extraire depuis un fichier texte.
/*
* Copyright 2011 Google Inc. All Rights Reserved.
* ... [Licence identique] ...
*/
package com.google.typography.font.tools.sfnttool;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.core.CMapTable;
import com.google.typography.font.tools.conversion.eot.EOTWriter;
import com.google.typography.font.tools.conversion.woff.WoffWriter;
import com.google.typography.font.tools.subsetter.HintStripper;
import com.google.typography.font.tools.subsetter.RenumberingSubsetter;
import com.google.typography.font.tools.subsetter.Subsetter;
import java.io.File;
import java.io.FileInputStream;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class FontSubsetter {
private boolean shouldStripHints = false;
private String characterSetSource = null;
private boolean outputWoff = false;
private boolean outputEot = false;
private boolean enableMtx = false;
public static void main(String[] args) throws IOException {
FontSubsetter processor = new FontSubsetter();
File inputFont = null;
File outputFont = null;
int iterationCount = 1;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("-")) {
String opt = arg.substring(1);
switch (opt) {
case "?":
case "help":
showUsage();
return;
case "b":
case "bench":
iterationCount = 10000;
break;
case "h":
case "hints":
processor.shouldStripHints = true;
break;
case "s":
case "string":
if (i + 1 < args.length) {
processor.characterSetSource = loadCharsFromFile(args[++i]);
}
break;
case "w":
case "woff":
processor.outputWoff = true;
break;
case "e":
case "eot":
processor.outputEot = true;
break;
case "x":
case "mtx":
processor.enableMtx = true;
break;
default:
showUsage();
return;
}
} else {
if (inputFont == null) {
inputFont = new File(arg);
} else if (outputFont == null) {
outputFont = new File(arg);
}
}
}
if (processor.outputWoff && processor.outputEot) {
System.err.println("Les options WOFF et EOT sont mutuellement exclusives.");
return;
}
if (inputFont != null && outputFont != null) {
processor.generateSubset(inputFont, outputFont, iterationCount);
} else {
showUsage();
}
}
private static String loadCharsFromFile(String filePath) throws IOException {
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String currentLine;
while ((currentLine = reader.readLine()) != null) {
contentBuilder.append(currentLine);
}
}
return contentBuilder.toString();
}
private static void showUsage() {
System.out.println("Utilisation : FontSubsetter [options] fichier_entree.ttf fichier_sortie.ttf");
System.out.println("Options :");
System.out.println(" -?, -help Afficher cette aide");
System.out.println(" -s, -string FILE Lire les caractères à conserver depuis FILE");
System.out.println(" -b, -bench Benchmark (10000 itérations)");
System.out.println(" -h, -hints Supprimer les hints de la police");
System.out.println(" -w, -woff Sortie au format WOFF");
System.out.println(" -e, -eot Sortie au format EOT");
System.out.println(" -x, -mtx Activer la compression Microtype Express pour EOT");
}
public void generateSubset(File source, File destination, int repeats) throws IOException {
FontFactory factory = FontFactory.getInstance();
byte[] originalFontData;
try (FileInputStream fis = new FileInputStream(source)) {
originalFontData = new byte[(int) source.length()];
fis.read(originalFontData);
}
Font[] loadedFonts = factory.loadFonts(originalFontData);
Font baseFont = loadedFonts[0];
List<cmaptable.cmapid> cmapIds = List.of(CMapTable.CMapId.WINDOWS_BMP);
for (int n = 0; n < repeats; n++) {
Font processedFont = baseFont;
if (characterSetSource != null && !characterSetSource.isEmpty()) {
Subsetter subsetter = new RenumberingSubsetter(processedFont, factory);
subsetter.setCMaps(cmapIds, 1);
List<integer> glyphIds = GlyphCoverage.getGlyphCoverage(processedFont, characterSetSource);
subsetter.setGlyphs(glyphIds);
Set<integer> tablesToRemove = new HashSet<>();
// Tables non renumérotées, à supprimer
tablesToRemove.add(Tag.GDEF);
tablesToRemove.add(Tag.GPOS);
tablesToRemove.add(Tag.GSUB);
tablesToRemove.add(Tag.kern);
tablesToRemove.add(Tag.hdmx);
tablesToRemove.add(Tag.vmtx);
tablesToRemove.add(Tag.VDMX);
tablesToRemove.add(Tag.LTSH);
tablesToRemove.add(Tag.DSIG);
tablesToRemove.add(Tag.vhea);
// Tables AAT (Apple Advanced Typography)
tablesToRemove.add(Tag.intValue(new byte[]{'m', 'o', 'r', 't'}));
tablesToRemove.add(Tag.intValue(new byte[]{'m', 'o', 'r', 'x'}));
subsetter.setRemoveTables(tablesToRemove);
processedFont = subsetter.subset().build();
}
if (shouldStripHints) {
Subsetter hintRemover = new HintStripper(processedFont, factory);
Set<integer> hintTables = new HashSet<>();
hintTables.add(Tag.fpgm);
hintTables.add(Tag.prep);
hintTables.add(Tag.cvt);
hintTables.add(Tag.hdmx);
hintTables.add(Tag.VDMX);
hintTables.add(Tag.LTSH);
hintTables.add(Tag.DSIG);
hintTables.add(Tag.vhea);
hintRemover.setRemoveTables(hintTables);
processedFont = hintRemover.subset().build();
}
try (FileOutputStream fos = new FileOutputStream(destination)) {
if (outputWoff) {
WritableFontData woffOutput = new WoffWriter().convert(processedFont);
woffOutput.copyTo(fos);
} else if (outputEot) {
WritableFontData eotOutput = new EOTWriter(enableMtx).convert(processedFont);
eotOutput.copyTo(fos);
} else {
factory.serializeFont(processedFont, fos);
}
}
}
}
}
</integer></integer></integer></cmaptable.cmapid>
Après avoir remplacé le code source original par cette version modifiée, reconstruisez le fichier JAR avec ant. L'outil peut alors être invoqué ainsi :
java -jar sfnttool.jar -s "liste_caracteres.txt" police_originale.ttf police_optimisee.ttf
Le fichier liste_caracteres.txt doit contenir tous les caractères souhaités, encodés en UTF-8. Pour une utilisation courante, il peut inclure les 2500 caractères chinois les plus fréquents, l'alphabet latin, la ponctuation de base, etc. Pour éviter les problèmes d'encodage, il est recommandé d'utiliser cette méthode de lecture de fichier plutôt que de passer les caractères en argument direct.
Polices recommandées pour l'intégration web :
- Apple PingHei (diverses graisses)
- Tengxiang Jiali Thin Round Simplified
- PingFang SC
- Microsoft YaHei Light
- HanYi QiHei H2312F45