Salta ai contenuti

Ridurre le dimensioni dei PDF con compressione e subsetting

L’obiettivo è ottenere il PDF più piccolo consentito dal contenuto, senza alcuna perdita di fedeltà. NextPDF mette a disposizione due leve sulle dimensioni, entrambe attive per impostazione predefinita:

  • Compressione dei flussi. Il writer racchiude ogni flusso di contenuto di pagina e ogni programma di font incorporato in un flusso FlateDecode (zlib). NextPDF\Core\Config espone il flag compress, che controlla questa impostazione. È possibile modificarlo con il wither withCompress() durante la creazione di un documento in streaming.
  • Subsetting dei font. Quando si incorpora un font TrueType o CFF, il writer ricostruisce il programma di font in modo che contenga solo i glifi effettivamente utilizzati dal documento, quindi comprime il risultato con FlateDecode. Questo avviene automaticamente: non c’è alcun flag da impostare né alcuna chiamata da effettuare. Se un tipo di carattere CJK da 20,000 glifi contribuisce a un documento solo con poche centinaia di glifi, viene incorporato a una frazione delle sue dimensioni su disco.

È importante essere espliciti fin dall’inizio: NextPDF Core non espone il ricampionamento delle immagini, un controllo della qualità delle immagini, un’opzione per i flussi di oggetti né un’impostazione di deduplicazione delle risorse. Gli unici controlli sulle dimensioni disponibili sono i due descritti sopra. Il resto di questa ricetta mostra come utilizzarli correttamente e che cosa ciascuno di essi non fa.

Prerequisiti: un’installazione di Core (composer require nextpdf/core:^3) e, per usare il percorso di subsetting, un file di font che si è autorizzati a incorporare.

Terminal window
composer require nextpdf/core:^3

Un PDF è un albero di oggetti. Gli oggetti più grandi sono di solito i flussi di contenuto (gli operatori di disegno di ciascuna pagina) e i programmi di font (i contorni dei glifi incorporati). Entrambi sono dati testuali e binari altamente comprimibili, perciò la leva più efficace per ridurre le dimensioni è comprimerli con FlateDecode. FlateDecode è il nome PDF 2.0 di un flusso DEFLATE racchiuso in zlib (ISO 32000-2:2020 §7.4.4) ed è il filtro generato da NextPDF.

Il writer imposta il livello di compressione DEFLATE a 9, il massimo previsto da RFC 1951, tramite NextPDF\Writer\PinnedZlibCompressor. Il livello 9 richiede un po’ più di CPU in cambio del flusso più piccolo possibile. Mantenerlo fisso rende inoltre deterministico l’output, perché l’header zlib codifica il livello e un livello variabile cambierebbe i byte. Il livello non è configurabile dall’utente: il motore lo fissa in modo che due esecuzioni sullo stesso input producano flussi identici byte per byte.

La seconda leva è il subsetting dei font. Un file di font su disco contiene ogni glifo definito dal tipo di carattere, ma un documento che stampa «Invoice 2026» ne richiede solo una dozzina. NextPDF\Typography\FontSubsetter (per TrueType) e NextPDF\Typography\CffSubsetter (per CFF / OpenType) percorrono i codepoint effettivamente renderizzati dal documento, risolvono le dipendenze dei glifi compositi e ricostruiscono solo le tabelle di font necessarie. Generano un binario valido di font subset con un tag di prefisso subset deterministico di sei lettere (ISO 32000-2:2020 §9.9). Il writer applica questa operazione ogni volta che è noto l’insieme di glifi utilizzati da un font incorporato, quindi comprime il subset con FlateDecode. Se il subsetting di un determinato tipo di carattere consentisse di risparmiare meno del dieci percento, il subsetter restituisce invece il programma originale, perché l’overhead della ricostruzione non giustifica un guadagno marginale.

In sintesi: i PDF restano compatti lasciando attiva la compressione (l’impostazione predefinita) e incorporando file di font reali (così che il subsetting abbia qualcosa da rimpicciolire), non regolando un lungo elenco di opzioni.

L’unico controllo configurabile sulle dimensioni si trova nell’oggetto di configurazione.

NextPDF\Core\Config è un value object immutabile final readonly con metodi wither tipizzati. Il membro rilevante è:

  • compress (bool, predefinito true) — abilita la compressione FlateDecode. È possibile modificarlo con withCompress(bool $compress): self, che restituisce un nuovo Config con il flag modificato e ogni altro campo invariato.

Associare un Config a un documento al momento della costruzione:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self crea un documento con registri effimeri per uno script CLI o un processo di breve durata, applicando il Config fornito.

Due membri determinano l’ambito in cui operano le leve sulle dimensioni, ma nessuno dei due è di per sé un controllo di compressione:

  • imageCacheBytes (int, predefinito 52_428_800) limita la cache delle immagini in memoria e withImageCacheBytes(int $bytes): self la modifica. Limita il picco di memoria durante una build. Non ricampiona, non ricomprime e non riduce in altro modo le immagini incorporate: è un limite massimo di memoria, non un controllo sulle dimensioni dell’output.
  • fontsDirectory (string) e withFontsDirectory(string $dir): self impostano il percorso di ricerca predefinito per i file di font, usato dal percorso di subsetting.

