Articolo tecnico

Subsetting di Font TrueType tramite fontsub.dll in Delphi

HotXLS, il componente Excel per Delphi e C++Builder, riduce la dimensione dei font PDF incorporati tramite il subsetting dei font TrueType: al momento dell'esportazione PDF chiama la funzione CreateFontPackage della libreria di sistema Windows fontsub.dll per ricostruire un font TrueType incorporato attorno ai soli code point Unicode effettivamente usati da un foglio di lavoro, invece di distribuire l'intero file del carattere tipografico. Un report con duecento righe di nomi di prodotto in cinese potrebbe aver bisogno solo di poche centinaia di caratteri Han distinti, eppure i font CJK forniti da Windows pesano abitualmente dai 5 ai 20 MB ciascuno. Incorporane uno intero, e il solo font può superare in peso ogni altro oggetto del PDF messo insieme

fontsub.dll non è una libreria di cui la maggior parte degli sviluppatori Delphi abbia mai sentito parlare, e c'è un motivo: Microsoft la distribuisce come una piccola DLL di utilità scarsamente documentata piuttosto che come un'API Win32 di primo piano. HotXLS la tratta come una capacità opzionale, non come una dipendenza rigida, quindi il modo in cui l'esportatore la carica, la chiama e ripiega su un'alternativa quando è assente racconta tanto della programmazione Windows difensiva quanto dei formati dei font, ed entrambe le metà di questa storia meritano di essere percorse

Perché il testo Unicode fa gonfiare un'esportazione PDF di HotXLS?

L'esportatore PDF di HotXLS ricorre a un font TrueType incorporato solo quando il testo del foglio di lavoro esce dai confini di WinAnsi, e resta sulla famiglia Helvetica integrata il resto del tempo, il percorso predefinito trattato in profondità nella guida all'esportazione da foglio di lavoro a PDF. WinAnsi copre il testo dell'Europa occidentale abbastanza bene da far sì che molte cartelle di lavoro non facciano mai scattare alcun incorporamento di font: il PDF si limita a riferirsi a Helvetica per nome e il lettore lo fornisce localmente, quindi il file resta piccolo. Nel momento in cui una cella contiene qualcosa che WinAnsi non può rappresentare, un nome di prodotto cinese, una nota in coreano, un simbolo estraneo in un commento, l'esportatore deve incorporare un vero programma font, perché un lettore PDF non ha alcuna fonte di glifi di riserva per caratteri al di fuori dei 14 font standard

HotXLS individua quel font automaticamente, scansionando la cartella Fonts di Windows alla ricerca di un breve elenco di candidati installati, inclusi i caratteri tipografici capaci di CJK forniti da Windows per il rendering cinese e coreano, a meno che la proprietà UnicodeFontFile dell'esportatore non punti già a un file specifico, e qualunque font finisca per scegliere viene incorporato interamente prima che il subsetting entri mai in gioco. Quel requisito di incorporamento è specifico del PDF: i percorsi di esportazione RTF e HTML di HotXLS mantengono intatto il testo Unicode facendo l'escape dei code point nel flusso di byte invece di distribuire un programma font, motivo per cui il problema di dimensione trattato in questo articolo non ha equivalente su quei due formati

uses
  lxHandle, lxPDF;

var
  Book: TXLSWorkbook;
  Exporter: TXLSPDFExport;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('catalog-cn.xlsx');
    Exporter := TXLSPDFExport.Create;
    try
      // Optional: pin a specific CJK-capable font instead of the
      // exporter's automatic Windows\Fonts scan.
      Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
      Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Cos'è fontsub.dll, e perché non scrivere un subsetter da zero?

fontsub.dll è una piccola libreria di sistema Windows, distribuita a partire da Windows XP, che espone un'unica funzione rilevante qui: CreateFontPackage. Passale i byte di un font TrueType sorgente e un elenco di code point Unicode da mantenere, e restituisce un font minimale che soddisfa comunque ogni vincolo del formato font: indici dei glifi rinumerati, glyf e loca ricostruiti attorno ai soli contorni mantenuti, hmtx e cmap riscritti per corrispondere. HotXLS dichiara il tipo di puntatore a funzione direttamente rispetto a quel contratto

const
  TTFCFP_FLAGS_SUBSET = 1;
  TTFMFP_SUBSET = 0;
  TTFCFP_MS_PLATFORMID = 3;
  TTFCFP_UNICODE_CHAR_SET = 1;

type
  TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
    var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
    var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
    usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
    pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
    lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;

Scrivere a mano il lavoro di CreateFontPackage invece di chiamarla significherebbe implementare un vero e proprio subsetter TrueType corretto: percorrere i glifi composti per includere ogni glifo componente a cui fa riferimento un glifo mantenuto, ricostruire gli offset di loca dopo che i contorni vengono eliminati, rispettare i bit di permesso di incorporamento nella tabella OS/2 di un font, e ottenere tutto questo correttamente su qualsiasi font bizzarro che il computer di un cliente si trovi ad avere installato. Microsoft ha già risolto quel problema e distribuisce la soluzione come parte di Windows stesso, quindi chiamare una DLL di sistema che lei stessa mantiene, testa contro il proprio stack di rendering dei font e distribuisce gratuitamente su ogni macchina costa a HotXLS un caricamento dinamico e un puntatore a funzione; reimplementare la stessa logica significherebbe possedere un parser per un formato binario con decenni di casi limite, per una funzionalità che conta solo quando un font risulta grande

Costruire l'elenco da mantenere a partire dai glifi effettivamente renderizzati

