Salta ai contenuti
getnextpdf.com

L'economia della dimensione di un file PDF

Spec: ISO 32000-2, §7.5.7

Due PDF possono apparire pixel-per-pixel identici sullo schermo e differire di dieci volte su disco. La differenza non è quasi mai il contenuto che si vede; è come il file è stato assemblato sotto. Questa pagina è una panoramica di economia della dimensione: dove vanno davvero i byte di un PDF, e le quattro leve su cui un autore spende un budget di byte.

È il compagno perché i file sono grandi di Stream e filtri, che copre come un filtro decodifica. Questa resta sul budget.

La dimensione del file è raramente una metrica di vanità. È banda su ogni download, archiviazione su ogni archivio, e latenza su ogni anteprima. Una fattura da 12 MB che avrebbe dovuto essere 400 KB non è un problema cosmetico quando se ne generano un milione al mese — è un conto trenta volte più grande.

La parte frustrante è che il gonfiore è di solito invisibile. Il documento si renderizza correttamente, si apre bene, si stampa bene. Nulla dice che lo stesso artefatto avrebbe potuto essere una frazione della dimensione, perché i byte sprecati sono strutturali, non visivi. Per trovarli si deve guardare al budget, non alla pagina.

Si pensi a un PDF come a un budget che si spende su quattro voci di costo.

  • Overhead per oggetto. Ogni oggetto indiretto porta un involucro N G obj / endobj, ed è tracciato da una voce di cross-reference. Un documento con molte pagine ha migliaia di piccoli oggetti, e gli involucri si sommano. Gli object stream eliminano quell’involucro per un intero gruppo di oggetti; ogni oggetto impacchettato mantiene comunque la propria voce di cross-reference, ma come una compatta voce di type-2.
  • Byte delle immagini. Per qualsiasi documento con fotografie o scansioni, le immagini dominano, e la singola leva più grande è la scelta del filtro — un codec lossless contro un codec immagine lossy è la differenza tra megabyte e kilobyte.
  • Byte dei font. Un font incorporato completo è centinaia di kilobyte di glifi che non si usano mai. Il subsetting mantiene solo i glifi che il documento disegna effettivamente.
  • L’indice. La cross-reference table che permette a un lettore di trovare ogni oggetto può essa stessa essere uno stream compresso anziché testo in chiaro.

Si azzecchino tutte e quattro e il file è piccolo. Se ne sbaglia una e domina tutto il resto che si è fatto bene.

Lo scrittore di NextPDF è un serializzatore in streaming a passaggio singolo: accoda i byte di ciascun oggetto man mano che vengono prodotti e registra una classica voce di cross-reference in-use per ciascuno. Quel valore predefinito è veloce, prevedibile, e produce un file byte-stabile — ma non è il layout più piccolo possibile, e NextPDF è onesto su questo.

Un PDF è un grafo di oggetti indiretti. La maggior parte di essi sono piccoli dizionari: nodi di pagina, dizionari di annotation, elementi dello structure tree, voci di outline. Ciascuno paga una tassa fissa — le keyword obj / endobj, i numeri di oggetto e di generazione, e una voce di cross-reference che lo localizza. Su un documento con migliaia di piccoli oggetti, quella tassa è una fetta significativa del file.

Un object stream raccoglie molti di quei piccoli oggetti non-stream in un unico stream e li comprime insieme (Spec: ISO 32000-2, §7.5.7). L’involucro obj / endobj è eliminato per l’intero gruppo; i valori al suo interno sono memorizzati uno dietro l’altro senza keyword per oggetto, poi deflazionati come un unico blocco — il che comprime anche meglio, perché il compressore deduplicante ora vede tutti quei dizionari simili in una volta. La voce di cross-reference non sparisce — ogni oggetto impacchettato ne ha ancora bisogno — ma si riduce a una compatta voce binaria di type-2 nel cross-reference stream (più su questo nella Leva 2).

In NextPDF questo è fornito da ObjectStreamPacker, un post-processore autonomo che prende un PDF a cross-reference-stream finito e riscrive gli oggetti idonei in un unico /Type /ObjStm. Le regole che segue vengono dritte dallo standard: un oggetto idoneo è un oggetto a generazione-zero, non-stream, dopo aver escluso gli oggetti speciali che devono restare direttamente indirizzabili. La §7.5.7 vieta di memorizzare un oggetto stream dentro un object stream, quindi gli oggetti stream — contenuto, font, immagini — mantengono le proprie voci; e ObjectStreamPacker declina inoltre il cross-reference stream stesso del documento (è riscritto) e il dizionario /Encrypt, entrambi i quali devono restare direttamente indirizzabili. Tutto il resto, a generazione-zero e non-stream, è impacchettato.

