Typographie : registre de polices, sous-ensembles, CMap, encodage, BiDi
En un coup d’œil
Section intitulée « En un coup d’œil »Le module de typographie transforme un fichier de police et une chaîne Unicode en octets nécessaires au flux de contenu d’un PDF. Il prend en charge l’analyse des polices, le registre dont la durée de vie est celle du processus, les sous-ensembles de glyphes, la CMap ToUnicode, les stratégies d’encodage fondées sur la cmap et le moteur bidirectionnel Unicode.
Installation
Section intitulée « Installation »composer require nextpdf/core:^3Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »FontRegistry est le magasin de polices dont la durée de vie est celle du processus et implémente FontRegistryInterface. Il analyse une seule fois un fichier TrueType, OpenType, TTC ou Type 1 (PFB et AFM) et renvoie un FontInfo immuable. Le registre est conçu pour les workers de longue durée : préchauffe un jeu de polices au démarrage, puis appelle lock(). Le registre rejette toute mutation ultérieure, tandis que les recherches continuent de traiter le trafic. Il ne conserve que des données PHP pures — métadonnées analysées et octets bruts de la police — afin qu’un pool de workers puisse partager une seule instance. registerFromBinary() accepte des octets bruts de police, que le pont HTML @font-face utilise pour une police récupérée depuis une source distante ou une URI de données.
Le moteur incorpore et réduit en sous-ensemble chaque police qu’il utilise. Un programme de police incorporé est embarqué dans le PDF, de sorte que le document s’affiche de la même manière sur n’importe quel lecteur, indépendamment des polices système installées — ISO 32000-2 §9. Un sous-ensemble ne transporte que les glyphes que le document référence, ce qui est décisif pour le contenu CJK ou riche en Unicode — ISO 32000-2 §9. FontSubsetter analyse le répertoire de tables d’origine, extrait la cmap, résout les dépendances de glyphes composites par fermeture transitive et reconstruit les tables head, hhea, maxp, cmap, loca, glyf et hmtx. Il préserve la numérotation des identifiants de glyphes d’origine et remplit de zéros les emplacements inutilisés, de sorte qu’un CIDToGIDMap de type /Identity reste valide. Il renvoie la police d’origine inchangée lorsque le sous-ensemble économiserait moins de dix pour cent, ce qui évite un travail qui ne serait pas amorti. CffSubsetter effectue l’opération équivalente pour les polices OpenType qui transportent une table de contours au format Compact Font Format.
L’émission de texte est une traduction en trois étapes : point de code Unicode, puis code de caractère dans le flux de contenu, puis identifiant de glyphe dans la police. Le module modélise ce flux sous la forme d’un collaborateur unique et explicite. FontInfo::encodeText() est la façade ; FontEncodingStrategyResolver répartit le traitement par police. Une police TrueType ou OpenType incorporée qui transporte une cmap Unicode est dirigée vers TrueTypeCmapStrategy, qui émet un flux hexadécimal Identity-H de deux octets. C’est la forme qu’exigent une police de Type 0 dotée d’une CMap Identity-H et d’un descendant CIDFontType2 (ISO 32000-2 §9.7.4 ; le digest du chunk RAG correspondant a été renvoyé tronqué par le plafond de licence, consigné dans _downgraded-claims-o3.md). Toute autre police — polices standard Base 14, Type 1 PFB et AFM — est dirigée vers Base14EncodingStrategy, qui émet une chaîne littérale WinAnsi d’un seul octet. Ce flux couvre l’intégralité du répertoire WinAnsiEncoding (page de code Windows 1252) — latin accentué, signe euro et ponctuation typographique courante. Les points de code situés hors de ce répertoire sont écartés du flux sur un seul octet et empruntent le repli de police par cluster lorsqu’une police couvrante est enregistrée (ISO 32000-2 Annex D.2). Le résolveur couvre tout l’espace de valeurs FontInfo ; il n’existe aucun chemin nullable. ToUnicodeCMapBuilder construit la ressource /ToUnicode qui permet à un lecteur de retrouver l’Unicode d’origine à partir d’une police Identity-H. Il applique une coalescence gloutonne des bfrange et un plafond de 100 entrées par bloc.
BidiEngine est le service frontière de l’algorithme bidirectionnel Unicode (UAX #9, Unicode 16). Lorsque la prise en charge des isolats est désactivée, il délègue au résolveur historique afin que les appelants existants ne soient pas affectés. Lorsqu’elle est activée, il exécute le pipeline qui prend en charge les isolats : la pile d’isolats explicites avec une profondeur maximale de 125, les passes de types faibles, les passes de types neutres incluant la résolution des paires de crochets, ainsi que les passes de niveau implicite et de réordonnancement de ligne. La couverture de glyphes CJK pour une police candidate est un diagnostic distinct : CjkFontValidator échantillonne les blocs Unicode requis par écriture et rapporte un pourcentage de couverture.
Surface d’API
Section intitulée « Surface d’API »| Type | Nature | Membres clés | Stabilité | Depuis |
|---|---|---|---|---|
FontRegistry | classe finale | register(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage() | stable | 1.7.0 |
FontInfo | classe finale en lecture seule | $family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText() | stable | 1.0.0 |
FontSubsetter | classe finale | subset(string, array<int>, int): string | stable | 1.0.0 |
CffSubsetter | classe finale | Sous-ensemble de contours OpenType/CFF | stable | 1.0.0 |
FontEncodingStrategyResolver | classe finale | resolve(FontInfo): FontEncodingStrategy | stable | 2.7.0 |
ToUnicodeCMapBuilder | classe finale | buildFromRun(), buildFromMap(), encodeUnicodeUtf16Be() | stable | 2.7.0 |
BidiEngine | classe finale | Résolution UAX #9 prenant en charge les isolats | stable | 3.1.0 |
CjkFontValidator | classe finale | validateCoverage(), detectScript(), isCjkCodepoint() | stable | 1.0.0 |
FontInfo est immuable : la signature de son constructeur et ses propriétés publiques sont figées. Les stratégies d’encodage sont des fonctions pures de (FontInfo, UTF-8 text) — même entrée, même EncodedGlyphRun, à chaque appel.
Exemple de code — Prise en main
Section intitulée « Exemple de code — Prise en main »<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.assert($encoded->mode === EncodingMode::TwoByteCid);register() analyse la police une seule fois et renvoie un FontInfo immuable. encodeText() passe par le résolveur et renvoie un EncodedGlyphRun qui transporte le flux d’octets, l’opérande de chaîne PDF, les largeurs d’avance par glyphe et la table GID-vers-Unicode consommée par une CMap /ToUnicode.
Exemple de code — Production
Section intitulée « Exemple de code — Production »<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;use NextPDF\Typography\FontRegistry;use Psr\Log\LoggerInterface;
final readonly class FontBootstrap{ public function __construct( private FontRegistry $registry, private LoggerInterface $logger, ) {}
/** * Warm a font set at worker boot, then lock the registry for the * lifetime of the process. * * @param list<string> $fontFiles Absolute paths to font files. */ public function boot(array $fontFiles): void { try { $this->registry->warmup($fontFiles); $this->registry->lock(); } catch (NextPdfException $e) { $this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e; }
$report = $this->registry->memoryUsage(); $this->logger->info('Font cache primed', [ 'fonts' => $report->entryCount, 'bytes' => $report->currentBytes, ]); }}warmup() puis lock() constituent la séquence de démarrage du worker. Après lock(), toute mutation lève une exception. Les recherches continuent de traiter le trafic. memoryUsage() renvoie un MemoryReport afin qu’un worker puisse suivre le cache de polices au regard de son budget.
Cas limites et pièges
Section intitulée « Cas limites et pièges »- Un registre verrouillé rejette
register(),registerFromBinary(),addFontDirectory()etwarmup(). Préchauffe et verrouille au démarrage ; n’enregistre jamais de police pendant le traitement d’une requête. FontSubsetter::subset()renvoie les octets d’origine inchangés lorsque l’économie serait inférieure à dix pour cent, ou lorsqu’une table essentielle est manquante. Une police renvoyée identique à l’entrée correspond au cas documenté où il n’y a aucun gain, pas à un échec.- Le sous-ensembliseur préserve la numérotation des identifiants de glyphes d’origine et remplit de zéros les glyphes inutilisés. Cela maintient
CIDToGIDMap /Identityvalide ; ne suppose pas que les identifiants de glyphes sont renumérotés en une plage contiguë. registerFromBinary()écrit les octets dans un fichier temporaire pour les analyser et supprime à la fois le fichier d’extension et le fichier de basetempnam()dans un blocfinally. Les données de police non fiables constituent une surface d’attaque pour l’analyseur — filtre-les avant qu’elles n’atteignent l’analyseur (voir les notes de sécurité).BidiEnginedélègue mot pour mot au résolveur historique lorsque la prise en charge des isolats est désactivée. Les caractères de formatage d’isolat passent alors tels quels et sont considérés comme neutres aux frontières. Active la prise en charge des isolats via la politique de conformité pour obtenir le comportement UAX #9 complet.CjkFontValidatoréchantillonne les points de code selon un pas donné plutôt que de tester chacun d’eux, de sorte que son taux de couverture est une estimation statistiquement suffisante, pas un décompte exhaustif.
Performances
Section intitulée « Performances »L’analyse des polices domine la première utilisation ; le registre l’amortit en une seule analyse par processus. Après le préchauffage, get() et has() sont des recherches O(1) dans une table. Le coût de création du sous-ensemble évolue avec le nombre de glyphes que le document utilise réellement, pas avec la table de glyphes complète de la police. C’est pourquoi le sous-ensemble apporte à la fois un gain de taille et de vitesse pour le contenu CJK : le sous-ensembliseur traite les polices de plus de 20,000 glyphes au moyen de recherches binaires, de tampons pré-alloués et d’opérations groupées sur les chaînes. La résolution des glyphes composites est bornée — elle est plafonnée à 100 itérations de fermeture pour se défendre contre les références circulaires de composants. L’analyseur cmap Format 12 plafonne le nombre de groupes et d’entrées pour borner la mémoire face à une police hostile. Le performance_budget de 1500 ms en temps réel et 64 Mo de pic couvre un préchauffage de polices typique plus le rendu du document.
Notes de sécurité
Section intitulée « Notes de sécurité »Deux surfaces ont un impact de sécurité. La première est l’entrée de polices. register() et registerFromBinary() analysent des octets arbitraires. registerFromBinary() matérialise un fichier temporaire. Les wrappers de flux et les octets nuls dans les chemins sont rejetés à la frontière. Les données de police non fiables doivent passer par une politique de ressource externe qui borne la taille de fichier et le nombre de glyphes avant qu’elles n’atteignent l’analyseur. Les lecteurs binaires du sous-ensembliseur vérifient les bornes de chaque décalage. Les analyseurs cmap plafonnent le nombre de groupes, d’entrées et de tables (numGroups > 31000 et un plafond d’entrées de 200,000 en Format 12) afin qu’une police fabriquée ne puisse pas provoquer une allocation non bornée. La seconde surface est l’extraction de texte : ToUnicodeCMapBuilder valide que chaque code de caractère appartient à l’espace de code 16 bits et que chaque valeur Unicode est un scalaire valide — les demi-substituts sont rejetés — afin qu’une table malformée ne puisse pas produire une ressource d’extraction corrompue. Traite toute police ou tout texte fourni de l’extérieur comme non fiable.
Conformité
Section intitulée « Conformité »| Affirmation | Norme | Clause | Preuve |
|---|---|---|---|
| Chaque police utilisée par le document est incorporée afin que le document s’affiche sans dépendre des polices système. | ISO 32000-2 | §9 | |
| La police incorporée est réduite en sous-ensemble aux glyphes que le document référence. | ISO 32000-2 | §9 | |
Une police CJK TrueType incorporée est émise comme une police de Type 0 dotée d’une CMap Identity-H et d’un descendant CIDFontType2. | ISO 32000-2 | §9.7.4 | Digest RAG tronqué par le plafond de licence ; préfixe 7a5258772f508e3b, voir _downgraded-claims-o3.md |
Les deux premières clauses sont paraphrasées et ancrées par digest. Le digest RAG complet de la troisième clause n’a pas été renvoyé (troncature par le plafond de licence) ; il est corroboré par l’ADR-013 et la vue d’ensemble développeur de l’encodeur cmap, puis consigné comme déclassé. NextPDF ne reproduit pas de texte normatif. La conformité PDF/A-4 et PDF/UA-2 pour le contenu CJK dépend de la création de sous-ensembles côté écriture et du câblage /ToUnicode correspondant.
Contexte commercial
Section intitulée « Contexte commercial »Un pack de fonctionnalités OpenType commercial et des chaînes de polices de repli premium s’appuient sur le registre du cœur et sur la liaison d’encodage. Le module de typographie du cœur incorpore, réduit en sous-ensemble et encode chaque police sans licence ; le pack payant ajoute une résolution de repli soignée. L’omission d’un lien de conversion est intentionnelle — c’est de la documentation, pas un entonnoir de vente.
Voir aussi
Section intitulée « Voir aussi »- Font : registre TrueType, OpenType et CID — les types de valeurs de police, l’incorporation et le repli.
- Text : shaping, découpage, BiDi — la liaison de gestion et de shaping des runs qui consomme les glyphes encodés.
- Contracts / Typography — les contrats
FontRegistryInterfaceet de préprocesseur de texte. - Moteur de rendu HTML — le pont
@font-facequi appelleregisterFromBinary().