HotXLS, komponenta Excelu pro Delphi a C++Builder, snižuje velikost vloženého fontu PDF pomocí subsettingu fontů TrueType: při exportu do PDF zavolá funkci CreateFontPackage ze systémové knihovny Windows fontsub.dll, aby přestavěl vložený font TrueType jen kolem těch bodů kódu Unicode, které list skutečně použil, místo aby odeslal celý soubor písma. Report s dvěma sty řádky čínských názvů produktů možná potřebuje jen pár set odlišných čínských znaků, přesto fonty CJK, které dodává Windows, běžně váží 5 až 20 MB kus. Vložte jeden celý a samotný font dokáže převážit každý jiný objekt v PDF dohromady
fontsub.dll není knihovna, o které by většina vývojářů v Delphi kdy slyšela, a má to svůj důvod: Microsoft ji dodává jako malé, řídce zdokumentované pomocné DLL, ne jako titulní Win32 API. HotXLS ji bere jako volitelnou schopnost, ne tvrdou závislost, takže to, jak ji exportér načítá, volá a jak se propadá do náhrady, když chybí, vypovídá o defenzivním programování ve Windows stejně tak jako o formátech fontů, a oba tyto poloviny příběhu se vyplatí projít
Proč unikódový text nafoukne export PDF v HotXLS?
Exportér PDF v HotXLS sáhne po vloženém fontu TrueType jen tehdy, když text listu vypadne mimo WinAnsi, a zbytek času zůstává na vestavěné rodině Helvetica, výchozí cestě, kterou podrobně popisuje průvodce exportem listu do PDF. WinAnsi pokrývá západoevropský text dost dobře na to, že spousta sešitů nikdy nevyvolá vložení fontu vůbec: PDF jen odkáže na Helvetiku jménem a čtenář ji dodá lokálně, takže soubor zůstává malý. V okamžiku, kdy buňka drží něco, co WinAnsi neumí reprezentovat — čínský název produktu, korejskou poznámku, zbloudilý symbol v komentáři — exportér musí vložit skutečný program fontu, protože čtenář PDF nemá žádný náhradní zdroj glyfů pro znaky mimo standardních 14 fontů
HotXLS tento font vyhledá automaticky, prohledá složku Windows Fonts na krátký seznam nainstalovaných kandidátů, včetně fontů schopných CJK, které Windows dodává pro vykreslování čínštiny a korejštiny, pokud už vlastnost exportéru UnicodeFontFile nemíří na konkrétní soubor, a ať už skončí na jakémkoli fontu, ten se před subsettingem vloží celý. Tento požadavek na vložení je specifický pro PDF: exportní cesty RTF a HTML v HotXLS udržují unikódový text neporušený tak, že body kódu escapují do bajtového proudu místo odesílání programu fontu, což je důvod, proč problém velikosti popisovaný v tomto článku nemá u těchto dvou formátů 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;
Co je fontsub.dll a proč si nenapsat subsetter od nuly?
fontsub.dll je malá systémová knihovna Windows, dodávaná od Windows XP, která vystavuje jednu funkci relevantní zde: CreateFontPackage. Podejte jí bajty zdrojového fontu TrueType a seznam bodů kódu Unicode k ponechání, a ona vrátí minimální font, který pořád splňuje každé omezení formátu fontu: přečíslované indexy glyfů, přestavěné glyf a loca jen kolem ponechaných obrysů, přepsané hmtx a cmap, aby odpovídaly. HotXLS deklaruje typ funkčního ukazatele přímo podle tohoto 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;
Napsat práci CreateFontPackage ručně místo jejího volání by znamenalo implementovat správný subsetter TrueType: procházet složené glyfy, aby se vtáhla každá komponentní glyfa, na kterou ponechaná glyfa odkazuje, přestavět posuny loca poté, co se obrysy zahodí, respektovat bity oprávnění vkládání v tabulce OS/2 fontu, a dostat to všechno správně napříč jakýmikoli podivnými fonty, které se zákazníkovi náhodou nainstalovaly na stroji. Microsoft tento problém už vyřešil a řešení dodává jako součást samotného Windows, takže volání systémové DLL, kterou udržuje, testuje proti vlastnímu zásobníku vykreslování fontů a distribuuje na každý stroj zdarma, stojí HotXLS dynamické načtení a ukazatel na funkci; přeimplementovat stejnou logiku by znamenalo vlastnit parser pro binární formát s desítkami let okrajových případů, kvůli funkci, na které záleží jen tehdy, když je font náhodou velký
Sestavení seznamu k ponechání z glyfů skutečně vykreslených
HotXLS staví seznam k ponechání pro subsetting z mapy, kterou už udržoval z jiného důvodu, takže účetnictví nestojí nic navíc. Pokaždé, když kód vykreslování stránky nakreslí znak, který potřebuje vložený unikódový font, vyhledá index glyfy tohoto znaku a zaznamená dvojici do FUnicodeGlyphMap, tabulky glyfa-na-bod-kódu, která také pohání PDF CMap ToUnicode, aby kopírování a vkládání z hotového dokumentu vrátilo původní text místo surových ID glyfů. V okamžiku, kdy jsou proudy obsahu stránek hotové, tato mapa už uvádí přesně tu množinu bodů kódu Unicode, kterou dokument použil, ani o víc, ani o míň
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;
Ve chvíli finalizace HotXLS projde tutéž mapu podruhé, aby sestavil seznam k ponechání, který CreateFontPackage očekává, obyčejné pole bodů kódu Unicode k zachování v 16bitové formě, kterou vyžaduje argument seznamu API. Protože je tento argument polem 16bitových slov, čistě adresuje Základní vícejazyčnou rovinu, což pokrývá obyčejný CJK text, cyrilici, řečtinu a arabštinu bez komplikací; list, který se opírá o znaky doplňkové roviny, určité emoji nebo vzácná historická písma, sedí mimo to, co dokáže jmenovat přímo jediný záznam seznamu k ponechání, což je hranice, kterou se vyplatí znát, ne vada, protože drtivá většina unikódově náročných business tabulek se k této rovině vůbec nepřiblíží
Co se stane, když fontsub.dll chybí?
HotXLS nikdy nepředpokládá, že je fontsub.dll přítomné, a export PDF nikdy neselže proto, že přítomné není. Knihovna se načítá dynamicky ve chvíli, kdy je subset potřeba, přes SafeLoadLibrary a GetProcAddress místo statického importu, přesně proto, že fontsub.dll není zdokumentované, zaručeně přítomné veřejné API tak, jako je kernel32.dll: je to přibalený nástroj pro vkládání fontů, a nic ve smlouvě Microsoftu neslibuje, že přežije na každém SKU, každé servisní větvi, nebo každé kompatibilní vrstvě, která se pokouší emulovat 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 selhání se sbalí do stejného výsledku. Chybějící DLL, chybějící export, nenulový návratový kód, nebo font, jehož tabulka OS/2 zakazuje subsetting přes své bity oprávnění vkládání — HotXLS prostě podrží celý font, který už vložil, a jde dál. Nic nevyvolá výjimku, nic export nepřeruší, a volající kód nikdy nemusí obalovat optimalizaci fontu vlastním zpracováním výjimek; exportované PDF je platné tak i tak, a jedinou proměnnou je, zda skončí malé, nebo poněkud větší
O kolik se PDF skutečně zmenší?
Subsetting fontů TrueType v HotXLS obvykle zmenší exportované PDF unikódově náročného listu na někde mezi dvacetinou a osminou jeho nesubsetované velikosti, redukci 8 až 20krát, jejíž rozsah sleduje, jak velké části celého fontu se daný dokument vůbec dotýká: objednávka postavená kolem pár set odlišných čínských znaků si ponechá jen těch pár set glyfů z desítek tisíc, které font CJK dodává, zatímco list rozprostírající se přes širší směs znaků si ponechá poměrně víc. HotXLS navrch bajtů subsetovaného fontu vrství další průchod komprese Flate ještě předtím, než je zapíše do proudu PDF /FontFile2, stejnou kompresi, kterou už procházejí proudy obsahu zbytku dokumentu, a nic z toho nežádá volající kód o nic navíc: list, který nikdy neopustí WinAnsi, se této cesty nikdy nedotkne a dál exportuje přes obyčejnou Helvetiku, zatímco list, který cestu unikódového fontu skutečně vyvolá, dostane subsetting automaticky, bez vlastnosti k nastavení a bez samostatného volání, a jediná zapojená vlastnost, UnicodeFontFile, jen vybírá, který font se vloží a subsetuje, ne zda k subsettingu dojde
Subsetting fontů je jeden detail uvnitř širší plochy exportu PDF v komponentě HotXLS Delphi Excel, spolu s paginací, metadaty tisku listu a exportními cestami CSV, HTML a RTF, se kterými se dodává