Odborný článok

Podmnožinovanie písiem TrueType cez fontsub.dll v Delphi

HotXLS, komponent Excel pre Delphi a C++Builder, znižuje veľkosť vložených písiem PDF prostredníctvom podmnožinovania písiem TrueType: v čase exportu do PDF volá funkciu CreateFontPackage zo systémovej knižnice Windows fontsub.dll, aby znova zostavil vložené písmo TrueType iba okolo tých unicode bodov kódu, ktoré hárok skutočne použil, namiesto toho, aby odoslal celý súbor s typografiou. Report s dvesto riadkami čínskych názvov produktov možno potrebuje iba niekoľko stoviek odlišných čínskych znakov, no CJK písma, ktoré Windows dodáva, bežne vážia 5 až 20 MB každé. Vložte jedno celé a samotné písmo môže prevážiť každý iný objekt v PDF spolu

fontsub.dll nie je knižnica, o ktorej väčšina vývojárov v Delphi niekedy počula, a je na to dôvod: Microsoft ju dodáva ako malú, riedko zdokumentovanú pomocnú DLL, nie ako hlavné API Win32. HotXLS ju berie ako voliteľnú schopnosť, nie tvrdú závislosť, takže spôsob, akým ju exportér načíta, volá a ako zlyhá s ladnosťou, keď chýba, hovorí rovnako veľa o obrannom programovaní pre Windows ako o formátoch písiem, a obe polovice tohto príbehu sa oplatí prejsť

Prečo unicode text nafúkne export do PDF v HotXLS?

Exportér PDF v HotXLS siahne po vloženom písme TrueType iba vtedy, keď text hárka spadne mimo WinAnsi, a zvyšok času zostáva pri vstavanej rodine Helvetica, čo je predvolená cesta, ktorú do hĺbky pokrýva sprievodca exportom hárka do PDF. WinAnsi pokrýva západoeurópsky text natoľko dobre, že množstvo zošitov nikdy nevyvolá vloženie písma vôbec: PDF jednoducho odkazuje na Helvetica podľa mena a čítačka ju dodá lokálne, takže súbor zostáva malý. Vo chvíli, keď bunka obsahuje niečo, čo WinAnsi nedokáže reprezentovať, čínsky názov produktu, kórejskú poznámku, náhodný symbol v komentári, exportér musí vložiť skutočný program písma, pretože čítačka PDF nemá žiadny náhradný zdroj glyfov pre znaky mimo štandardných 14 písiem

HotXLS toto písmo nájde automaticky, prehľadá priečinok Windows Fonts pre krátky zoznam nainštalovaných kandidátov, vrátane písiem schopných CJK, ktoré Windows dodáva pre čínske a kórejské vykresľovanie, pokiaľ vlastnosť exportéra UnicodeFontFile už neukazuje na konkrétny súbor, a nech skončí pri ktoromkoľvek písme, celé sa vloží ešte predtým, než sa vôbec spustí podmnožinovanie. Táto požiadavka vkladania je špecifická pre PDF: exportné cesty RTF a HTML v HotXLS udržujú unicode text neporušený tak, že escapujú body kódu do bajtového prúdu namiesto toho, aby odoslali program písma, a preto problém veľkosti, ktorý tento článok rieši, na týchto dvoch formátoch nemá obdobu

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;

Čo je fontsub.dll a prečo si nenapísať podmnožinovač od nuly?

fontsub.dll je malá systémová knižnica Windows, dodávaná od Windows XP, ktorá tu sprístupňuje jedinú relevantnú funkciu: CreateFontPackage. Odovzdajte jej bajty zdrojového písma TrueType a zoznam unicode bodov kódu, ktoré si má ponechať, a ona vráti minimálne písmo, ktoré stále spĺňa každé obmedzenie formátu písma: preindexované indexy glyfov, znova zostavené glyf a loca iba okolo ponechaných obrysov, prepísané hmtx a cmap tak, aby sedeli. HotXLS deklaruje typ ukazovateľa na funkciu priamo podľa tohto kontraktu

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;

Napísať prácu CreateFontPackage ručne namiesto jej volania by znamenalo implementovať správny podmnožinovač TrueType: prechádzať kompozitné glyfy, aby sa vtiahol každý komponentný glyf, na ktorý ponechaný glyf odkazuje, znova zostavovať posuny loca po zahodení obrysov, rešpektovať bity povolenia vkladania v tabuľke OS/2 písma, a to všetko robiť správne naprieč akýmikoľvek zvláštnymi písmami, aké má zákazníkov počítač náhodou nainštalované. Microsoft tento problém už vyriešil a dodáva riešenie ako súčasť samotného Windows, takže volanie systémovej DLL, ktorú udržiava, testuje voči vlastnému stacku vykresľovania písiem a distribuuje na každý počítač zadarmo, stojí HotXLS iba dynamické načítanie a ukazovateľ na funkciu; opätovná implementácia rovnakej logiky by znamenala vlastniť parser binárneho formátu s desaťročiami okrajových prípadov, pre funkciu, na ktorej záleží iba vtedy, keď je písmo náhodou veľké

