Aller au contenu

Réduire la taille d'un fichier PDF grâce à la compression et au subsetting

Tu veux produire le PDF le plus petit que ton contenu permette, sans aucune perte de fidélité. NextPDF te donne deux leviers de taille, tous deux activés par défaut :

  • Compression des flux. Le writer encapsule chaque flux de contenu de page et chaque programme de police incorporé dans un flux FlateDecode (zlib). Le drapeau NextPDF\Core\Config compress contrôle ce réglage. Redéfinis-le avec le wither withCompress() lorsque tu construis un document en streaming.
  • Subsetting des polices. Quand tu incorpores une police TrueType ou CFF, le writer reconstruit le programme de police pour ne conserver que les glyphes réellement utilisés par le document, puis compresse le résultat en FlateDecode. Cela se fait automatiquement : aucun drapeau à régler, aucun appel à effectuer. Une police CJK de 20,000 glyphes dont le document n’utilise que quelques centaines de glyphes est incorporée à une fraction de sa taille sur le disque.

Clarification d’emblée : NextPDF Core n’expose ni rééchantillonnage des images, ni réglage de qualité d’image, ni bascule de flux d’objets, ni paramètre de déduplication des ressources. Les contrôles de taille disponibles sont les deux ci-dessus. Le reste de ce recipe montre comment les utiliser correctement, ainsi que ce que chacun ne fait pas.

Prérequis : une installation de Core (composer require nextpdf/core:^3) et, pour le chemin de subsetting, un fichier de police que tu es autorisé à incorporer.

Fenêtre de terminal
composer require nextpdf/core:^3

Un PDF est un arbre d’objets. Les plus gros objets sont généralement les flux de contenu (les opérateurs de dessin de chaque page) et les programmes de police (les contours de glyphes incorporés). Les deux sont des données textuelles ou binaires très compressibles ; le levier de taille le plus efficace consiste donc à les compresser en FlateDecode. FlateDecode est, dans PDF 2.0, le nom d’un flux DEFLATE encapsulé en zlib (ISO 32000-2:2020 §7.4.4), et c’est le filtre émis par NextPDF.

Le writer épingle le niveau de compression DEFLATE à 9, le maximum RFC 1951, via NextPDF\Writer\PinnedZlibCompressor. Le niveau 9 consomme un peu plus de CPU en échange du flux le plus petit. Cet épinglage garde aussi la sortie déterministe, parce que l’en-tête zlib encode le niveau et qu’un niveau qui varierait changerait les octets. Tu ne choisis pas le niveau — le moteur le fixe pour que deux exécutions sur la même entrée produisent des flux strictement identiques.

Le second levier est le subsetting des polices. Un fichier de police sur le disque contient chaque glyphe que la fonte définit, mais un document qui imprime « Invoice 2026 » n’en a besoin que d’une douzaine. NextPDF\Typography\FontSubsetter (pour TrueType) et NextPDF\Typography\CffSubsetter (pour CFF / OpenType) parcourent les points de code que le document a réellement rendus, résolvent les dépendances des glyphes composites, et reconstruisent uniquement les tables de police nécessaires. Ils émettent un binaire de police sous-ensemble valide avec un tag déterministe de préfixe de sous-ensemble de six lettres (ISO 32000-2:2020 §9.9). Le writer applique cela chaque fois que l’ensemble de glyphes utilisés d’une police incorporée est connu, puis compresse le sous-ensemble en FlateDecode. Si le subsetting d’une police donnée ferait économiser moins de dix pour cent, le subsetter renvoie le programme d’origine à la place, parce que le surcoût de reconstruction ne vaut pas un gain marginal.

À retenir : tu gardes les PDF compacts en laissant la compression activée (le défaut) et en incorporant de vrais fichiers de police (pour que le subsetting ait de quoi réduire), pas en ajustant une longue liste d’options.

Le seul réglage de taille que tu ajustes se trouve sur l’objet de configuration.

NextPDF\Core\Config est un objet-valeur immuable et final readonly doté de méthodes wither typées. Le membre concerné est :

  • compress (bool, défaut true) — active la compression FlateDecode. Redéfinis-le avec withCompress(bool $compress): self, qui renvoie un nouveau Config avec le drapeau modifié et tous les autres champs préservés.

Associe un Config à un document au moment de la construction :

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self construit un document avec des registres éphémères pour un script CLI ou un processus de courte durée, en appliquant ton Config.

Deux membres déterminent la matière disponible pour ces leviers de taille, mais aucun n’est lui-même un contrôle de compression :

  • imageCacheBytes (int, défaut 52_428_800) plafonne le cache d’images en mémoire, et withImageCacheBytes(int $bytes): self le modifie. Ce réglage borne la mémoire de pointe pendant une construction. Il ne rééchantillonne pas, ne recompresse pas et ne réduit pas autrement les images que tu incorpores — c’est un plafond mémoire, pas un contrôle de taille de sortie.
  • fontsDirectory (string) et withFontsDirectory(string $dir): self définissent le chemin de recherche par défaut des fichiers de police, qui alimente le chemin de subsetting.