Le operazioni sui font avvengono attraverso la superficie tipografica del documento:

  • setFont(string $family, string $style = '', float $size = 12.0): static seleziona un tipo di carattere. Quando la famiglia viene risolta in un file di font incorporabile, il writer registra i codepoint renderizzati così da poter eseguire il subsetting di quel tipo di carattere al momento del salvataggio.
  • addFontDirectory(string $directory): static registra una directory aggiuntiva in cui cercare i file di font.

L’output è il consueto trio: getPdfData(): string restituisce i byte, save(string $path): void li scrive in modo atomico e output(?string $filename, OutputDestination $dest): string gestisce la consegna via HTTP.

Il subsetting non dispone di alcun metodo pubblico né di alcun flag. È un effetto dell’incorporamento di un font e del rendering del testo: il writer richiama automaticamente FontSubsetter / CffSubsetter all’interno di NextPDF\Writer\PdfFontWriter.

Questo esempio crea un documento con la compressione esplicitamente abilitata e un font incorporato e sottoposto a subsetting, quindi scrive i byte. Omette la gestione degli errori per mostrare la struttura delle chiamate; l’esempio di produzione più avanti aggiunge tutte le protezioni.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.
$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);
$doc->addFontDirectory(__DIR__ . '/fonts');
$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer
// subsets this font automatically when the document is built.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));

Si tratta di un programma autonomo. Crea un documento con la compressione attiva, incorpora un font da una directory sotto controllo del server, esegue il rendering del testo affinché il subsetter disponga di un insieme di glifi utilizzati e scrive il risultato in modo atomico. Intercetta le eccezioni NextPDF più specifiche generate dai percorsi di creazione e salvataggio, quindi rilancia ciascuna con contesto anziché ignorarla. Impostare NEXTPDF_FONT_DIR su una directory contenente un tipo di carattere TrueType o CFF che si è autorizzati a incorporare; il programma convalida il percorso prima di procedere all’incorporamento.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
/**
* Resolve and validate the font directory from a server-controlled source.
*
* Reading the directory from the environment keeps the path off the request
* surface. The function rejects a missing or unreadable directory so the
* embedding path never runs against untrusted or absent input.
*/
function resolveFontDirectory(): string
{
$configured = getenv('NEXTPDF_FONT_DIR');
$dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir);
if ($real === false || !is_dir($real) || !is_readable($real)) {
throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir));
}
return $real;
}
/**
* Build a compressed, font-subsetted document and return its bytes.
*
* @param non-empty-string $fontDirectory Validated directory of embeddable fonts.
*
* @return string Raw PDF bytes.
*/
function buildCompactPdf(string $fontDirectory): string
{
// compress is true by default; pin it so the intent is explicit and the
// streaming writer path honours it regardless of any wrapper defaults.
$config = (new Config())
->withCompress(true)
->withFontsDirectory($fontDirectory)
// Bound the image cache so a build cannot exhaust memory. This is a
// memory ceiling, not an output-size control.
->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config);
$doc->addFontDirectory($fontDirectory);
$doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the
// writer turns into a font subset at build time.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026', newLine: true);
$doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program
// are FlateDecode-compressed before the bytes are returned.
return $doc->getPdfData();
}
try {
$fontDirectory = resolveFontDirectory();
$pdf = buildCompactPdf($fontDirectory);
} catch (CompressionException $e) {
// Raised if the zlib encoder hard-fails while compressing a stream.
throw new RuntimeException(
sprintf('Compression failed for a %s stream.', $e->getAlgorithm()),
previous: $e,
);
} catch (InvalidConfigException $e) {
// Raised by the output path for an invalid destination configuration.
throw new RuntimeException(
sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()),
previous: $e,
);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) {
throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));
}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);

STDOUT previsto (il numero di byte dipende dal font e dalla build):

