Article technique

HotPDF CompressDocument : sous-ensembles de polices compacts

THotPDF.CompressDocument de HotPDF est un interrupteur unique qui fait produire à BeginDoc le plus petit PDF sans perte que le composant sache écrire : FlateDecode au niveau maximum, un flux de références croisées avec object streams, du font subsetting et des sous-ensembles de polices compacts qui renumérotent les glyphes gardés derrière un /CIDToGIDMap explicite. EndDoc remet ensuite vos propres réglages en place. Un document de test de trois pages en Arial et SimSun est tombé de 10.2 MB à 20 KB avec un rendu identique

Que déclenche réellement CompressDocument ?

CompressDocument passe outre six réglages de l'écrivain, plus le plafond des object streams, pour un seul document et les restaure tous ensuite. À BeginDoc, avant que la version PDF ne se fige, HotPDF enregistre vos valeurs et règle Compression sur cmFlateDecode, CompressionLevel sur clMaximum, active EnableFontSubsetting et CompactFontSubsetting, et met en route UseXRefStream plus UseObjectStreams (ISO 32000-1 §7.5.7 et §7.5.8). Les object streams exigent PDF 1.5, donc une Version plus ancienne est montée à 1.5 quand elle n'est pas verrouillée. PDF/A-1 interdit les deux structures, donc un document PDF/A-1 garde sa table de références croisées classique et ne reçoit que le travail Flate et polices. Les images sont laissées exactement comme vous les avez embarquées

Schéma du cycle de vie de HotPDF CompressDocument en Delphi : BeginDoc enregistre les valeurs propres de l'écrivain, passe outre six réglages dont Compression et UseObjectStreams pour un seul document, et EndDoc rend chaque valeur empruntée dans son finally le plus externe tandis que la propriété CompressDocument elle-même reste True
Six réglages de l'écrivain et le plafond des object streams sont empruntés pour exactement un document et rendus quand EndDoc tourne, si bien qu'un rapport en échec ne laisse jamais le composant coincé en compression maximum
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // appliqué par BeginDoc, annulé par EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

La restauration se produit dans le finally le plus externe de EndDoc, donc une exception à mi-parcours d'un rapport ne laisse pas un composant de longue vie coincé en compression maximum pour le job suivant. La propriété CompressDocument elle-même reste True ; seuls les six réglages qu'elle a empruntés rentrent chez eux. La version est traitée avec plus de soin. HotPDF annule sa propre montée à 1.5 seulement si le document finit encore à 1.5, donc quand une autre fonctionnalité a poussé le fichier à 1.6 pendant l'exécution (une police OpenType embarquée, disons), la version supérieure reste, exactement comme elle l'aurait fait sans compression

Pourquoi les sous-ensembles de polices restent-ils lourds sans compaction ?

Un sous-ensemble TrueType classique jette les contours que vous ne dessinez jamais mais garde chaque glyph ID là où il était, et cette numérotation est ce qui le rend lourd. Le flux de contenu montre des CIDs égaux aux GIDs d'origine, donc le sous-ensemble doit garder un décalage loca et une entrée hmtx pour chaque emplacement jusqu'au plus haut glyphe qu'il retient, vide ou pas. Pour une police latine, cette surcharge est un bruit de fond. Pour une police CJK comme SimSun, dont les idéogrammes logent au fond d'une très grande table de glyphes, deux caractères chinois traînent des tables dimensionnées pour toute la police. Les règles de fermeture de sous-ensemble de polices pour les glyphes façonnés décident quels glyphes survivent ; la compaction, elle, porte sur ce que coûtent les survivants

CompactFontSubsetting renumérote les glyphes gardés dans une plage dense commençant à zéro et écrit un flux /CIDToGIDMap sur le CIDFont, qu'ISO 32000-1 §9.7.4.2 définit comme une table de GIDs de deux octets indexée par CID. Cette table est toute l'astuce. Les flux de contenu, le tableau de largeurs /W et la CMap ToUnicode gardent tous les CIDs d'origine, donc rien de ce qui est déjà écrit n'a à changer ; seule la recherche du CID vers le glyphe déménage dans la map. Dans le test qui a motivé la fonctionnalité, SimSun avec deux caractères est passé de 24.8 KB de données de police à 3.1 KB

Comparaison d'un sous-ensemble de police HotPDF épars, qui retient des entrées loca et hmtx pour chaque glyph ID d'origine jusqu'au GID retenu le plus haut, contre la sortie de CompactFontSubsetting qui renumérote densément les glyphes gardés depuis zéro et mappe les CIDs via un flux CIDToGIDMap tandis que les flux de contenu, /W et ToUnicode restent inchangés
La renumérotation sort le coût du programme de police pour le ranger dans un petit flux de map — deux caractères SimSun sont tombés de 24.8 KB à 3.1 KB sans toucher un octet du contenu déjà écrit

La compaction a des limites fermes, et elle se dégrade en silence plutôt que d'échouer. HotPDF bâtit des sous-ensembles compacts uniquement pour les polices Type 0 TrueType, à la fois celles réglées via SetFont avec subsetting actif et la police enregistrée via RegisterUnicodeTTF. Une police TrueType simple trouve ses glyphes par la cmap à l'intérieur du programme de police, ce que la renumérotation casserait, donc elle garde le sous-ensemble épars. Les polices OpenType-CFF n'ont pas de voie compacte non plus. Une construction compacte qui échoue retombe sur le sous-ensemble épars au lieu de lever. La propriété est désactivée par défaut, donc la sortie existante reste identique à l'octet près, tandis que sous PDF/A la police Unicode enregistrée reçoit toujours un sous-ensemble compact

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // utilisable sans CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Comment l'écrivain tassé comprime-t-il la structure du fichier ?