Les opérations sur les polices passent par la surface typographique du document :

  • setFont(string $family, string $style = '', float $size = 12.0): static sélectionne une police. Quand la famille correspond à un fichier de police incorporable, le writer enregistre les points de code que tu rends pour pouvoir créer le subset de cette police au moment de l’enregistrement.
  • addFontDirectory(string $directory): static enregistre un répertoire supplémentaire où chercher des fichiers de police.

Pour la sortie, le trio standard est : getPdfData(): string renvoie les octets, save(string $path): void les écrit de façon atomique, et output(?string $filename, OutputDestination $dest): string gère la livraison HTTP.

Le subsetting n’a aucune méthode publique et aucun drapeau. Il découle simplement de l’incorporation d’une police et du rendu de texte — le writer pilote FontSubsetter / CffSubsetter à ta place à l’intérieur de NextPDF\Writer\PdfFontWriter.

Cet exemple construit un document avec la compression explicitement activée et une police incorporée puis sous-ensemblée, puis écrit les octets. Il omet la gestion des erreurs pour montrer la forme de l’appel ; l’exemple de production ci-dessous ajoute toutes les protections.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.
$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);
$doc->addFontDirectory(__DIR__ . '/fonts');
$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer
// subsets this font automatically when the document is built.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));

Cet exemple est un programme autonome. Il construit un document avec la compression activée, incorpore une police depuis un répertoire que tu contrôles, rend du texte pour que le subsetter dispose d’un ensemble de glyphes utilisés, et écrit le résultat de façon atomique. Il intercepte les exceptions NextPDF les plus spécifiques que les chemins de construction et d’enregistrement peuvent lever, puis les relance avec du contexte au lieu de les ignorer. Fais pointer NEXTPDF_FONT_DIR vers un répertoire contenant une police TrueType ou CFF que tu es autorisé à incorporer ; le programme valide le chemin avant l’incorporation.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
/**
* Resolve and validate the font directory from a server-controlled source.
*
* Reading the directory from the environment keeps the path off the request
* surface. The function rejects a missing or unreadable directory so the
* embedding path never runs against untrusted or absent input.
*/
function resolveFontDirectory(): string
{
$configured = getenv('NEXTPDF_FONT_DIR');
$dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir);
if ($real === false || !is_dir($real) || !is_readable($real)) {
throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir));
}
return $real;
}
/**
* Build a compressed, font-subsetted document and return its bytes.
*
* @param non-empty-string $fontDirectory Validated directory of embeddable fonts.
*
* @return string Raw PDF bytes.
*/
function buildCompactPdf(string $fontDirectory): string
{
// compress is true by default; pin it so the intent is explicit and the
// streaming writer path honours it regardless of any wrapper defaults.
$config = (new Config())
->withCompress(true)
->withFontsDirectory($fontDirectory)
// Bound the image cache so a build cannot exhaust memory. This is a
// memory ceiling, not an output-size control.
->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config);
$doc->addFontDirectory($fontDirectory);
$doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the
// writer turns into a font subset at build time.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026', newLine: true);
$doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program
// are FlateDecode-compressed before the bytes are returned.
return $doc->getPdfData();
}
try {
$fontDirectory = resolveFontDirectory();
$pdf = buildCompactPdf($fontDirectory);
} catch (CompressionException $e) {
// Raised if the zlib encoder hard-fails while compressing a stream.
throw new RuntimeException(
sprintf('Compression failed for a %s stream.', $e->getAlgorithm()),
previous: $e,
);
} catch (InvalidConfigException $e) {
// Raised by the output path for an invalid destination configuration.
throw new RuntimeException(
sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()),
previous: $e,
);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) {
throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));
}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);

Sortie STDOUT attendue (le nombre d’octets dépend de la police et de la construction) :

