HotXLS, komponent Excel dla Delphi i C++Buildera, ogranicza rozmiar osadzonych czcionek PDF przez tworzenie podzbiorów czcionek TrueType: w chwili eksportu do PDF wywołuje funkcję CreateFontPackage z systemowej biblioteki Windows fontsub.dll, żeby odbudować osadzoną czcionkę TrueType wokół wyłącznie tych punktów kodowych Unicode, które arkusz faktycznie wykorzystał, zamiast wysyłać cały plik kroju pisma. Raport z dwustu wierszami chińskich nazw produktów może potrzebować zaledwie kilkuset odrębnych znaków chińskich, a jednak czcionki CJK dostarczane przez Windows zwyczajowo ważą od 5 do 20 MB każda. Osadź jedną w całości, a sama czcionka może przeważyć wszystkie pozostałe obiekty w PDF razem wzięte
fontsub.dll to biblioteka, o której większość programistów Delphi nigdy nie słyszała, i jest ku temu powód: Microsoft dostarcza ją jako małą, słabo udokumentowaną narzędziową bibliotekę DLL, a nie sztandarowe API Win32. HotXLS traktuje ją jako opcjonalną możliwość, a nie twardą zależność, więc sposób, w jaki eksporter ją wczytuje, wywołuje i cofa się, gdy jej brakuje, mówi tyle samo o defensywnym programowaniu w Windows, co o formatach czcionek, i obie połowy tej historii warto przejść
Dlaczego tekst Unicode rozdyma eksport PDF w HotXLS?
Eksporter PDF w HotXLS sięga po osadzoną czcionkę TrueType tylko wtedy, gdy tekst arkusza wykracza poza WinAnsi, a pozostaje przy wbudowanej rodzinie Helvetica przez resztę czasu — domyślną ścieżkę szczegółowo opisuje przewodnik po eksporcie arkusza do PDF. WinAnsi pokrywa tekst zachodnioeuropejski na tyle dobrze, że mnóstwo skoroszytów nigdy w ogóle nie wyzwala osadzenia czcionki: PDF po prostu odwołuje się do Helveticy po nazwie, a czytnik dostarcza ją lokalnie, więc plik pozostaje mały. W chwili gdy komórka zawiera coś, czego WinAnsi nie potrafi wyrazić — chińską nazwę produktu, koreańską notatkę, zabłąkany symbol w komentarzu — eksporter musi osadzić rzeczywisty program czcionki, ponieważ czytnik PDF nie ma zapasowego źródła glifów dla znaków spoza czternastu standardowych czcionek
HotXLS lokalizuje tę czcionkę automatycznie, skanując folder Czcionki Windows w poszukiwaniu krótkiej listy zainstalowanych kandydatów, w tym krojów pisma obsługujących CJK, jakie Windows dostarcza do renderowania chińskiego i koreańskiego, chyba że właściwość eksportera UnicodeFontFile już wskazuje na konkretny plik, a niezależnie od tego, na jakiej czcionce się zatrzyma, zostaje ona osadzona w całości, zanim w ogóle uruchomi się tworzenie podzbioru. Ten wymóg osadzania jest specyficzny dla PDF: ścieżki eksportu RTF i HTML w HotXLS zachowują tekst Unicode nienaruszony, uciekając punkty kodowe do strumienia bajtów zamiast wysyłać program czcionki, dlatego problem rozmiaru opisany w tym artykule nie ma odpowiednika w tych dwóch formatach
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;
Czym jest fontsub.dll i dlaczego nie napisać narzędzia do tworzenia podzbiorów od zera?
fontsub.dll to niewielka biblioteka systemowa Windows, dostarczana od czasów Windows XP, która udostępnia jedną istotną tu funkcję: CreateFontPackage. Podaj jej bajty źródłowej czcionki TrueType oraz listę punktów kodowych Unicode do zachowania, a zwróci minimalną czcionkę, która wciąż spełnia każde ograniczenie formatu czcionek: indeksy glifów przenumerowane, glyf i loca odbudowane wokół wyłącznie zachowanych konturów, hmtx i cmap przepisane, żeby pasowały. HotXLS deklaruje typ wskaźnika funkcji bezpośrednio zgodnie z tym kontraktem
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;
Napisanie zadania CreateFontPackage ręcznie zamiast jej wywołania oznaczałoby zaimplementowanie poprawnego narzędzia do tworzenia podzbiorów TrueType: przechodzenie po glifach złożonych, żeby wciągnąć każdy glif składowy, do którego odwołuje się zachowywany glif, odbudowanie przesunięć loca po odrzuceniu konturów, respektowanie bitów uprawnień osadzania w tabeli OS/2 czcionki oraz poprawne wykonanie tego wszystkiego na jakichkolwiek dziwacznych czcionkach, jakie akurat są zainstalowane na komputerze klienta. Microsoft już rozwiązał ten problem i dostarcza rozwiązanie jako część samego Windows, więc wywołanie systemowej biblioteki DLL, którą utrzymuje, testuje względem własnego stosu renderowania czcionek i rozprowadza na każdy komputer za darmo, kosztuje HotXLS dynamiczne wczytanie i wskaźnik funkcji; reimplementacja tej samej logiki oznaczałaby posiadanie na własność parsera dla formatu binarnego z dekadami przypadków brzegowych, dla funkcji, która ma znaczenie tylko wtedy, gdy czcionka akurat jest duża
Budowanie listy zachowania z glifów faktycznie wyrenderowanych
HotXLS buduje listę zachowania do tworzenia podzbioru z mapy, którą i tak już utrzymywał z innego powodu, więc księgowanie nic dodatkowo nie kosztuje. Za każdym razem, gdy kod renderujący stronę rysuje znak wymagający osadzonej czcionki Unicode, wyszukuje indeks glifu tego znaku i zapisuje parę w FUnicodeGlyphMap, tabeli glif-na-punkt-kodowy, która steruje też mapą PDF ToUnicode, dzięki czemu kopiowanie i wklejanie z gotowego dokumentu zwraca oryginalny tekst, a nie surowe identyfikatory glifów. Zanim strumienie treści stron są gotowe, ta mapa już wylicza dokładnie zbiór punktów kodowych Unicode, jakich użył dokument, ani więcej, ani mniej
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;
W chwili finalizacji HotXLS przechodzi przez tę samą mapę drugi raz, żeby zbudować listę zachowania oczekiwaną przez CreateFontPackage, zwykłą tablicę punktów kodowych Unicode do zachowania w 16-bitowej formie wymaganej przez argument listy zachowania API. Ponieważ ten argument to tablica 16-bitowych słów, adresuje ona czysto Podstawową Płaszczyznę Wielojęzyczną, co pokrywa zwykły tekst CJK, cyrylicę, grekę i arabski bez komplikacji; arkusz opierający się na znakach z płaszczyzn uzupełniających, pewnych emoji albo rzadkich pismach historycznych, znajduje się poza tym, co pojedynczy wpis listy zachowania może nazwać bezpośrednio, co jest granicą wartą poznania, a nie usterką, ponieważ zdecydowana większość biznesowych arkuszy kalkulacyjnych intensywnie korzystających z Unicode nigdy nawet nie zbliża się do tej płaszczyzny
Co się dzieje, gdy brakuje fontsub.dll?
HotXLS nigdy nie zakłada, że fontsub.dll jest obecna, a eksport PDF nigdy nie zawodzi z jej powodu. Biblioteka jest wczytywana dynamicznie w chwili, gdy potrzebny jest podzbiór, przez SafeLoadLibrary i GetProcAddress, a nie statyczny import, właśnie dlatego, że fontsub.dll nie jest udokumentowanym, gwarantowanym publicznym API tak, jak jest nim kernel32.dll: to dołączone narzędzie do osadzania czcionek, i nic w kontrakcie Microsoftu nie obiecuje, że przetrwa na każdym SKU, każdej gałęzi serwisowej ani każdej warstwie zgodności próbującej emulować 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żda ścieżka niepowodzenia zwija się z powrotem do tego samego wyniku. Brakująca biblioteka DLL, brakujący eksport, niezerowy kod zwrotny albo czcionka, której tabela OS/2 zakazuje tworzenia podzbiorów przez swoje bity uprawnień osadzania — HotXLS po prostu zachowuje pełną czcionkę, którą już osadził, i kontynuuje. Nic nie zgłasza wyjątku, nic nie przerywa eksportu, a kod wywołujący nigdy nie musi opakowywać optymalizacji czcionki we własną obsługę wyjątków; wyeksportowany PDF jest ważny w obu przypadkach, a jedyną zmienną jest to, czy wyjdzie mały, czy nieco większy
O ile faktycznie mniejszy staje się PDF?
Tworzenie podzbiorów czcionek TrueType w HotXLS zwykle zmniejsza wyeksportowany PDF arkusza intensywnie korzystającego z Unicode do gdzieś między jedną dwudziestą a jedną ósmą jego nieograniczonego rozmiaru, redukcję 8- do 20-krotną, której skala śledzi to, jak dużej części pełnej czcionki dany dokument faktycznie dotyka: zamówienie zakupu zbudowane wokół kilkuset odrębnych chińskich znaków zachowuje tylko te kilkaset glifów spośród dziesiątek tysięcy, jakie wysyła krój pisma CJK, podczas gdy arkusz obejmujący szerszą mieszankę znaków zachowuje proporcjonalnie więcej. HotXLS nakłada dodatkowy przebieg kompresji Flate na bajty podzbioru czcionki, zanim zapisze je do strumienia PDF /FontFile2, tę samą kompresję, przez którą przechodzi już reszta strumieni treści dokumentu, i nic z tego nie wymaga niczego dodatkowego od kodu wywołującego: arkusz, który nigdy nie wychodzi poza WinAnsi, nigdy nie dotyka tej ścieżki i nadal eksportuje przez zwykłą Helveticę, podczas gdy arkusz, który wyzwala ścieżkę czcionki Unicode, automatycznie otrzymuje tworzenie podzbioru, bez właściwości do ustawienia i bez osobnego wywołania do wykonania, a jedyna zaangażowana właściwość, UnicodeFontFile, wybiera tylko, która czcionka zostaje osadzona i poddana tworzeniu podzbioru, a nie czy tworzenie podzbioru w ogóle zachodzi
Tworzenie podzbiorów czcionek to jeden szczegół w szerszej powierzchni eksportu PDF komponentu Excel HotXLS dla Delphi, obok paginacji, metadanych druku arkusza oraz ścieżek eksportu CSV, HTML i RTF, z jakimi jest dostarczany