Articolo tecnico

HotPDF CompressDocument: subset di font compatti in Delphi

HotPDF THotPDF.CompressDocument è un unico interruttore che fa produrre a BeginDoc il più piccolo PDF lossless che il componente sappia scrivere: FlateDecode al livello massimo, un cross-reference stream con object stream, font subsetting e subset di font compatti che rinumerano i glyph conservati dietro a un esplicito /CIDToGIDMap. EndDoc poi rimette a posto le tue impostazioni. Un documento di prova di tre pagine in Arial e SimSun è sceso da 10.2 MB a 20 KB con rendering identico

Che cosa attiva davvero CompressDocument?

CompressDocument sovrascrive sei impostazioni dello scrittore, più il tetto degli object stream, per un solo documento e le ripristina tutte in seguito. A BeginDoc, prima che la versione PDF si assesti, HotPDF registra i tuoi valori e imposta Compression a cmFlateDecode, CompressionLevel a clMaximum, accende EnableFontSubsetting e CompactFontSubsetting, e abilita UseXRefStream più UseObjectStreams (ISO 32000-1 §7.5.7 e §7.5.8). Gli object stream richiedono PDF 1.5, quindi un Version più vecchio viene portato a 1.5 quando non è bloccato. PDF/A-1 vieta entrambe le strutture, quindi un documento PDF/A-1 conserva la sua classica tabella cross-reference e riceve solo il lavoro Flate e sui font. Le immagini restano esattamente come le hai incorporate

Diagramma del ciclo di vita di HotPDF CompressDocument in Delphi: BeginDoc registra i valori propri dello scrittore, sovrascrive sei impostazioni tra cui Compression e UseObjectStreams per un solo documento, e EndDoc ripristina ogni valore preso in prestito nel suo finally più esterno mentre la proprietà CompressDocument stessa resta True
Sei impostazioni dello scrittore e il tetto degli object stream sono presi in prestito per esattamente un documento e restituiti quando EndDoc gira, così un report fallito non lascia mai il componente bloccato alla compressione massima
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'invoice-2026-1042.pdf';
    Pdf.CompressDocument := True;    // applicato da BeginDoc, annullato da EndDoc
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Il ripristino avviene nel finally più esterno di EndDoc, quindi un'eccezione a metà di un report non lascia un componente di lunga vita bloccato alla compressione massima per il lavoro successivo. La proprietà CompressDocument stessa resta True; solo le sei impostazioni prese in prestito tornano indietro. La versione è gestita con più cura. HotPDF annulla la propria elevazione a 1.5 solo se il documento finisce comunque a 1.5, quindi quando un'altra funzionalità ha spinto il file a 1.6 durante l'esecuzione (un font OpenType incorporato, diciamo), la versione più alta resta, esattamente come sarebbe stata senza compressione

Perché i subset di font restano grandi senza compattazione?

Un subset TrueType classico butta i contorni che non disegni mai ma conserva ogni glyph ID dov'era, ed è quella numerazione a tenerlo pesante. Il content stream mostra CID uguali ai GID originali, quindi il subset deve conservare un offset loca e una voce hmtx per ogni slot fino al glyph più alto che trattiene, vuoto o no. Per un carattere latino quell'overhead è rumore. Per un carattere CJK come SimSun, i cui ideogrammi siedono in profondità in una tabella di glyph molto grande, due caratteri cinesi si trascinano dietro tabelle dimensionate per l'intero font. Le regole di chiusura del subset per i glyph modellati dallo shaping decidono quali glyph sopravvivono; la compattazione riguarda quanto costano i superstiti

CompactFontSubsetting rinumera i glyph conservati in un intervallo denso che parte da zero e scrive uno stream /CIDToGIDMap sul CIDFont, che ISO 32000-1 §9.7.4.2 definisce come una tabella di GID a due byte indicizzata per CID. Quella tabella è tutto il trucco. Content stream, array delle larghezze /W e ToUnicode CMap conservano tutti i CID originali, quindi nulla di già scritto deve cambiare; solo la ricerca da CID a glyph si sposta nella mappa. Nel test che ha motivato la funzionalità, SimSun con due caratteri è passato da 24.8 KB di dati font a 3.1 KB

Confronto tra un subset di font HotPDF sparso, che trattiene voci loca e hmtx per ogni glyph ID originale fino al GID trattenuto più alto, e l'output di CompactFontSubsetting che rinumera i glyph conservati densamente da zero e mappa i CID attraverso uno stream CIDToGIDMap mentre content stream, /W e ToUnicode restano invariati
La rinumerazione sposta il costo fuori dal programma del font e in un piccolo stream di mappa — due caratteri SimSun scesi da 24.8 KB a 3.1 KB senza toccare un byte di contenuto già scritto

La compattazione ha limiti netti, e degrada in silenzio invece di fallire. HotPDF costruisce subset compatti solo per caratteri TrueType Type 0, sia quelli impostati attraverso SetFont con subsetting attivo sia il carattere registrato attraverso RegisterUnicodeTTF. Un semplice font TrueType trova i propri glyph attraverso la cmap dentro il programma del font, che la rinumerazione romperebbe, quindi conserva il subset sparso. I caratteri OpenType-CFF non hanno un percorso compatto nemmeno loro. Una build compatta che fallisce ripiega sul subset sparso invece di sollevare un'eccezione. La proprietà è spenta per default, quindi l'output esistente resta identico byte per byte, mentre sotto PDF/A il carattere Unicode registrato riceve sempre un subset compatto

Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True;   // usabile senza CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;

Come comprime la struttura del file lo scrittore packed?