Object stream packing (ObjStm) — edition availability
EditionAvailability
CorePieno supporto nel core open-source tramite ObjectStreamPacker. È opt-in: il valore predefinito dello scrittore a passaggio singolo emette classiche voci in-use, quindi l’output resta byte-identico a meno che non si abiliti il packing. Il packer è deterministico, con la propria baseline golden riproducibile.
ProNot in this edition
EnterpriseNot in this edition

L’opt-in è una postura deliberata, non una limitazione che si nasconde. L’output predefinito è byte-stabile e coincide con le baseline golden esistenti; attivare il packing è un’opt-in a un layout diverso, più piccolo, ugualmente deterministico. Si sceglie il compromesso, e il motore non lo fa mai alle tue spalle.

Leva 2 — anche l’indice può essere uno stream

Sezione intitolata “Leva 2 — anche l’indice può essere uno stream”

Una volta che gli oggetti vivono dentro un object stream, l’indice che punta ad essi cambia forma. Un lettore trova un oggetto impacchettato attraverso una voce di cross-reference compressa — una voce di type-2 che nomina l’object stream e l’indice al suo interno (Spec: ISO 32000-2, §7.5.8.3). Poiché l’intera cross-reference è essa stessa uno stream /Type /XRef, l’indice per migliaia di oggetti è impacchettato in binario e deflazionato anziché scritto come righe di testo in chiaro. La mappa si riduce assieme al territorio.

ObjectStreamPacker ricostruisce esattamente questo: emette il singolo object stream, poi un cross-reference stream riscritto che porta una compatta voce di type-1 per ogni oggetto trattenuto e una voce di type-2 per ogni oggetto impacchettato, preservando ogni numero di oggetto così che i riferimenti esistenti restino validi.

Per qualsiasi documento con immagini reali, questa leva fa impallidire le altre. I byte sono la stessa immagine; il codec è il budget. I nomi dei filtri vivono nel set di filtri standard (Spec: ISO 32000-2, §7.4), e la scelta tra di essi è una decisione di dimensione:

  • FlateDecode è lossless. Perfetto per line art, screenshot, e qualsiasi cosa con colore piatto — e rovinoso per una fotografia, dove lossless significa ogni byte dell’originale.
  • DCTDecode è JPEG: lossy, e per le fotografie la scelta giusta con ampio margine, spesso una riduzione di dieci volte per un calo di qualità che nessuno nota.
  • JPXDecode è JPEG 2000: compressione wavelet con una diversa curva qualità/dimensione, ma supporto disomogeneo e non permesso da alcuni profili di archiviazione.

Questo è esattamente dove come un filtro decodifica conta, e quello è il compito dell’articolo Stream e filtri. Il punto economico è più ristretto: una fotografia memorizzata lossless è la singola causa più comune di un PDF inutilmente enorme, e nessuna quantità di object-stream packing salverà un file il cui peso reale è una scansione non-JPEGgizzata.

Un font incorporato è un programma. Uno completo può essere parecchie centinaia di kilobyte, perché porta ogni glifo che il progettista del carattere abbia mai disegnato — migliaia di caratteri attraverso scritture che non si useranno mai in questo documento. Il subsetting incorpora solo i glifi che il documento disegna effettivamente, trasformando quel programma in una piccola frazione di sé stesso. Una lettera di una pagina non ha bisogno dell’intero di un font con capacità CJK; ha bisogno delle poche dozzine di glifi che imposta. La meccanica di come i glifi sono selezionati e re-indicizzati è un argomento a sé — si veda Font: la parte difficile.

  1. Image bytesFor media-heavy files, the largest line item. The filter choice — lossless Flate vs a lossy image codec like DCT (or JPX in a lossy mode) — is the dominant size lever.
  2. Font bytesA full embedded font is mostly glyphs you never draw. Subsetting keeps only the used glyphs, often shrinking the program by an order of magnitude.
  3. Per-object overheadThousands of small dictionaries each pay an obj/endobj envelope plus a cross-reference entry. Object streams (ObjStm) drop the envelope for the whole group; each object keeps a compact type-2 cross-reference entry.
  4. The indexA compressed cross-reference stream with type-2 entries binary-packs and deflates the map of every object, replacing plaintext index rows.
Where the bytes go: the four levers an author spends a PDF's size budget on, in the order they typically dominate a real file.

Non c’è alcuna API fabbricata da mostrare qui, perché le decisioni di dimensione più importanti sono prese prima che i byte raggiungano lo scrittore — e l’unica leva strutturale che NextPDF espone è un singolo opt-in. Concettualmente, il budget si legge così:

  • Si forniscano le fotografie come JPEG così che finiscano sotto DCTDecode, non ricodificate senza perdita. La vittoria più grande è una scelta sui dati sorgente, non un flag dello scrittore.
  • Si lasci che lo scrittore faccia il subset dei font incorporati così che solo i glifi disegnati vengano spediti.
  • Per un documento con molti piccoli oggetti, si faccia opt-in all’object-stream packing, che instrada il file finito attraverso ObjectStreamPacker per raggruppare i piccoli oggetti non-stream e riscrivere il cross-reference stream.

