Pour découvrir où va réellement la taille d un fichier PDF, losLab PDF Library expose AuditDocumentSpace, qui classe chaque objet indirect dans douze catégories — images, programmes de police, dictionnaires de police, flux de contenu, XObjects de formulaire, flux d objets, fichiers intégrés, métadonnées, arbre de structure, annotations, arbre de pages, autre — et rapporte le nombre d objets, les octets stockés et la part en pourcentage de chacune
La situation pour laquelle cela existe est familière. Un rapport de 40 pages sort de votre générateur à 80 Mo, le client demande pourquoi, et tout ce que vous pouvez offrir est une supposition. Probablement les images. Peut-être les polices. Alors vous activez le sous-échantillonnage, vous livrez, et le fichier atterrit à 74 Mo parce que le vrai poids se trouvait ailleurs. Notre article compagnon sur le sous-ensemble de polices et le sous-échantillonnage d images couvre comment réduire un PDF ; celui-ci couvre l étape qui devrait venir en premier, à savoir mesurer ce que vous êtes sur le point de réduire
Pourquoi mesurer avant de compresser ?
Parce que les trois passes d optimisation standard ont des rendements radicalement différents sur un fichier donné, et rien dans le fichier ne vous dit laquelle s applique tant que vous n avez pas compté. Sous-ensembler les polices d un document dont les polices représentent déjà 2 % de ses octets, c est passer un après-midi à déplacer une erreur d arrondi. Sous-échantillonner les images d un fichier dont l essentiel est constitué de flux de contenu non compressés produit la même déception. L optimiseur n est pas la partie difficile — chaque bibliothèque en a un. Savoir quel optimiseur pointer vers ce fichier est la partie difficile, et c est une question de comptabilité, pas une question de compression. Un audit détecte aussi les cas où aucun optimiseur n est la réponse : un fichier qui s avère être composé à 60 % de pièces jointes intégrées n a pas besoin d une meilleure compression, il a besoin d une conversation sur le fait de savoir si ces pièces jointes appartiennent au document, et un fichier qui est à 30 % arbre de structure paie pour un balisage d accessibilité, ce qui est généralement un coût délibéré que vous ne devriez pas retirer silencieusement. Une fois les octets attribués, vous prenez une décision produit avec des chiffres à l appui plutôt que d actionner l interrupteur le plus proche
Ce que contient le rapport en douze catégories
AuditDocumentSpace renvoie un handle de liste de chaînes plutôt qu un enregistrement, de sorte que le rapport traverse inchangé les façades DLL plate et COM. La liste contient une ligne de résumé Total,Objects,Bytes,100.0 suivie d exactement douze lignes Category,Objects,Bytes,Percent dans un ordre fixe qui fait partie du contrat : Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Treize lignes, toujours, même quand une catégorie est vide
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
Un détail Delphi dans cette boucle vous mordra exactement une fois. GetStringListItem utilise des index d élément basés sur 1, correspondant à GetStringListCount, et un index hors plage renvoie une chaîne vide plutôt que de lever une exception. Écrivez la boucle for I := 0 to Count - 1 par habitude et vous obtenez une première ligne vide, une dernière ligne discrètement omise, et aucune exception nulle part pour vous dire que l indexation est erronée. Le rapport lui-même paraîtra presque correct, ce qui est le pire mode de défaillance qu un outil de diagnostic puisse avoir
Pourquoi l audit utilise-t-il la longueur stockée plutôt que la taille décodée ?
Parce que la longueur stockée est à la fois le chiffre que vous voulez et le chiffre bon marché à obtenir. Chaque objet indirect porte TPDFIndObj.FLength, la longueur brute en octets que l objet occupe dans le fichier tel qu analysé. L utiliser signifie qu une image DCTDecode de 900 Ko est rapportée comme 900 Ko — les octets qu elle vous coûte sur le disque — plutôt que les 40 Mo d échantillons RVB qu elle décode. Cela signifie aussi que l audit n a jamais besoin de décoder quoi que ce soit : les objets chargés paresseusement restent paresseux, les filtres restent inexécutés, et auditer un fichier de 500 Mo est une passe sur des en-têtes d objet plutôt qu un cycle de décompression complet
La seconde règle est une défense contre le double comptage. Quand un objet vit à l intérieur d un flux d objets compressé, indiqué par un FObjStrNum non nul, son nombre d octets est enregistré comme zéro. Son stockage a déjà été payé une fois par le flux conteneur, que la norme ISO 32000-1 §7.5.7 définit comme un flux /Type /ObjStm contenant de nombreux objets dans une seule charge utile compressée Flate. Facturer à chaque membre sa propre part puis facturer à nouveau le conteneur gonflerait le total au-delà de la taille réelle du fichier. Cela a une conséquence directe sur la façon de lire le résultat, couverte plus bas et plus en détail dans notre article sur les flux d objets et les flux de table de références croisées
Pourquoi un programme de police ne peut-il pas se classer lui-même ?
Parce qu un fichier de police TrueType intégré dans un PDF n a aucun marqueur le signalant. La norme ISO 32000-1 §9.8.1 définit le programme de police intégré comme la valeur de /FontFile, /FontFile2 ou /FontFile3 dans un descripteur de police, et le dictionnaire de flux à l autre bout de cette référence porte /Length1 et des clés de filtre mais aucun /Type et aucun /Subtype qui l identifie comme une police. Vu isolément, c est un flux binaire anonyme. Seul le descripteur qui pointe vers lui sait ce que c est. La même asymétrie apparaît pour les annotations : le §12.5.2 rend /Type /Annot optionnel dans un dictionnaire d annotation, donc le signal fiable est l appartenance à un tableau /Annots de page, pas le dictionnaire lui-même
La classification s exécute donc deux fois. La première passe lit le /Type et le /Subtype propres de chaque objet et prend les victoires faciles : /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font et /Type /FontDescriptor, /Metadata, /EmbeddedFile et /Filespec, /StructTreeRoot et /StructElem, /Annot, /Page et /Pages. Tout le reste atterrit provisoirement dans Other. La seconde passe parcourt ensuite le côté référençant et remplace : chaque dictionnaire de page réassigne son /Contents aux flux de contenu, ses entrées /Annots aux annotations, et son /Thumb aux images, tandis que chaque dictionnaire de police parcourt sa propre chaîne de descripteur
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
Lire le rapport et choisir le prochain geste
Lisez d abord les parts, ensuite les nombres d objets, et traitez tout grand écart entre eux comme un signal. Un PDF moderne place la plupart de ses petits dictionnaires à l intérieur de flux d objets, donc Page tree et Structure tree affichent régulièrement des dizaines d objets pour presque zéro octet — leur coût réel a été replié dans la ligne Object streams. Si Object streams est elle-même importante, le fichier est dense en structure de type métadonnées plutôt qu en contenu, et le levier est l élagage d objets, pas leur compression. Les flux d apparence des annotations se comportent de façon similaire : ils portent /Subtype /Form, donc un document lourdement tamponné affiche son poids sous Form XObjects tandis que la ligne Annotations reste faible
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
Deux faits de formatage comptent si vous analysez les pourcentages plutôt que de les afficher. Le séparateur décimal est toujours un point littéral quel que soit le paramètre régional de la machine, donc analyser avec le FormatSettings ambiant sur un poste allemand ou français échouera ou, pire, mal lira. Et les zéros de fin sont éliminés, donc une catégorie représentant exactement 40 % des octets s affiche 40, pas 40.0 — ne supposez jamais un nombre fixe de décimales. Une fois la part en main, l aiguillage est mécanique : une part Images dominante pointe vers DownsampleImages, une part Font programs dominante vers SubsetEmbeddedFonts, et des Content streams volumineux vers CompressContent
Ce que l audit ne vous dit délibérément pas
Le total est une somme sur les objets indirects, et un fichier PDF est légèrement plus que ses objets. L en-tête du fichier, le trailer, les espaces inter-objets et une table de références croisées classique ne sont pas des objets indirects, donc ces octets ne sont attribués à rien et le total de l audit atterrit un peu en dessous de la taille sur disque. Un flux de références croisées est différent — c est un véritable objet avec /Type /XRef, donc dans un fichier moderne ces octets apparaissent bien, dans la catégorie Other. Aucun des deux comportements n est un défaut, mais si vous réconciliez l audit avec un décompte d octets provenant du système de fichiers, c est de là que vient l écart
Deux autres limites méritent d être énoncées clairement. D abord, les chiffres décrivent un fichier qui a été chargé, pas un fichier en cours de rédaction : pour les objets construits en mémoire qui n ont pas encore de longueur stockée, la taille se rabat sur la sortie sérialisée avec une allocation nominale pour le dictionnaire de flux, ce qui est une estimation de l écriture éventuelle plutôt qu une mesure. Faites l audit après un enregistrement suivi d un rechargement si vous voulez des chiffres exacts. Ensuite, une ligne Other épaisse est une découverte, pas un rapport de bogue — elle signifie généralement des objets orphelins que plus rien ne référence, ce qui relève du ramasse-miettes marquer-et-balayer plutôt que d une quelconque passe de compression
Utilisé ainsi, l audit change la nature de la conversation. Au lieu de deviner à propos du rapport de 80 Mo, vous l ouvrez, exécutez un seul appel, et lisez que les images représentent 8 %, les programmes de police 61 %, et que le document intègre neuf programmes de police complets pour une charte maison qui utilise trois graisses. C est une réponse corrigible accompagnée d un chiffre. AuditDocumentSpace, avec les passes d optimisation vers lesquelles il vous oriente, est livré dans losLab PDF Library pour Delphi et C++Builder, où les pages de référence documentent la liste complète des catégories et l API de liste de chaînes qui l entoure