Una volta che font e stream sono piccoli, i dizionari e i dati cross-reference diventano il costo residuo più grande, quindi lo scrittore di object stream dietro a CompressDocument li sfoltisce pure. La guida su object stream e aggiornamenti incrementali copre il formato contenitore in sé; il percorso di compressione aggiunge quattro raffinamenti sopra:

  • Sintassi compatta per ISO 32000-1 §7.2.2: uno spazio viene scritto solo tra due token che altrimenti scorrerebbero insieme come caratteri regolari, così /Type /Page diventa /Type/Page
  • I campi dello cross-reference stream prendono qualsiasi larghezza §7.5.8.2 consenta, così un file sotto 16 MB memorizza ogni offset in 3 byte invece di 4
  • Fino a 250 oggetti finiscono in ogni object stream invece dei consueti 100, a meno che tu non imposti il tuo tetto attraverso ConfigureAdaptiveObjectStreamPacking
  • Quando il file non è cifrato, il Catalog e il dizionario Info vengono impacchettati negli object stream pure loro; l'output cifrato li tiene al livello più alto

La sintassi compatta è arrivata con una trappola che vale conoscere se estendi lo scrittore. La firma riempie la signature dopo che il file è stato scritto cercando nei byte i placeholder letterali /ByteRange ( e /Contents <, e la grafia compatta trasformerebbe quelli in /ByteRange( e /Contents<, che la ricerca non trova mai. I dizionari di firma (Type Sig o DocTimeStamp, FT Sig) e il dizionario di cifratura conservano quindi il layout con spazi. Un difetto correlato ha colpito le build prima della v2.766.41: ogni salvataggio con object stream, CompressDocument incluso, iniziava con due righe di header %PDF-, quindi aggiorna se un validatore rigoroso segnala il tuo output

Puoi comprimere un PDF già caricato?

Sì, attraverso l'overload con opzioni CompressLoadedDocument(Options, Info), che esegue gli stessi passi lossless su un file esistente. Con THPDFLoadedDocumentCompressionOptions.Default rimuove le risorse di pagina inutilizzate, fonde font e form identici, crea subset dei font incorporati con subset compatti attivi, ricomprime gli stream non filtrati, Flate, LZW, ASCII e RunLength con Flate quando il risultato è più piccolo, e fa sì che il prossimo salvataggio usi gli object stream. HighRatioFlate è spento per default, e gli object stream vengono saltati per PDF/A-1 e salvataggi incrementali. L'overload senza parametri CompressLoadedDocument è la chiamata più vecchia e più stretta che si limita a comprimere con Flate gli stream non compressi

Flusso di HotPDF CompressLoadedDocument in Delphi: la chiamata rimuove le risorse di pagina inutilizzate, fonde font e form identici, crea subset dei font incorporati con subset compatti, ricomprime gli stream con Flate solo quando il risultato è più piccolo, e accende gli object stream per il prossimo salvataggio, mentre i campi di firma scatenano RefusedBySignaturePolicy e lasciano il file intoccato
Ogni passo riscrive byte che una firma copre, quindi l'intero documento viene rifiutato a meno che tu non autorizzi esplicitamente l'invalidazione — Info.BytesSaved poi somma solo il lavoro su risorse, font e stream
var
  Doc: THotPDF;
  Options: THPDFLoadedDocumentCompressionOptions;
  Info: THPDFLoadedDocumentCompressionInfo;
begin
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    Doc.LoadFromFile('quarterly-report.pdf');
    Options := THPDFLoadedDocumentCompressionOptions.Default;
    Doc.CompressLoadedDocument(Options, Info);
    if Info.RefusedBySignaturePolicy then
      Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
    else
    begin
      Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
        [Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
      Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
    end;
  finally
    Doc.Free;
  end;
end;

Due confini contano sul percorso loaded. Ogni passo riscrive byte che una firma copre, quindi un documento con campi di firma viene rifiutato nell'insieme: la chiamata restituisce 0, imposta RefusedBySignaturePolicy e non cambia nulla, a meno che tu non imposti AllowSignatureInvalidation, dopo di che Info.SignaturesInvalidated ti dice cosa hai rinunciato. La compattazione è anche più conservativa qui che sul percorso di creazione. HotPDF compatta solo i programmi di font usati esclusivamente da font CIDFontType2 con una /CIDToGIDMap Identity, dove CID eguaglia GID, e salta i programmi con uno stream di mappa esistente, un /CIDSet, o tabelle di glyph a colori come COLR, sbix, CBDT o SVG, perché la ricostruzione compatta perderebbe i layer di colore. Nota anche che Info.BytesSaved somma solo i passi su risorse, font e stream; il guadagno degli object stream si vede quando il file viene scritto

Quali risultati puoi aspettarti in pratica?

I guadagni seguono quanto di un file è struttura non compressa e dati font sovradimensionati, non quante pagine ha. Il campione di tre pagine in Arial e SimSun si è ridotto da 10.2 MB a 20 KB quando generato con CompressDocument, e da 10.2 MB a 19.8 KB quando l'originale non compresso è stato caricato e passato per CompressLoadedDocument, con rendering identico in entrambi i casi. Un PDF già compatto si muove appena: nel set di regression, tali file hanno risparmiato tra -0.07% e +0.06% della loro dimensione originale. I file ricchi di foto guadagnano poco, perché nessuno dei due percorsi tocca i dati delle immagini

Se generi gli stessi report CJK ogni notte, abbina i subset compatti alla cache persistente dei subset di font su disco così il lavoro di subsetting non viene ripetuto a ogni esecuzione, e fai il diff degli output compressi per contenuto di oggetto piuttosto che per byte, dato che un solo campo modificato ri-Flate un intero object stream. I riferimenti completi a proprietà e record sono sulla pagina di prodotto di HotPDF Delphi PDF component