Aller au contenu
getnextpdf.com

Provisionner les polices en production

Ton PDF s’affiche correctement sur ton portable, puis part vers un conteneur et en ressort sous forme de rangée de cases vides — le glyphe « tofu » — ou avec des accents et des caractères non latins manquants. La cause est presque toujours la même : la police que tu as sélectionnée n’est pas présente dans l’image déployée.

Le moteur NextPDF natif, en processus, résout les polices à partir de fichiers de polices que le registre de polices peut lire. Il ne découvre pas automatiquement les polices de l’OS ni de fontconfig — les fichiers de polices installés par l’OS n’aident que si tu enregistres explicitement ces fichiers ou ajoutes leur répertoire conteneur au chemin de recherche du FontRegistry. Un conteneur construit à partir d’une image de base minimale n’a aucune police installée par apt/apk, et même quand il en a, le moteur natif les ignore tant que tu ne pointes pas le registre vers leurs fichiers. La solution consiste à empaqueter les vrais fichiers de polices à l’intérieur de ton application ou de ton image et à les enregistrer auprès du moteur. Le registre lit les fichiers TrueType (.ttf), OpenType (.otf) et TrueType Collection (.ttc) ; le Type1 hérité (.pfb) est aussi accepté mais rarement nécessaire pour de nouveaux travaux.

Avant de commencer, confirme que ces éléments sont en place :

  • NextPDF core est installé.
  • Tu disposes des vrais fichiers de polices que tu comptes utiliser, et tu as le droit de les embarquer. Les droits d’embarquement relèvent de ta responsabilité — voir Embarquer et sous-ensembler une police TrueType.
  • Ton build peut copier ces fichiers dans l’artefact déployé.

C’est un mode d’emploi d’exploitation. Le code est minimal ; le travail réside dans le build et l’agencement du système de fichiers. Pour la mécanique au niveau de l’API d’enregistrement et de sous-ensemblage d’une seule fonte, lis la recette embed-and-subset liée ci-dessus. Cette page couvre la mise des fichiers sur la machine et le pointage du moteur vers eux.

Pourquoi le moteur natif ne trouve pas automatiquement les polices de l’OS

Section intitulée « Pourquoi le moteur natif ne trouve pas automatiquement les polices de l’OS »

Il existe deux chemins de rendu distincts, et l’histoire des polices diffère entre eux.

  • Moteur natif en processus (le défaut, Document / writeHtml) : le moteur n’appelle pas le système de polices du système d’exploitation ni fontconfig pour la découverte. Il résout une fonte via le registre de polices, qui lit un fichier de police précis que tu as enregistré ou trouve un fichier dans un répertoire que tu as configuré comme chemin de recherche. Installer une police avec apt-get install fonts-noto ou exécuter fc-cache ne fait rien en soi — le moteur natif ne voit ces fichiers que si tu les enregistres ou ajoutes leur répertoire au chemin de recherche du registre.
  • Pont Chrome (le moteur de rendu HTML-vers-PDF qui pilote un navigateur headless) : ce chemin utilise bien les polices installées de l’hôte via la découverte de polices normale du navigateur, donc les paquets de polices apt/apk et fontconfig comptent là.

Si tu lis des conseils généraux du type « installe ces paquets de polices système dans ton Dockerfile », ils s’appliquent au pont Chrome, pas au moteur natif couvert sur cette page. Pour la génération native, empaquette les fichiers et enregistre-les.

Étape 1 — Empaqueter les vrais fichiers de polices

Section intitulée « Étape 1 — Empaqueter les vrais fichiers de polices »

Mets les fichiers de polices à l’intérieur de l’arborescence de ton application pour qu’ils soient versionnés et livrés avec chaque build. Un emplacement conventionnel est un répertoire resources/fonts/.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

Nomme les fichiers pour que la recherche par répertoire du moteur puisse les retrouver par famille et par style. Quand tu enregistres un répertoire (plutôt qu’un fichier précis) et que tu appelles ensuite setFont('DejaVuSans', 'B', 12), le moteur cherche des fichiers tels que DejaVuSans-B.ttf, DejaVuSansB.ttf ou DejaVuSans.ttf dans chaque répertoire configuré. La recherche par répertoire construit ces noms candidats à partir du même code de style à une lettre que tu passes à setFont (B pour gras, I pour italique, BI pour gras-italique), et non d’un mot écrit en toutes lettres — donc la forme fiable est Family-<StyleCode>.ttf (par exemple DejaVuSans-B.ttf ou DejaVuSans-BI.ttf), pas Family-Bold.ttf. Un fichier nommé DejaVuSans-Bold.ttf n’est jamais trouvé par la recherche par répertoire ; pour utiliser un tel fichier, enregistre-le explicitement avec register() — ce qui analyse la police et l’indexe sous la famille et le style lus dans ses propres tables de noms, si bien que le nom de fichier écrit en toutes lettres n’a plus d’importance (voir l’étape 2).

Étape 2 — Enregistrer les polices auprès du moteur

Section intitulée « Étape 2 — Enregistrer les polices auprès du moteur »