Wrote <n> bytes to <path>.
  • La compressione è attiva per impostazione predefinita. Un Config appena creato ha compress impostato su true. Raramente è necessario ricorrere a withCompress(). Impostarlo esplicitamente solo per documentare l’intento, oppure per disattivarlo in una build di debug in cui si desidera leggere i flussi grezzi.
  • Disattivare la compressione rende i file più grandi, non più piccoli. withCompress(false) è uno strumento diagnostico per ispezionare i flussi non compressi. Non è mai un’ottimizzazione delle dimensioni. Distribuire con la compressione attiva.
  • Il subsetting richiede un font reale incorporato. I font standard Base14 (Helvetica, Times, Courier e affini) sono referenziati per nome e non contengono alcun programma incorporato in un documento semplice, perciò non c’è nulla su cui eseguire il subsetting. Il subsetting riduce solo i tipi di carattere incorporati a partire da un file di font.
  • Il subsetting è automatico e silenzioso. Non esiste alcun flag, alcun metodo né alcuna conferma. Se è stato incorporato un font ed è stato renderizzato testo con quel font, il writer ne ha eseguito il subsetting. Il programma incorporato include un tag di prefisso subset di sei lettere (ad esempio ABCDEF+LiberationSans), così che un lettore possa distinguere un subset da un incorporamento completo.
  • Un risparmio ridotto mantiene il font completo. Quando un subset consentirebbe di risparmiare meno del dieci percento delle dimensioni del programma, il subsetter restituisce l’originale. Si tratta di una soglia minima deliberata: la ricostruzione non giustifica un guadagno marginale. Incorporare un tipo di carattere già molto piccolo, o renderizzare quasi tutti i glifi, può rientrare in questo caso.
  • imageCacheBytes non è un controllo delle dimensioni delle immagini. Limita la memoria, non i byte di output. NextPDF Core incorpora i dati delle immagini forniti; non è previsto alcun passaggio di ricampionamento, sottocampionamento o ricodifica. Se sono necessarie immagini più piccole, ridimensionarle e ricodificarle prima di incorporarle.
  • Non esiste alcuna impostazione per i flussi di oggetti o per la deduplicazione. NextPDF Core non espone alcuna opzione per i flussi di oggetti PDF 2.0 né per la deduplicazione delle risorse. Non cercarla: le leve sulle dimensioni sono la compressione dei flussi e il subsetting dei font.

La compressione a livello 9 è il principale costo CPU nella scrittura di un flusso. Richiede qualche punto percentuale in più del tempo di build in cambio dell’output più piccolo possibile. Il costo è lineare rispetto al numero di byte non compressi, perciò il numero di pagine e la quantità di dati di font incorporati determinano il budget. Il subsetting aggiunge un passaggio una tantum per ogni tipo di carattere incorporato: analizza la directory delle tabelle del font, risolve la chiusura dei glifi utilizzati e ricostruisce le tabelle necessarie. Per un tipo di carattere CJK di grandi dimensioni questo è il più oneroso dei due passaggi, ma viene eseguito una volta per font, non una volta per pagina. La soglia minima di risparmio del dieci percento esiste in parte per tenere quel passaggio fuori dal percorso critico quando non sarebbe vantaggioso. Un documento di piccole dimensioni con un solo subset incorporato rientra agevolmente in un limite di 1500 ms e in un budget di picco di 96 MB. Limitare imageCacheBytes al proprio limite massimo effettivo, così che una build che incorpora molte immagini fallisca rapidamente per esaurimento di memoria anziché ricorrere allo swap.

La build viene eseguita nel processo; nessun byte del documento lascia l’host e non viene effettuata alcuna chiamata di rete. Trattare qualsiasi font o immagine fornito dall’esterno come input non attendibile:

  • Convalidare la directory dei font. L’esempio per la produzione legge il percorso del font da una variabile d’ambiente controllata dal server e rifiuta una directory mancante o non leggibile prima dell’incorporamento. Non ricavare mai un percorso di font da un campo di richiesta.
  • Incorporare solo i font che si è autorizzati a ridistribuire. Un subset è pur sempre un programma di font incorporato. Verificare che la licenza consenta l’incorporamento prima di distribuire un documento che include il tipo di carattere.
  • I font malformati generano un’eccezione, non corrompono in modo silenzioso. Un file di font che non può essere analizzato genera NextPDF\Exception\FontParsingException e un errore irreversibile di zlib genera NextPDF\Exception\CompressionException. Catturare l’eccezione più specifica e agire di conseguenza. Non racchiudere mai la build in un catch vuoto.
  • Non interpolare mai l’input dell’utente nel percorso di output. L’esempio scrive su un percorso fisso o su un canale secondario controllato dal server e rifiuta gli stream wrapper e i byte null tramite il writer atomico in save(). Ricavare i percorsi di output da valori controllati dal server per evitare il path traversal.
  • Nessun segreto nel documento. Non incorporare credenziali, token o identificatori interni in un documento generato che viene restituito a un client.

Questa ricetta non formula affermazioni normative autonome sugli standard. I meccanismi che utilizza sono definiti dalla specifica PDF 2.0: la compressione dei flussi FlateDecode (ISO 32000-2:2020 §7.4.4) e la denominazione dei subset di font con un prefisso subset di sei caratteri (ISO 32000-2:2020 §9.9). NextPDF emette entrambi come parte del suo percorso di scrittura standard; non occorre configurarli oltre il flag compress. Il profilo di riproducibilità structural dichiarato da questa pagina riflette il fatto che il writer fissa il livello DEFLATE, perciò i flussi compressi sono deterministici, mentre gli identificatori a livello di documento possono comunque variare tra le esecuzioni a meno che non si configurino anche impostazioni deterministiche. Per i meccanismi di incorporamento alla base del subsetting, consultare la ricetta embed-and-subset collegata di seguito.