Une fois polices et flux petits, les dictionnaires et les données de références croisées deviennent le plus gros coût restant, donc l'écrivain d'object streams derrière CompressDocument rogne ça aussi. Le guide des object streams et mises à jour incrémentales couvre le format du conteneur lui-même ; le chemin de compression ajoute quatre raffinements par-dessus :

  • Syntaxe compacte selon ISO 32000-1 §7.2.2 : une espace n'est écrite qu'entre deux jetons qui sinon colleraient comme caractères ordinaires, donc /Type /Page devient /Type/Page
  • Les champs du flux de références croisées prennent toute largeur que §7.5.8.2 permet, donc un fichier sous 16 MB range chaque décalage sur 3 octets au lieu de 4
  • Jusqu'à 250 objets partent dans chaque object stream au lieu des 100 habituels, sauf si vous réglez votre propre plafond via ConfigureAdaptiveObjectStreamPacking
  • Quand le fichier n'est pas chiffré, le Catalog et le dictionnaire Info sont eux aussi tassés dans des object streams ; la sortie chiffrée les garde au niveau supérieur

La syntaxe compacte est venue avec un piège à connaître si vous étendez l'écrivain. La signature remplit la signature après l'écriture du fichier en cherchant dans les octets les placeholders littéraux /ByteRange ( et /Contents <, et l'orthographe compacte transformerait ça en /ByteRange( et /Contents<, que la recherche ne trouve jamais. Les dictionnaires de signature (Type Sig ou DocTimeStamp, FT Sig) et le dictionnaire de chiffrement gardent donc la mise en page espacée. Un défaut apparenté a touché les builds antérieurs à la v2.766.41 : chaque sauvegarde avec object streams, CompressDocument comprise, commençait par deux lignes d'en-tête %PDF-, donc mettez à jour si un validateur strict signale votre sortie

Peut-on compresser un PDF déjà chargé ?

Oui, via la surcharge à options CompressLoadedDocument(Options, Info), qui fait tourner les mêmes étapes sans perte sur un fichier existant. Avec THPDFLoadedDocumentCompressionOptions.Default, elle retire les ressources de pages inutilisées, fusionne polices et formulaires identiques, fait des sous-ensembles des polices embarquées avec sous-ensembles compacts actifs, recomprime les flux non filtrés, Flate, LZW, ASCII et RunLength en Flate quand le résultat est plus petit, et fait que la prochaine sauvegarde emploie des object streams. HighRatioFlate est désactivé par défaut, et les object streams sont sautés pour PDF/A-1 et les sauvegardes incrémentales. La surcharge CompressLoadedDocument sans paramètre est l'appel plus ancien et plus étroit qui se contente de compresser en Flate les flux non compressés

Flux de HotPDF CompressLoadedDocument en Delphi : l'appel retire les ressources de pages inutilisées, fusionne polices et formulaires identiques, fait des sous-ensembles des polices embarquées avec sous-ensembles compacts, recomprime les flux en Flate seulement quand le résultat est plus petit, et allume les object streams pour la prochaine sauvegarde, tandis que des champs de signature déclenchent RefusedBySignaturePolicy et laissent le fichier intact
Chaque étape réécrit des octets qu'une signature couvre, donc tout le document est refusé sauf si vous autorisez explicitement l'invalidation — Info.BytesSaved ne fait alors la somme que du travail sur ressources, polices et flux
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Deux frontières comptent sur la voie chargée. Chaque étape réécrit des octets qu'une signature couvre, donc un document porteur de champs de signature est refusé en bloc : l'appel renvoie 0, pose RefusedBySignaturePolicy et ne change rien, sauf si vous posez AllowSignatureInvalidation, après quoi Info.SignaturesInvalidated vous dit ce que vous avez abandonné. La compaction est aussi plus conservatrice ici que sur la voie de création. HotPDF ne compacte que les programmes de police employés uniquement par des polices CIDFontType2 avec une /CIDToGIDMap Identity, là où CID égale GID, et saute les programmes porteurs d'un flux de map existant, d'un /CIDSet ou de tables de glyphes couleur comme COLR, sbix, CBDT ou SVG, car la reconstruction compacte perdrait les couches de couleur. Notez aussi que Info.BytesSaved ne fait la somme que des étapes ressources, polices et flux ; le gain des object streams se voit quand le fichier est écrit

Quels résultats attendre en pratique ?

Les gains suivent la part du fichier qui est structure non compressée et données de police surdimensionnées, pas son nombre de pages. L'échantillon trois pages Arial et SimSun est passé de 10.2 MB à 20 KB quand il a été généré avec CompressDocument, et de 10.2 MB à 19.8 KB quand l'original non compressé a été chargé puis passé par CompressLoadedDocument, avec un rendu identique dans les deux cas. Un PDF déjà compact bouge à peine : dans le jeu de régression, de tels fichiers ont économisé entre -0.07% et +0.06% de leur taille d'origine. Les fichiers chargés en photos gagnent peu, car aucune des deux voies ne touche aux données d'image

Si vous générez les mêmes rapports CJK chaque nuit, mariez les sous-ensembles compacts au cache persistant de sous-ensembles de polices sur disque pour ne pas répéter le travail de subsetting à chaque exécution, et faites vos diffs de sorties compressées par contenu d'objet plutôt que par octets, car un seul champ changé re-Flate un object stream entier. Les références complètes des propriétés et des records sont sur la page produit du HotPDF Delphi PDF component