Tu as deux moyens équivalents de rendre les fichiers visibles. Les deux passent par NextPDF\Typography\FontRegistry, qui implémente NextPDF\Contracts\FontRegistryInterface.

Enregistre un fichier précis sous un alias quand tu contrôles la fonte exacte :

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0) accepte les fichiers .ttf, .otf et .ttc, plus le Type1 hérité .pfb (qui charge ses métriques .afm compagnes depuis le même chemin) ; $fontIndex sélectionne une sous-police à l’intérieur d’une TrueType Collection (.ttc). register() analyse le fichier et indexe la fonte par la famille et le style lus dans ses propres tables de noms, donc le nom de fichier physique est sans importance une fois enregistré. L’$alias optionnel n’est qu’un nom de recherche supplémentaire pour la fonte — ce n’est pas un code de style et il ne change pas le style fourni par le fichier ; passe-le quand tu veux appeler setFont() avec un nom autre que le nom de famille embarqué de la police. Il renvoie le FontInfo analysé.

Enregistre un répertoire quand tu veux que le moteur résolve les fontes par nom depuis un dossier que tu contrôles :

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

Le constructeur de FontRegistry prend ce répertoire comme premier argument, et addFontDirectory() ajoute d’autres chemins de recherche. Un Document nu expose aussi addFontDirectory() pour le cas autonome.

Pour utiliser un registre que tu as toi-même peuplé, construis les documents via DocumentFactory, qui câble exactement ce registre dans chaque document qu’il crée :

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone() construit son propre registre interne, donc une fonte que tu as enregistrée sur un FontRegistry séparé lui est invisible. En production, passe par DocumentFactory (ou la fabrique de ton framework) pour que le registre peuplé soit celui en service.

Chaque intégration de framework expose les deux mêmes concepts sous forme de configuration, si bien que tu touches rarement le registre directement. Dans le nextpdf.php du paquet Laravel, fonts_path (par défaut NEXTPDF_FONTS_PATH, avec repli sur resource_path('fonts')) est le répertoire de recherche, et preload_fonts est une liste de chemins absolus de fichiers de polices analysés au démarrage du worker. Pointe fonts_path vers le répertoire que tu as empaqueté et tes fontes enregistrées se résolvent automatiquement.

Étape 3 — Provisionner les polices dans une image Docker

Section intitulée « Étape 3 — Provisionner les polices dans une image Docker »

Dans un conteneur, les fichiers de polices doivent faire partie de la couche d’image, copiés au moment du build. Comme le code applicatif et les polices sont livrés ensemble quand tu les empaquettes sous resources/fonts/, un simple COPY . . les emporte déjà. Si tu gardes les polices hors du contexte de build, copie-les explicitement et assure-toi que le chemin que tu enregistres correspond au chemin à l’intérieur de l’image.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

Sur un système de fichiers immuable ou en lecture seule (un conteneur readOnlyRootFilesystem, une image serverless ou un hôte durci), les fichiers de polices sont lus au moment de la génération et jamais écrits, donc un montage en lecture seule convient. La seule écriture que le moteur peut vouloir est son cache de polices analysées : soit tu donnes à ce répertoire un petit volume inscriptible, soit tu chauffes et verrouilles le registre au démarrage (section suivante) pour qu’aucune écriture ni aucun enregistrement à l’exécution ne soit tenté.

Dans un worker à longue durée de vie, analyse chaque fonte une fois au démarrage, puis verrouille le registre pour qu’aucun enregistrement par requête n’ait lieu et qu’une mauvaise configuration échoue bruyamment au lieu de retomber silencieusement :

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

Après lock(), register(), addFontDirectory() et warmup() lèvent une exception, ce qui transforme une erreur de « mauvais chemin dans l’image » en échec de démarrage net plutôt qu’en page de tofu en production.

Ajoute une vérification de fumée de déploiement qui rend une page avec chaque fonte requise. Le contrôle d’en-tête ci-dessous vérifie seulement que le document a produit une sortie — il ne prouve pas que la police a été analysée, embarquée ni même résolue. Une fonte que le moteur ne trouve pas peut retomber sur une police de base standard (et, sous le comportement non strict actuel, un profil de conformité peut au contraire fournir un substitut embarqué) tout en émettant un PDF valide et non vide — donc même là où ce repli se produit, ce contrôle seul ne détectera pas la dégradation silencieuse. Ne te fie pas à ce que le repli soit garanti ou silencieux sur chaque chemin ; vérifie directement le programme embarqué, comme montré ci-dessous :

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

Pour réellement faire échouer le déploiement quand une fonte est absente, cherche dans le PDF émis le programme de police embarqué. Une fonte enregistrée qui se résout porte son propre dictionnaire de police avec un programme embarqué ; affirmer sa présence détecte donc le cas où la fonte demandée ne s’est jamais résolue (quel que soit le repli du moteur) que le contrôle d’en-tête manque. La clé qui contient le programme dépend du format des contours : les contours TrueType (.ttf, .ttc) utilisent /FontFile2, les contours CFF/OpenType (.otf à contours PostScript) utilisent /FontFile3, et le Type1 hérité (.pfb) utilise /FontFile.

