Articolo tecnico

Ridurre le dimensioni dei file PDF in Delphi: Font, Immagini, LZW

Per ridurre le dimensioni dei file PDF in Delphi, losLab PDF Library fornisce tre API che colpiscono le tre principali fonti di gonfiore: SubsetEmbeddedFonts riduce ogni programma di font TrueType incorporato ai soli glifi effettivamente renderizzati nel documento, DownsampleImages ricampiona le immagini raster che superano una DPI target e NormalizeLZWStreams sostituisce la compressione legacy LZWDecode con FlateDecode. Ciascuna restituisce il numero di oggetti modificati, quindi uno zero indica che il passaggio è stato una no-op anziché un fallimento silenzioso

Un PDF unito o generato a livello di programmazione è solitamente sovradimensionato per uno di tre motivi: font interamente incorporati, immagini campionate ben al di sopra della loro risoluzione di visualizzazione e flussi ancora compressi con il filtro LZW legacy. Lo standard ISO 32000-1 §9.9 consente al produttore di incorporare l'intero programma del font, e la maggior parte dei produttori fa esattamente questo perché è l'impostazione predefinita sicura. Un file Arial FontFile2 completo pesa centinaia di kilobyte; incorporalo in una dozzina di file sorgente, uniscili e ti ritroverai con una dozzina di copie dei contorni dei glifi per caratteri che nessuno ha mai digitato. L'unione in sé non crea lo spreco, lo concentra semplicemente in un unico file dove il totale diventa finalmente visibile

Le immagini sono il secondo colpevole. Una scansione larga 4800 pixel inserita in una cornice di un quarto di pagina trasporta circa 40 volte più dati di pixel di quanti una pipeline di stampa a 300 DPI possa effettivamente utilizzarne. Il terzo è più silenzioso: i flussi filtrati con LZWDecode. Lo standard ISO 32000-1 §7.4.4 specifica sia LZWDecode che FlateDecode, e osserva che di solito Flate comprime altrettanto bene, se non meglio; in pratica, l'output Flate è costantemente più piccolo a parità di dati, e LZW sopravvive principalmente in file che a un certo punto della loro storia sono passati attraverso strumenti degli anni '90. Il resto di questo articolo esamina i tre passaggi della losLab PDF Library che risolvono ciascun problema, per poi combinarli in un'unica pipeline

Subsetting dei font con SubsetEmbeddedFonts

SubsetEmbeddedFonts riduce ogni font TrueType incorporato in un documento caricato ai soli caratteri effettivamente utilizzati nel documento, e non necessita di argomenti poiché ricava l'elenco dei caratteri da conservare direttamente dai flussi di contenuto stessi. Internamente, il passaggio analizza il flusso di contenuto di ogni pagina tramite GetTextRuns, raccoglie i codici dei caratteri a cui si fa riferimento in ciascuna risorsa font, crea un elenco di conservazione e passa il programma originale del font all'engine Windows FontSub (CreateFontPackage) per produrre un subset. Il programma riscritto sostituisce il flusso FontFile2 sul posto, e il nome BaseFont ottiene un tag LOSABC+ tag, la convenzione definita da ISO 32000-1 §9.6.4 (sei lettere maiuscole seguite da un segno più) per i font subset. Tale prefisso è anche ciò che rende la chiamata idempotente: eseguendo il passaggio due volte, i font già sottoposti a subsetting vengono riconosciuti e saltati, il che rende sicuro l'inserimento in un processo batch che potrebbe elaborare nuovamente i file

Due dettagli implementativi meritano di essere notati perché spiegano i limiti dell'API. In primo luogo, il passaggio ha come target FontFile2, quindi copre i programmi TrueType incorporati; i font incorporati come Type 1 o CFF non vengono toccati per evitare rischi. In secondo luogo, fa affidamento su FontSub, il che rende SubsetEmbeddedFonts disponibile solo su Windows. Un punto più sottile dell'implementazione: se un font è idoneo viene deciso risolvendo effettivamente la catena di riferimenti FontDescriptorFontFile2, e non fidandosi di un'euristica del flag di incorporamento, poiché i font in un documento caricato non sono mai passati attraverso le operazioni sul lato creazione che impostano tali flag. Se il flusso risolto esiste, il font è un candidato; altrimenti viene ignorato senza errori

Il compromesso effettivo: un font subset contiene solo i glifi presenti al momento del subsetting. Se uno strumento a valle, o il tuo codice stesso, aggiunge successivamente del testo nello stesso font, qualsiasi carattere al di fuori del subset non avrà contorni e verrà visualizzato come un glifo mancante. Esegui il subsetting come ultimo passaggio di modifica dei contenuti, mai prima di una fase di editing. La stessa cautela si applica se prevedi di estrarre nuovamente il font in seguito per riutilizzarlo; l'articolo sull'estrazione di testo, immagini e font con PDFlibPas illustra ciò che un programma subset estratto può e non può offrire

Come decide DownsampleImages quali immagini ridurre?

DownsampleImages(MaxDPI, Quality, Filter) ricampiona solo le immagini che può considerare con certezza sovracampionate, utilizzando una stima DPI deliberatamente conservativa. Un XObject immagine PDF memorizza le dimensioni dei pixel ma non una risoluzione fisica affidabile, e qualsiasi tag DPI dell'immagine sorgente raramente sopravvive a un ciclo di caricamento-modifica-salvataggio. Pertanto, il passaggio stima SrcDPI = PixelWidth / 8.5, in pratica chiedendo: se questa immagine coprisse l'intera larghezza di una pagina Letter, quale sarebbe la sua risoluzione? Vengono toccate solo le immagini la cui stima supera MaxDPI. Questo comportamento è intenzionale: un'immagine posizionata in piccolo sulla pagina ha un DPI reale superiore alla stima, quindi il passaggio preferisce attivarsi meno volte piuttosto che degradare una risorsa di qualità di stampa che non può misurare