HotXLS costruisce l'elenco da mantenere per il subsetting a partire da una mappa che stava già manutenendo per un motivo diverso, quindi la contabilità non costa nulla in più. Ogni volta che il codice di rendering di pagina disegna un carattere che richiede il font Unicode incorporato, cerca l'indice del glifo di quel carattere e registra l'abbinamento in FUnicodeGlyphMap, una tabella glifo-verso-code-point che guida anche la CMap PDF ToUnicode, cosicché copiare e incollare fuori dal documento finito restituisca il testo originale invece degli ID di glifo grezzi. Nel momento in cui gli stream di contenuto della pagina sono terminati, quella mappa elenca già esattamente l'insieme di code point Unicode che il documento ha usato, né più né meno

var
  keepList: array of Word;
  keepCount, i: Integer;
  codePoint: LongWord;
begin
  SetLength(keepList, FUnicodeGlyphMap.Count);
  keepCount := 0;
  for i := 0 to FUnicodeGlyphMap.Count - 1 do
  begin
    codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
    if codePoint > 0 then
    begin
      keepList[keepCount] := Word(codePoint);
      Inc(keepCount);
    end;
  end;
end;

Al momento della finalizzazione, HotXLS percorre quella stessa mappa una seconda volta per costruire l'elenco da mantenere che CreateFontPackage si aspetta, un semplice array dei code point Unicode da mantenere nella forma a 16 bit richiesta dall'argomento dell'elenco da mantenere dell'API. Poiché quell'argomento è un array di parole a 16 bit, indirizza in modo pulito il Basic Multilingual Plane, che copre senza complicazioni il normale testo CJK, cirillico, greco e arabo; un foglio di lavoro che si appoggia a caratteri del piano supplementare, certe emoji o script storici rari, si trova al di fuori di ciò che una singola voce dell'elenco da mantenere può nominare direttamente, il che è un confine da conoscere piuttosto che un difetto, poiché la grande maggioranza dei fogli di calcolo aziendali ricchi di Unicode non si avvicina mai a quel piano in primo luogo

Cosa succede quando fontsub.dll è assente?

HotXLS non presuppone mai che fontsub.dll sia presente, e l'esportazione PDF non fallisce mai perché non lo è. La libreria viene caricata dinamicamente nel momento in cui serve un subset, con SafeLoadLibrary e GetProcAddress anziché un import statico, proprio perché fontsub.dll non è un'API pubblica documentata e garantita presente nel modo in cui lo è kernel32.dll: è uno strumento di incorporamento font incluso, e nulla nel contratto di Microsoft promette che sopravviva su ogni SKU, ogni ramo di servicing, o ogni livello di compatibilità che tenti di emulare Windows

var
  hFontSub: HMODULE;
  CreateFontPackage: TCreateFontPackage;
begin
  hFontSub := SafeLoadLibrary('FontSub.dll');
  if hFontSub = 0 then
    Exit; // no subsetting available - keep the full embedded font
  try
    @CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
    if not Assigned(CreateFontPackage) then
      Exit;
    // ... call CreateFontPackage, check its return code ...
  finally
    FreeLibrary(hFontSub);
  end;
end;

Ogni percorso di fallimento si ripiega sullo stesso esito. Una DLL mancante, un export mancante, un codice di ritorno diverso da zero, oppure un font la cui tabella OS/2 vieta il subsetting tramite i propri bit di permesso di incorporamento, HotXLS si limita a mantenere il font completo che aveva già incorporato e prosegue. Nulla solleva eccezioni, nulla interrompe l'esportazione, e il codice chiamante non deve mai avvolgere un'ottimizzazione dei font nella propria gestione delle eccezioni; il PDF esportato è valido in entrambi i casi, e l'unica variabile è se finisce per essere piccolo o un po' più grande

Quanto diventa realmente più piccolo il PDF?

Il subsetting dei font TrueType di HotXLS tipicamente riduce il PDF esportato di un foglio di lavoro ricco di Unicode a un valore compreso tra un ventesimo e un ottavo della sua dimensione non sottoposta a subsetting, una riduzione di 8-20 volte la cui scala segue quanto di un font completo un dato documento tocchi effettivamente: un ordine di acquisto costruito attorno a poche centinaia di caratteri cinesi distinti mantiene solo quelle poche centinaia di glifi sulle decine di migliaia distribuite da un carattere tipografico CJK, mentre un foglio che copre un mix più ampio di caratteri ne mantiene proporzionalmente di più. HotXLS applica un ulteriore passaggio di compressione Flate sopra i byte del font sottoposto a subsetting prima di scriverli nello stream /FontFile2 del PDF, la stessa compressione a cui è già sottoposto il resto degli stream di contenuto del documento, e nulla di tutto ciò richiede alcunché in più al codice chiamante: un foglio di lavoro che non esce mai da WinAnsi non tocca mai questo percorso e continua a esportare tramite il semplice Helvetica, mentre un foglio di lavoro che fa scattare il percorso del font Unicode ottiene automaticamente il subsetting, senza alcuna proprietà da impostare e nessuna chiamata separata da effettuare, e l'unica proprietà coinvolta, UnicodeFontFile, sceglie solo quale font venga incorporato e sottoposto a subsetting, non se il subsetting avvenga

Il subsetting dei font è un dettaglio all'interno della più ampia superficie di esportazione PDF del componente Excel Delphi HotXLS, insieme alla paginazione, ai metadati di stampa del foglio di lavoro e ai percorsi di esportazione CSV, HTML e RTF con cui viene distribuito