Per scoprire dove finisca realmente la dimensione di un file PDF, losLab PDF Library espone AuditDocumentSpace, che classifica ogni oggetto indiretto in dodici categorie — immagini, programmi font, dizionari font, content stream, form XObject, object stream, file incorporati, metadata, albero di struttura, annotazioni, albero delle pagine, altro — e riporta il conteggio degli oggetti, i byte memorizzati e la quota percentuale di ciascuna
La situazione per cui esiste è familiare. Un report di 40 pagine esce dal tuo generatore a 80 MB, il cliente chiede perché, e tutto ciò che puoi offrire è un'ipotesi. Probabilmente le immagini. Forse i font. Così attivi il downsampling, spedisci, e il file arriva a 74 MB perché il peso reale stava altrove. Il nostro articolo gemello su subsetting dei font e downsampling delle immagini tratta come ridurre un PDF; questo tratta il passo che dovrebbe venire prima, cioè misurare cosa stai per ridurre
Perché misurare prima di comprimere?
Perché i tre passaggi di ottimizzazione standard hanno rese estremamente diverse su un dato file, e niente nel file ti dice quale si applica finché non conti. Fare subsetting dei font su un documento i cui font sono già il 2% dei suoi byte è un pomeriggio speso a spostare un errore di arrotondamento. Il downsampling delle immagini in un file la cui massa è content stream non compressi produce la stessa delusione. L'ottimizzatore non è la parte difficile — ogni libreria ne ha uno. Sapere quale ottimizzatore puntare su questo file è la parte difficile, ed è una questione di contabilità, non di compressione. Un audit intercetta anche i casi in cui nessun ottimizzatore è la risposta: un file che risulta essere per il 60% allegati incorporati non ha bisogno di compressione migliore, ha bisogno di una conversazione sul fatto che quegli allegati appartengano al documento, e un file che è per il 30% albero di struttura sta pagando per il tagging di accessibilità, che di solito è un costo deliberato che non dovresti rimuovere silenziosamente. Una volta attribuiti i byte, prendi una decisione di prodotto con numeri alla base invece di afferrare l'interruttore più vicino
Cosa contiene il report a dodici categorie
AuditDocumentSpace restituisce un handle a string list piuttosto che un record, così il report sopravvive invariato attraverso le facciate flat DLL e COM. La lista contiene una riga di riepilogo Total,Objects,Bytes,100.0 seguita da esattamente dodici righe Category,Objects,Bytes,Percent in un ordine fisso che fa parte del contratto: Images, Font programs, Font dictionaries, Content streams, Form XObjects, Object streams, Embedded files, Metadata, Structure tree, Annotations, Page tree, Other. Tredici righe, sempre, anche quando una categoria è vuota
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 dettaglio Delphi in quel ciclo ti morderà esattamente una volta. GetStringListItem usa indici di elemento a base uno, in linea con GetStringListCount, e un indice fuori intervallo restituisce una stringa vuota invece di sollevare un'eccezione. Scrivi il ciclo for I := 0 to Count - 1 per abitudine e ottieni una prima riga vuota, un'ultima riga silenziosamente scartata, e nessuna eccezione da nessuna parte a dirti che l'indicizzazione è sbagliata. Il report stesso sembrerà quasi corretto, che è la peggiore modalità di fallimento che uno strumento diagnostico possa avere
Perché l'audit usa la lunghezza memorizzata invece della dimensione decodificata?
Perché la lunghezza memorizzata è sia il numero che vuoi sia il numero economico da ottenere. Ogni oggetto indiretto porta TPDFIndObj.FLength, la lunghezza in byte grezza che l'oggetto occupa nel file così come analizzato. Usarla significa che un'immagine DCTDecode da 900 KB viene riportata come 900 KB — i byte che ti costa su disco — invece dei 40 MB di campioni RGB a cui si decodifica. Significa anche che l'audit non deve mai decodificare nulla: gli oggetti caricati lazily restano lazy, i filtri restano non eseguiti, e sottoporre ad audit un file da 500 MB è una passata sugli header degli oggetti piuttosto che un ciclo completo di decompressione
La seconda regola è una difesa contro il doppio conteggio. Quando un oggetto vive dentro un object stream compresso, indicato da un FObjStrNum diverso da zero, il suo conteggio di byte viene registrato come zero. La sua memorizzazione è già stata pagata una volta dallo stream contenitore, che ISO 32000-1 §7.5.7 definisce come uno stream /Type /ObjStm che contiene molti oggetti in un unico payload compresso Flate. Addebitare a ogni membro la propria quota e poi addebitare di nuovo il contenitore gonfierebbe il totale oltre la dimensione reale del file. Questo ha una conseguenza diretta su come leggere l'output, trattata più avanti e in modo più approfondito nel nostro articolo su object stream e cross-reference stream
Perché un programma font non può classificare sé stesso?
Perché un file font TrueType incorporato in un PDF non ha alcun marcatore che lo dichiari. ISO 32000-1 §9.8.1 definisce il programma font incorporato come il valore di /FontFile, /FontFile2 o /FontFile3 in un descrittore di font, e il dizionario dello stream all'altro capo di quel riferimento porta /Length1 e chiavi di filtro ma nessun /Type e nessun /Subtype che lo identifichi come font. Guardato isolatamente è uno stream binario anonimo. Solo il descrittore che vi punta sa cosa sia. La stessa asimmetria appare per le annotazioni: §12.5.2 rende /Type /Annot opzionale in un dizionario di annotazione, quindi il segnale affidabile è l'appartenenza a un array /Annots di pagina, non il dizionario stesso
Quindi la classificazione avviene due volte. La prima passata legge il proprio /Type e /Subtype di ogni oggetto e prende le vittorie facili: /ObjStm, /Subtype /Image, /Subtype /Form, /Type /Font e /Type /FontDescriptor, /Metadata, /EmbeddedFile e /Filespec, /StructTreeRoot e /StructElem, /Annot, /Page e /Pages. Tutto il resto atterra provvisoriamente in Other. La seconda passata poi percorre il lato dei riferimenti e sovrascrive: ogni dizionario di pagina riassegna il proprio /Contents ai content stream, le proprie voci /Annots alle annotazioni, e il proprio /Thumb alle immagini, mentre ogni dizionario font percorre la propria catena di descrittori
// 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]));
Leggere il report e scegliere la prossima mossa
Leggi prima le quote, poi i conteggi degli oggetti, e tratta qualsiasi ampio divario tra loro come un segnale. Un PDF moderno mette la maggior parte dei suoi piccoli dizionari dentro object stream, quindi Page tree e Structure tree mostrano abitualmente decine di oggetti contro quasi zero byte — il loro costo reale è stato ripiegato nella riga Object streams. Se Object streams è a sua volta grande, il file è denso di struttura simile a metadata piuttosto che di contenuto, e la leva è potare oggetti, non comprimerli. Gli appearance stream delle annotazioni si comportano in modo simile: portano /Subtype /Form, quindi un documento pesantemente timbrato mostra il suo peso sotto Form XObjects mentre la riga Annotations resta piccola
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;
Due fatti di formattazione contano se analizzi le percentuali invece di limitarti a mostrarle. Il separatore decimale è sempre un punto letterale a prescindere dal locale della macchina, quindi analizzarlo con il FormatSettings ambientale su una workstation tedesca o francese fallirà o, peggio, leggerà male. E gli zeri finali sono tagliati, quindi una categoria che detiene esattamente il 40% dei byte stampa 40, non 40.0 — non dare mai per scontato un numero fisso di decimali. Con la quota in mano, l'instradamento è meccanico: una quota Images dominante punta a DownsampleImages, una quota Font programs dominante a SubsetEmbeddedFonts, e Content streams voluminosi a CompressContent
Cosa l'audit deliberatamente non ti dice
Il totale è una somma sugli oggetti indiretti, e un file PDF è leggermente più dei suoi oggetti. L'header del file, il trailer, gli spazi bianchi tra oggetti e una tabella cross-reference classica non sono oggetti indiretti, quindi quei byte non vengono attribuiti a nulla e il totale dell'audit atterra un po' sotto la dimensione su disco. Uno stream cross-reference è diverso — è un vero oggetto con /Type /XRef, quindi in un file moderno quei byte appaiono, nella categoria Other. Nessuno dei due comportamenti è un difetto, ma se stai riconciliando l'audit con un conteggio di byte dal file system, è da lì che arriva il divario
Vale la pena affermare chiaramente altri due confini. Primo, i numeri descrivono un file che è stato caricato, non uno in fase di creazione: per gli oggetti costruiti in memoria che non hanno ancora una lunghezza memorizzata, la dimensione ricade sull'output serializzato con un margine nominale per il dizionario dello stream, che è una stima della scrittura eventuale piuttosto che una misurazione. Fai l'audit dopo un salva-e-ricarica se vuoi cifre esatte. Secondo, una riga Other grassa è un riscontro, non una segnalazione di bug — di solito significa oggetti orfani a cui nulla fa più riferimento, il che è un lavoro per garbage collection mark-and-sweep piuttosto che per qualsiasi passaggio di compressione
Usato così, l'audit cambia la forma della conversazione. Invece di tirare a indovinare sul report da 80 MB lo apri, esegui una chiamata, e leggi che le immagini sono l'8%, i programmi font sono il 61%, e il documento incorpora nove programmi font completi per uno stile aziendale che usa tre font. Questa è una risposta risolvibile con un numero attaccato. AuditDocumentSpace, insieme ai passaggi di ottimizzazione a cui ti indirizza, è distribuito nella losLab PDF Library per Delphi e C++Builder, dove le pagine di riferimento documentano l'elenco completo delle categorie e l'API string-list attorno ad esso