Wrote <n> bytes to <path>.
  • La compression est activée par défaut. Un Config tout neuf a compress réglé sur true. Tu as rarement besoin d’appeler withCompress(). Ne le définis explicitement que pour documenter l’intention, ou pour le désactiver dans une construction de débogage où tu veux lire les flux bruts.
  • Désactiver la compression rend les fichiers plus gros, pas plus petits. withCompress(false) est une aide au diagnostic pour inspecter les flux non compressés. Ce n’est jamais une optimisation de taille. Livre avec la compression activée.
  • Le subsetting a besoin d’une vraie police incorporée. Les polices standard Base14 (Helvetica, Times, Courier et leurs proches) sont référencées par leur nom et ne portent aucun programme incorporé dans un document simple, donc il n’y a rien à sous-ensembler. Le subsetting ne réduit que les polices que tu incorpores depuis un fichier de police.
  • Le subsetting est automatique et silencieux. Il n’y a aucun drapeau, aucune méthode et aucune confirmation. Si tu as incorporé une police et rendu du texte avec elle, le writer l’a sous-ensemblée. Le programme incorporé porte un tag de préfixe de sous-ensemble de six lettres (par exemple ABCDEF+LiberationSans) pour qu’un lecteur puisse distinguer un sous-ensemble d’une incorporation complète.
  • Une faible économie conserve la police complète. Quand un sous-ensemble ferait économiser moins de dix pour cent de la taille du programme, le subsetter renvoie l’original. C’est un plancher délibéré : la reconstruction ne vaut pas un gain marginal. Incorporer une police déjà minuscule, ou rendre presque tous ses glyphes, peut tomber dans ce cas.
  • imageCacheBytes n’est pas un réglage de taille d’image. Il plafonne la mémoire, pas les octets de sortie. NextPDF Core incorpore les données d’image que tu lui donnes ; il n’y a aucune étape de rééchantillonnage, de sous-échantillonnage ou de réencodage. Si tu as besoin d’images plus petites, redimensionne-les et réencode-les avant de les incorporer.
  • Aucun réglage de flux d’objets ou de déduplication n’existe. NextPDF Core n’expose aucune bascule pour les flux d’objets PDF 2.0 ni pour la déduplication des ressources. N’en cherche pas — les leviers de taille sont la compression des flux et le subsetting des polices.

La compression au niveau 9 est le principal coût CPU lors de l’écriture d’un flux. Elle ajoute quelques pour cent au temps de construction en échange de la sortie la plus petite. Le coût est linéaire par rapport au nombre d’octets non compressés ; le nombre de pages et la quantité de données de police incorporées déterminent donc le budget. Le subsetting ajoute une passe unique par police incorporée qui analyse le répertoire de tables de la police, résout la fermeture des glyphes utilisés, et reconstruit les tables nécessaires. Pour une grande police CJK, c’est le plus coûteux des deux leviers, mais il s’exécute une fois par police, pas une fois par page. Le plancher d’économie de dix pour cent existe en partie pour garder cette passe hors du chemin critique quand elle ne serait pas rentable. Un petit document avec un seul sous-ensemble incorporé reste confortablement sous un plafond de 1500 ms et un budget de pointe de 96 Mo. Borne imageCacheBytes à ton vrai plafond pour qu’une construction qui incorpore beaucoup d’images échoue vite par manque de mémoire plutôt que de partir en swap.

La construction s’exécute dans le processus courant ; aucun octet du document ne quitte l’hôte et aucun appel réseau n’est effectué. Traite toute police ou image fournie de l’extérieur comme une entrée non fiable :

  • Valide le répertoire de polices. L’exemple de production lit le chemin des polices depuis une variable d’environnement contrôlée par le serveur et rejette un répertoire manquant ou illisible avant l’incorporation. Ne dérive jamais un chemin de police d’un champ de requête.
  • N’incorpore que des polices que tu es autorisé à redistribuer. Un sous-ensemble reste un programme de police incorporé. Confirme que la licence permet l’incorporation avant de livrer un document qui porte la police.
  • Les polices malformées lèvent une exception, elles ne corrompent pas en silence. Un fichier de police dont l’analyse échoue lève NextPDF\Exception\FontParsingException, et un échec fatal de zlib lève NextPDF\Exception\CompressionException. Intercepte l’exception la plus spécifique et agis en conséquence. N’enveloppe jamais la construction dans un catch vide.
  • N’interpole jamais d’entrée utilisateur dans le chemin de sortie. L’exemple écrit vers un chemin fixe ou un canal secondaire contrôlé par le serveur, et il rejette les wrappers de flux et les octets nuls grâce au writer atomique dans save(). Dérive les chemins de sortie de valeurs contrôlées par le serveur pour éviter la traversée de répertoires.
  • Aucun secret dans le document. N’incorpore pas d’identifiants, de jetons ou d’identifiants internes dans un document généré que tu renvoies à un client.

Ce recipe ne formule aucune revendication normative propre sur les standards. Les mécanismes qu’il utilise sont définis par la spécification PDF 2.0 : la compression de flux FlateDecode (ISO 32000-2:2020 §7.4.4) et le nommage des sous-ensembles de police avec un préfixe de sous-ensemble de six caractères (ISO 32000-2:2020 §9.9). NextPDF émet les deux via son chemin d’écriture standard ; tu ne les configures pas au-delà du drapeau compress. Le profil de reproductibilité structural que cette page déclare reflète le fait que le writer épingle le niveau DEFLATE : les flux compressés sont donc déterministes, tandis que les identifiants au niveau du document peuvent encore varier entre les exécutions sauf si tu configures aussi des réglages déterministes. Pour le mécanisme d’incorporation derrière le subsetting, consulte le recipe d’incorporation et de subsetting lié ci-dessous.