Il percorso predefinito — classiche voci in-use, nessun ObjStm — è la baseline giusta: deterministica, byte-stabile, e facile da verificare. Il packing è l’aggiornamento ponderato quando è il numero di oggetti, non il peso delle immagini, ciò che gonfia il file.

La trappola è ricorrere a un pulsante «comprimi il PDF» e aspettarsi che risolva tutto. La compressione non è una leva; sono quattro, e non si sostituiscono l’una all’altra. L’object-stream packing non può ridurre una fotografia — quello è il compito del filtro immagine. Un JPEG perfetto non può compensare un font che si è dimenticato di fare il subset. E nulla di tutto ciò aiuta se il gonfiore reale è una scansione da 10 MB memorizzata senza perdita perché nessuno ha scelto DCTDecode per essa.

Il secondo equivoco è che più piccolo sia sempre strettamente meglio. Non lo è. Gli object stream sono incompatibili con la linearizzazione — il layout fast-web-view che fissa la collocazione assoluta degli oggetti così che la prima pagina arrivi in streaming presto. E alcuni profili di archiviazione limitano quali filtri immagine siano persino permessi. La dimensione è un asse. Si scambia con lo streaming, la conformità di archiviazione, e la riproducibilità, e il punto giusto sulla curva dipende da a cosa serve il documento.

Le quattro leve sono l’economia strutturale della dimensione del file. Non sono una garanzia universale di «rendilo più piccolo», e NextPDF non pretende di essere un ri-ottimizzatore per PDF in ingresso arbitrari.

ObjectStreamPacker è un’ottimizzazione best-effort che non rischia mai la correttezza. Declina — restituendo l’input invariato — quando il file non è un PDF a cross-reference-stream, quando è cifrato, quando porta una firma digitale (ri-disporre gli oggetti sposterebbe i byte range che una firma protegge), quando contiene già object stream, o quando non c’è alcun oggetto idoneo da impacchettare. Emette esattamente un object stream per l’intero documento; non si divide in una collezione /Extends, che è fuori ambito e conforme per le dimensioni di documento che NextPDF produce.

Le leve delle immagini e dei font sono in gran parte decisioni sull’input. Lo scrittore di NextPDF non transcodifica implicitamente una bitmap lossless in uno stream immagine lossy — ricodificare una fotografia in DCTDecode o JPXDecode è una decisione esplicita di codifica immagine a monte, non qualcosa che il serializzatore fa alle tue spalle — né recupera glifi da un font che il chiamante ha chiesto di incorporare per intero. Le vittorie di dimensione più grandi sono fatte a monte del serializzatore di byte; il compito del motore è non sprecare il budget che gli porti.

  • Stream e filtri — il compagno come un filtro decodifica; questa pagina lo complementa deliberatamente, non lo duplica.
  • Cosa è davvero un PDF — il modello a oggetti indiretti il cui overhead per oggetto la leva degli object stream riduce.
  • L’anatomia di un file PDF — la struttura di cross-reference che diventa uno stream compresso.
  • Font: la parte difficile — come il subsetting seleziona e re-indicizza i glifi che un documento disegna.
  • Object stream (ObjStm) — un unico stream che contiene molti piccoli oggetti indiretti non-stream, compressi insieme, così che l’involucro obj / endobj sia eliminato per il gruppo. Ogni oggetto impacchettato mantiene comunque la propria voce di cross-reference, come una compatta voce di type-2. Opt-in in NextPDF.
  • Oggetto indiretto — un oggetto numerato nel grafo di un PDF, avvolto in un involucro obj / endobj e tracciato dall’indice di cross-reference. L’involucro è l’overhead per oggetto.
  • Cross-reference stream — lo stream /Type /XRef che indicizza ogni oggetto, impacchettato in binario e deflazionato anziché scritto come righe di testo in chiaro.
  • Voce compressa (type-2) — una voce di cross-reference che punta a un oggetto che vive dentro un object stream, nominando lo stream e l’indice al suo interno.
  • Subsetting dei font — incorporare solo i glifi che un documento disegna effettivamente, anziché l’intero carattere, riducendo il programma del font incorporato.
  • Filtro lossless contro lossy — un codec lossless (FlateDecode) riproduce ogni byte; un codec immagine lossy (DCTDecode, o JPXDecode in modalità lossy) scarta dettaglio impercettibile per un risultato molto più piccolo. (JPEG 2000 / JPX possono anche essere configurati lossless.) La scelta è la leva dominante su un file ricco di media.