Pour réduire la taille des fichiers PDF en Delphi, losLab PDF Library propose trois API qui s'attaquent aux trois plus grandes sources de surcharge : SubsetEmbeddedFonts réécrit chaque programme de polices TrueType intégrées pour le réduire aux glyphes que le document affiche réellement, DownsampleImages rééchantillonne les images matricielles qui dépassent une résolution cible (DPI), et NormalizeLZWStreams remplace l'ancienne compression LZWDecode par FlateDecode. Chacune renvoie le nombre d'objets modifiés, ainsi une valeur nulle vous indique que l'étape n'a eu aucun effet (no-op) plutôt qu'un échec silencieux
Pourquoi mon PDF fusionné est-il plus volumineux que ses fichiers sources ?
Un PDF fusionné ou généré par programme est généralement surdimensionné pour l'une de ces trois raisons : des polices entièrement intégrées, des images échantillonnées bien au-delà de leur résolution d'affichage, ou des flux toujours compressés avec l'ancien filtre LZW. L'ISO 32000-1 §9.9 permet à un producteur d'intégrer le programme de polices complet, et la plupart des producteurs le font précisément car c'est le choix par défaut le plus sûr. Un programme complet Arial FontFile2 représente des centaines de kilo-octets ; intégrez-le dans une douzaine de fichiers sources, fusionnez-les, et vous transporterez une douzaine de copies de contours de glyphes pour des caractères que personne n'a saisis. La fusion en elle-même ne crée pas ce gaspillage, elle ne fait que le concentrer dans un seul fichier où le volume total finit par se faire remarquer
Les images sont la deuxième source de problème. Un scan de 4800 pixels de large placé dans un cadre d'un quart de page contient environ 40 fois plus de données de pixels qu'un pipeline d'impression à 300 DPI ne peut en exploiter. La troisième source est plus discrète : les flux filtrés avec LZWDecode. L'ISO 32000-1 §7.4.4 spécifie à la fois LZWDecode et FlateDecode, et note que Flate compresse généralement au moins aussi bien ; en pratique, le résultat de Flate est systématiquement plus petit pour les mêmes données, et LZW survit principalement dans les fichiers qui sont passés par des outils des années 1990 à un moment donné de leur histoire. Le reste de cet article présente les trois passes de losLab PDF Library qui corrigent chacun de ces problèmes, puis les combine en un seul pipeline
Sous-ensemblement de polices avec SubsetEmbeddedFonts
SubsetEmbeddedFonts réduit chaque police TrueType intégrée dans un document chargé aux caractères réellement utilisés par le document. Aucun argument n'est nécessaire car il extrait la liste de conservation à partir des flux de contenu eux-mêmes. En interne, la passe parcourt le flux de contenu de chaque page avec GetTextRuns, collecte les codes de caractères référencés sous chaque ressource de police, construit une liste de conservation et transmet le programme de polices d'origine au moteur Windows FontSub (CreateFontPackage) pour produire un sous-ensemble. Le programme réécrit remplace le flux FontFile2 sur place, et le nom BaseFont reçoit une étiquette LOSABC+, la convention de six lettres majuscules suivies d'un signe plus définie par l'ISO 32000-1 §9.6.4 pour les polices de sous-ensemble. Ce préfixe rend également l'appel idempotent : exécutez la passe deux fois, et les polices déjà sous-ensemblement sont reconnues et ignorées, ce qui permet de l'intégrer en toute sécurité dans un traitement par lots pouvant réexaminer des fichiers
var
Lib: TPDFlib;
Fonts: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
begin
Fonts := Lib.SubsetEmbeddedFonts;
// Fonts = number of FontFile2 programs rewritten;
// 0 means nothing embedded, or everything already subsetted
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
Deux détails d'implémentation méritent d'être connus car ils expliquent les limites de l'API. Premièrement, la passe cible FontFile2, elle couvre donc les programmes TrueType intégrés ; les polices intégrées en tant que Type 1 ou CFF brut sont laissées intactes pour éviter tout risque. Deuxièmement, elle s'appuie sur FontSub, ce qui limite SubsetEmbeddedFonts à Windows. Un point plus subtil de l'implémentation : la qualification d'une police est décidée en résolvant réellement la chaîne de référence FontDescriptor → FontFile2, et non en se fiant à une heuristique de drapeau (flag) d'intégration, car les polices d'un document chargé n'ont jamais fait l'objet de la comptabilité côté création qui définit ces drapeaux. Si le flux résolu existe, la police est candidate ; sinon, elle est ignorée sans erreur
Le compromis réel : une police de sous-ensemble ne contient que les glyphes présents au moment du sous-ensemblement. Si un outil en aval, ou votre propre code, ajoute ultérieurement du texte avec cette même police, tout caractère en dehors du sous-ensemble n'aura pas de contour et s'affichera sous la forme d'un glyphe manquant. Réalisez le sous-ensemblement comme dernière étape de modification du contenu, jamais avant une phase d'édition. La même prudence s'applique si vous prévoyez d'extraire la police plus tard pour la réutiliser ; l'article sur l'extraction de texte, d'images et de polices avec PDFlibPas présente ce qu'un programme de sous-ensemble extrait peut ou ne peut pas vous apporter
Comment DownsampleImages décide-t-il quelles images réduire ?
DownsampleImages(MaxDPI, Quality, Filter) ne rééchantillonne que les images qu'elle peut qualifier avec certitude de suréchantillonnées, en utilisant une estimation de DPI délibérément prudente. Un objet d'image PDF XObject stocke les dimensions en pixels mais aucune résolution physique fiable, et toute étiquette DPI de l'image source survit rarement à un cycle chargement-édition-sauvegarde. Ainsi, la passe estime SrcDPI = PixelWidth / 8,5, ce qui revient à se demander : si cette image occupait toute la largeur d'une page au format Letter, quelle serait sa résolution ? Seules les images dont l'estimation dépasse MaxDPI sont traitées. Ce biais est intentionnel : une image affichée en petite taille sur la page possède un DPI réel plus élevé que l'estimation, de sorte que la passe se déclenche moins souvent plutôt que de dégrader une ressource de qualité d'impression qu'elle ne peut pas mesurer
Quality de 1 à 100 sélectionne la qualité de réencodage JPEG, tandis que 0 conserve la sortie au format Flate sans perte (de type PNG) ; Filter choisit le noyau de rééchantillonnage, 0 pour une moyenne de boîte (box average) et 1 pour bilinéaire. Pour les documents de bureau numérisés, DownsampleImages(150, 75, 1) is a sensible starting point ; pour tout ce qui est susceptible d'être réimprimé, augmentez le MaxDPI à 300 ou ignorez complètement la passe. Le sous-échantillonnage est la seule étape avec perte parmi les trois, elle doit donc être placée derrière un paramètre que vos utilisateurs peuvent désactiver
Convertir les anciens flux LZW avec NormalizeLZWStreams
NormalizeLZWStreams est un gain gratuit : il décompresse sans perte chaque flux LZWDecode et le recompresse avec FlateDecode, sur place, en renvoyant le nombre de flux convertis. Il gère à la fois une entrée unique /Filter /LZWDecode et les cas où LZW apparaît dans un tableau de chaîne de filtres, où seul le lien LZW est remplacé tandis que le reste de la chaîne est préservé. Les paramètres du prédicteur (Predictor, Columns, Colors, BitsPerComponent) are read from the stream's DecodeParms and passed through to the decompressor, so predictor-encoded image data round-trips correctly. Les deux filtres étant des codecs exacts au bit près, les octets décodés sont identiques avant et après ; seule la compression du conteneur change, c'est pourquoi cette passe peut être exécutée en toute sécurité de manière inconditionnelle sur chaque fichier
Sur un document sans flux LZW, l'appel renvoie simplement 0 et ne modifie rien, ce que la suite de régression de la bibliothèque teste explicitement : un fichier uniquement Flate fraîchement créé doit signaler zéro conversion. Cette garantie d'absence d'effet (no-op) est importante lorsque la passe fait partie d'un pipeline qui traite des milliers de fichiers hétérogènes, certains datant de 2024 et d'autres de 1998
Le pipeline complet d'optimisation de taille en Delphi
Les trois passes se combinent en une seule fonction chargement-optimisation-sauvegarde, et l'ordre importe moins que l'on pourrait le penser car elles opèrent sur des types d'objets disjoints : polices, objets d'image XObject et filtres de flux. L'exécution du sous-ensemblement en premier reste le choix le plus ordonné, puisqu'il s'agit de la passe soumise à une contrainte d'ordre d'édition
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
Vérifiez le pipeline de la même manière que la bibliothèque se vérifie elle-même : par un cycle complet (round-trip). Les tests de régression de la version 3.130 créent un document, l'enregistrent, le rechargent, exécutent l'optimisation, l'enregistrent à nouveau, puis vérifient trois points : la sortie est plus petite, les décomptes renvoyés correspondent aux attentes, et un rechargement du fichier optimisé s'analyse et s'affiche toujours correctement. Reproduire cette boucle création-optimisation-rechargement sur un échantillon de vos propres fichiers de production, et comparer le texte extrait avant et après, est un investissement d'une heure qui permet de détecter les erreurs d'intégration bien avant qu'un client n'ouvre une facture corrompue
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Où se place le pipeline dans un flux de travail de fusion ? Après la fusion, et non pendant celle-ci. Fusionner d'abord et optimiser le résultat unique signifie que chaque police intégrée est sous-ensemblement une seule fois par rapport à l'union de tous les caractères utilisés, au lieu de l'être par fichier source. Si le débit de fusion est le goulot d'étranglement, PDFlibPas propose un chemin rapide au niveau de l'octet qui évite l'analyse complète de l'objet, décrit dans l'article sur la fusion rapide de PDF avec décalage de référence d'octet ; et pour les entrées trop volumineuses pour tenir entièrement en mémoire, la section fusion et division avec accès direct pour les grands PDF présente la méthode en flux. Les deux s'associent naturellement avec une passe d'optimisation finale sur la sortie fusionnée
Ce que les trois passes ne feront pas
Le trio d'optimisation de losLab PDF Library exclut délibérément tout ce qui modifie la sémantique du document. SubsetEmbeddedFonts ne réunit pas les polices en double de sources fusionnées en un seul programme, il réduit chacune indépendamment ; la déduplication est une transformation différente et plus risquée. DownsampleImages ignorera une image dont l'estimation prudente de DPI reste inférieure au seuil, même si un humain pourrait dire qu'elle est surdimensionnée pour son cadre. Et aucune des passes ne touche à la structure du document, ainsi un fichier gonflé par des milliers d'objets orphelins nécessite une sauvegarde de type réécriture plutôt que ces passes au niveau du flux. Dans ces limites, la combinaison du sous-ensemblement de polices, du sous-échantillonnage d'images et de la normalisation LZW en Flate supprime les trois sources classiques de surcharge des PDF avec un appel d'API prévisible pour chacune. Les trois fonctions sont livrées avec losLab PDF Library pour Delphi, C# et VB.NET, aux côtés des API de fusion, d'extraction et d'affichage décrites ci-dessus