S’il te suffit d’un signal agnostique au format « un programme de police quelconque est embarqué », teste /FontFile seul — parce que /FontFile est une sous-chaîne de /FontFile2 comme de /FontFile3, un simple test de sous-chaîne correspond déjà à tout type de contour, et ajouter /FontFile2//FontFile3 comme branches || supplémentaires est redondant :

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

Une simple sous-chaîne /FontFile ne peut cependant pas distinguer les types de contours. Pour les distinguer, fais correspondre le jeton exact avec une limite de mot pour que /FontFile ne se déclenche pas aussi sur /FontFile2 ou /FontFile3 :

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

Dans tous les cas, considère cela comme une heuristique grossière seulement, pas comme une barrière de déploiement fiable. Une recherche d’octets bruts dans le PDF sérialisé est inexacte pour plusieurs raisons : les programmes de police peuvent vivre dans des flux d’objets compressés (où /FontFile* n’apparaît jamais en octets bruts), les mises à jour incrémentales peuvent ajouter ou remplacer des objets, les polices non embarquées ou standard-14 ne portent légitimement aucun programme de police, et les différences de sérialisation (ordre des objets, espaces, encodage des noms) peuvent déplacer ou masquer le jeton. Au mieux, elle confirme qu’une fonte a embarqué un programme — jamais que la fonte précise que tu voulais s’est résolue.

Pour une vraie barrière de déploiement, ne te fie pas à la recherche d’octets. Analyse le PDF émis avec un véritable analyseur PDF ou un inspecteur d’objets et affirme que l’objet de police de la fonte cible porte un programme embarqué /FontFile//FontFile2//FontFile3, ou utilise une assertion de résolution de police fournie par le produit si ton intégration en propose une. Les expressions régulières sensibles au jeton ci-dessus sont utiles pour un contrôle de cohérence local rapide, mais c’est une inspection structurelle qui devrait faire échouer le déploiement. L’embarquement et la structure du dictionnaire de police sont décrits dans Embarquer et sous-ensembler une police TrueType.

  • createStandalone() a son propre registre. Une fonte enregistrée sur un FontRegistry séparé n’est pas visible d’un document autonome. Utilise DocumentFactory (ou la fabrique du framework) pour que ton registre soit celui qui est actif.
  • Les fichiers de style doivent exister en tant que fichiers. Le moteur ne synthétise pas le gras ni l’italique à partir d’une fonte régulière. Si tu appelles setFont('DejaVuSans', 'B'), la recherche par répertoire cherche DejaVuSans-B.ttf, DejaVuSansB.ttf ou DejaVuSans.ttf (en minuscules et avec les variantes .otf aussi) — elle forme le candidat à partir du code de style littéral B, donc elle ne cherche jamais DejaVuSans-Bold.ttf. Un fichier au nom écrit en toutes lettres comme DejaVuSans-Bold.ttf ne se résout que lorsque tu l’enregistres explicitement avec register(), qui l’indexe par la famille et le style lus dans ses propres tables de noms quel que soit le nom de fichier ; compter sur la recherche par répertoire pour le trouver produit un échec, après quoi le moteur peut retomber sur une police de base (pas un chemin garanti ni toujours silencieux) — la dégradation contre laquelle cette page met en garde.
  • Les chemins via stream-wrapper et distants sont rejetés. Le registre refuse les chemins contenant un schéma d’URI ou un octet nul. Enregistre uniquement des fichiers locaux ; pour des polices récupérées à l’exécution, utilise registerFromBinary() avec les octets bruts.
  • Un registre verrouillé est immuable. Une fois que tu appelles lock(), tout register(), addFontDirectory() ou warmup() ultérieur lève une exception. Les méthodes de recherche restent disponibles. Enregistre et chauffe tout avant de verrouiller.
  • Les collections CJK sont volumineuses. Enregistre la bonne sous-police d’un .ttc avec $fontIndex, et prévois un budget pour un sous-ensemble embarqué plus grand. Voir les notes CJK dans la recette embed-and-subset.
  • Un fichier de police est une entrée binaire non fiable. N’empaquette que des polices de sources de confiance, et valide la provenance de toute fonte acceptée de la part d’utilisateurs finaux.
  • Verrouiller le registre après le préchauffage supprime une surface de mutation à l’exécution et fait échouer une erreur de chemin au démarrage plutôt que de dégrader silencieusement la sortie.
  • N’interpole pas d’entrée utilisateur dans un chemin de fichier enregistré. Enregistre un ensemble fixe de fontes empaquetées ; ne laisse pas une requête choisir un chemin de système de fichiers arbitraire.

Ce guide ne fait aucune revendication normative de standard. Chaque symbole montré est une surface publique vérifiée : NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), l’argument répertoire du constructeur), son contrat NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() et NextPDF\Core\Document::setFont() / addFontDirectory(). Les clés Laravel fonts_path et preload_fonts sont la configuration documentée du paquet nextpdf/laravel. Le comportement d’embarquement et de marquage de sous-ensemble, avec ses citations ISO 32000-2, est documenté dans la recette embed-and-subset liée sous Voir aussi.