Zostavenie zoznamu na ponechanie z glyfov skutočne vykreslených

HotXLS zostavuje zoznam na ponechanie pre podmnožinovanie z mapy, ktorú si už udržiaval z iného dôvodu, takže toto účtovníctvo nestojí nič naviac. Zakaždým, keď kód vykresľovania stránky nakreslí znak, ktorý potrebuje vložené unicode písmo, vyhľadá index glyfu tohto znaku a zaznamená túto dvojicu do FUnicodeGlyphMap, tabuľky glyf-na-bod-kódu, ktorá tiež poháňa CMap ToUnicode v PDF, aby kopírovanie a vkladanie z hotového dokumentu vrátilo pôvodný text namiesto surových ID glyfov. Vo chvíli, keď sú obsahové prúdy stránok hotové, táto mapa už uvádza presne tú množinu unicode bodov kódu, ktorú dokument použil, nič viac a nič menej

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;

V čase finalizácie HotXLS prejde tú istú mapu druhýkrát, aby zostavil zoznam na ponechanie, ktorý CreateFontPackage očakáva, obyčajné pole unicode bodov kódu na ponechanie v 16-bitovej forme, akú vyžaduje argument zoznamu na ponechanie tohto API. Keďže tento argument je pole 16-bitových slov, čisto adresuje základnú viacjazyčnú rovinu (Basic Multilingual Plane), ktorá pokrýva bežný CJK, cyriliku, gréčtinu a arabčinu bez komplikácií; hárok, ktorý sa spolieha na znaky z doplnkových rovín, niektoré emoji alebo vzácne historické písma, sedí mimo toho, čo dokáže priamo pomenovať jediná položka zoznamu na ponechanie, čo je hranica, ktorú sa oplatí poznať, nie chyba, keďže drvivá väčšina obchodných tabuliek náročných na unicode sa k tejto rovine ani nepribližuje

Čo sa stane, keď fontsub.dll chýba?

HotXLS nikdy nepredpokladá, že fontsub.dll je prítomná, a export do PDF nikdy nezlyhá kvôli tomu, že nie je. Knižnica sa načítava dynamicky vo chvíli, keď je podmnožina potrebná, pomocou SafeLoadLibrary a GetProcAddress namiesto statického importu, práve preto, že fontsub.dll nie je zdokumentované, garantovane prítomné verejné API tak, ako je kernel32.dll: je to súčasť nástrojov na vkladanie písiem a nič v kontrakte Microsoftu nesľubuje, že prežije na každom SKU, každej servisnej vetve alebo akejkoľvek kompatibilnej vrstve, ktorá sa pokúša emulovať 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;

Každá cesta zlyhania sa zloží späť do rovnakého výsledku. Chýbajúca DLL, chýbajúci export, nenulový návratový kód, alebo písmo, ktorého tabuľka OS/2 zakazuje podmnožinovanie cez svoje bity povolenia vkladania, HotXLS jednoducho ponechá celé písmo, ktoré už vložil, a pokračuje ďalej. Nič nevyvolá výnimku, nič neprevrhne export, a volajúci kód nikdy nemusí obaľovať optimalizáciu písma vlastným ošetrením výnimiek; exportované PDF je platné v oboch prípadoch, jedinou premennou je iba to, či skončí malé alebo o niečo väčšie

O koľko sa PDF v skutočnosti zmenší?

Podmnožinovanie písiem TrueType v HotXLS bežne zmenší exportované PDF hárka náročného na unicode niekam medzi jednu dvadsatinu a jednu osminu jeho nezmenšenej veľkosti, čo je 8- až 20-násobné zníženie, ktorého rozsah sleduje, koľko z celého písma daný dokument skutočne využíva: objednávka postavená okolo niekoľkých stoviek odlišných čínskych znakov si ponechá iba tých niekoľko stoviek glyfov z desiatok tisíc, aké nesie typografia CJK, zatiaľ čo hárok pokrývajúci širšiu zmes znakov si ponechá pomerne viac. HotXLS navrch bajtov podmnožinovaného písma vrství ešte prechod kompresie Flate ešte predtým, než ich zapíše do prúdu /FontFile2 v PDF, rovnakú kompresiu, akou už prechádzajú obsahové prúdy zvyšku dokumentu, a nič z toho nežiada od volajúceho kódu nič naviac: hárok, ktorý nikdy neopustí WinAnsi, sa tejto cesty nikdy nedotkne a naďalej exportuje cez obyčajnú Helvetica, zatiaľ čo hárok, ktorý vyvolá cestu unicode písma, dostane podmnožinovanie automaticky, bez akejkoľvek vlastnosti na nastavenie a bez samostatného volania, a jediná zapojená vlastnosť, UnicodeFontFile, iba volí, ktoré písmo sa vloží a podmnožinuje, nie to, či podmnožinovanie prebehne

Podmnožinovanie písiem je jeden detail vnútri širšej plochy exportu do PDF v komponente HotXLS Delphi Excel, spolu s paginovaním, metadátami tlače hárka a exportnými cestami CSV, HTML a RTF, ktoré sa s ním dodávajú