Quality da 1 a 100 seleziona la qualità di ricodifica JPEG, mentre 0 mantiene l'output come Flate senza perdita in stile PNG; Filter sceglie il kernel di ricampionamento, 0 per una media box e 1 per bilineare. Per i documenti d'ufficio scansionati, DownsampleImages(150, 75, 1) è un punto di partenza ragionevole; per qualsiasi cosa possa essere ristampata, aumenta MaxDPI a 300 o salta completamente il passaggio. Il ricampionamento è l'unico passaggio con perdita dei tre, quindi dovrebbe essere protetto da un'impostazione che gli utenti possono disattivare

Convertire i flussi LZW legacy con NormalizeLZWStreams

NormalizeLZWStreams è un vantaggio gratuito: decomprime senza perdita ogni flusso LZWDecode e lo ricomprime con FlateDecode sul posto, restituendo il numero di flussi convertiti. Gestisce sia una singola voce /Filter /LZWDecode sia la presenza di LZW all'interno di un array di filtri concatenati, dove solo il collegamento LZW viene sostituito e il resto della catena viene preservato. I parametri del predittore (Predictor, Columns, Colors, BitsPerComponent) vengono letti dai DecodeParms del flusso e passati al decompressore, in modo che i dati delle immagini codificati con predittore vengano convertiti correttamente. Poiché entrambi i filtri sono codec esatti a livello di bit, i byte decodificati sono identici prima e dopo; cambia solo la compressione del contenitore, motivo per cui questo passaggio è sicuro da eseguire incondizionatamente su ogni file

Su un documento senza flussi LZW, la chiamata restituisce semplicemente 0 e non tocca nulla, operazione che la suite di test di regressione della libreria verifica esplicitamente: un file Flate appena creato deve segnalare zero conversioni. Tale garanzia no-op è importante quando il passaggio si trova in una pipeline che elabora migliaia di file eterogenei, alcuni del 2024 e altri del 1998

La pipeline di ottimizzazione delle dimensioni completa in Delphi

I three passaggi si combinano in un'unica funzione di caricamento-ottimizzazione-salvataggio, e l'ordine è meno importante di quanto ci si possa aspettare perché operano su tipi di oggetti disgiunti: font, XObject immagine e filtri di flusso. Eseguire prima il subsetting rimane la scelta ottimale, poiché è il passaggio con un vincolo sull'ordine di modifica

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

Verifica la pipeline nello stesso modo in cui la libreria verifica se stessa: round-trip. I test di regressione v3.130 creano un documento, lo salvano, lo ricaricano, eseguono l'ottimizzazione, lo salvano di nuovo e quindi verificano tre cose: l'output è più piccolo, i conteggi restituuiti corrispondono alle aspettative e il ricaricamento del file ottimizzato viene comunque analizzato e renderizzato. Riprodurre questo ciclo di creazione-ottimizzazione-ricaricamento su un campione dei propri file di produzione e confrontare il testo estratto prima e dopo è un investimento di un'ora che rileva errori di integrazione molto prima che un cliente apra una fattura corrotta

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

Dove si colloca la pipeline in un flusso di lavoro di unione? Dopo l'unione, non durante. Eseguire prima l'unione e ottimizzare il singolo risultato finale significa che ogni font incorporato viene sottoposto a subsetting una sola volta rispetto all'unione di tutti i caratteri utilizzati, anziché per singolo file sorgente. Se il throughput di unione rappresenta il collo di bottiglia, PDFlibPas offre un percorso rapido a livello di byte che evita l'analisi completa degli oggetti, descritto nell'articolo sulla fusione rapida di PDF con spostamento dei riferimenti di byte; e per input troppo grandi per essere memorizzati completamente in memoria, la guida alla fusione e divisione con accesso diretto per PDF di grandi dimensioni copre il percorso di streaming. Entrambi si associano naturalmente a un passaggio di ottimizzazione finale sull'output unito

Cosa non faranno i tre passaggi

Il trio di ottimizzazione di losLab PDF Library esclude deliberatamente qualsiasi cosa che modifichi la semantica del documento. SubsetEmbeddedFonts non unifica i font duplicati tra le sorgenti unite in un unico programma, ma riduce ciascuno in modo indipendente; la deduplicazione è una trasformazione diversa e più rischiosa. DownsampleImages ignorerà un'immagine la cui stima DPI conservativa rimane sotto la soglia anche quando un essere umano potrebbe dire che è sovradimensionata per la sua cornice. E nessuno dei passaggi tocca la struttura del documento, quindi un file gonfiato da migliaia di oggetti orfani richiede un salvataggio in stile riscrittura piuttosto che questi passaggi a livello di flusso. Entro questi limiti, la combinazione di subsetting dei font, ricampionamento delle immagini e normalizzazione da LZW a Flate elimina le tre classiche fonti di gonfiore dei PDF con una sola chiamata API prevedibile ciascuna. Le tre funzioni vengono fornite come parte della losLab PDF Library per Delphi, C# e VB.NET, insieme alle API di unione, estrazione e rendering sopra descritte