HotXLS, компонент Excel для Delphi та C++Builder, скорочує розмір вбудованого шрифту PDF через субсетинг шрифтів TrueType: під час експорту в PDF він викликає функцію CreateFontPackage системної бібліотеки Windows fontsub.dll, щоб перебудувати вбудований шрифт TrueType лише навколо тих кодових точок Unicode, які справді використав аркуш, замість того щоб постачати весь файл гарнітури. Звіту з двомастами рядків китайських назв товарів може знадобитися лише кілька сотень окремих ієрогліфів хань, тоді як шрифти CJK, що постачаються з Windows, зазвичай важать по 5-20 МБ кожен. Вбудуйте один цілком — і сам лише шрифт може переважити всі інші об'єкти PDF разом узяті
fontsub.dll — не та бібліотека, про яку чув більшість розробників Delphi, і на це є причина: Microsoft постачає її як невелику, слабо задокументовану допоміжну DLL, а не флагманський Win32 API. HotXLS трактує її як опційну можливість, а не жорстку залежність, тож те, як експортер завантажує її, викликає й відкочується назад, коли її бракує, розповідає стільки ж про захисне програмування для Windows, скільки й про формати шрифтів, і обидві половини цієї історії варті детального розгляду
Чому текст Unicode роздуває експорт PDF у HotXLS?
Експортер PDF у HotXLS звертається до вбудованого шрифту TrueType лише тоді, коли текст аркуша виходить за межі WinAnsi, а решту часу залишається на вбудованій родині Helvetica — типовий шлях, який детально описує огляд експорту аркуша в PDF. WinAnsi достатньо добре покриває західноєвропейський текст, тож чимало книг узагалі ніколи не спричиняють вбудовування шрифту: PDF просто посилається на Helvetica за іменем, і читач надає її локально, тож файл залишається малим. Щойно клітинка містить щось, що WinAnsi не може представити, — китайську назву товару, корейську примітку, випадковий символ у коментарі, — експортеру доводиться вбудовувати справжню програму шрифту, бо в читача PDF немає запасного джерела гліфів для символів поза стандартними 14 шрифтами
HotXLS знаходить цей шрифт автоматично, скануючи папку шрифтів Windows на короткий список встановлених кандидатів, включно з гарнітурами, здатними на CJK, які Windows постачає для китайського та корейського відображення, якщо тільки властивість UnicodeFontFile експортера вже не вказує на конкретний файл, і на якому б шрифті він не зупинився, той вбудовується цілком ще до того, як узагалі спрацює субсетинг. Ця вимога вбудовування специфічна для PDF: шляхи експорту RTF і HTML у HotXLS зберігають текст Unicode цілим, екрануючи кодові точки в потік байтів, а не постачаючи програму шрифту, тому проблема розміру, яку розглядає ця стаття, не має аналога в цих двох форматах
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;
Що таке fontsub.dll і чому не написати субсетер із нуля?
fontsub.dll — невелика системна бібліотека Windows, що постачається з часів Windows XP, яка відкриває одну релевантну тут функцію: CreateFontPackage. Передайте їй байти вихідного шрифту TrueType та список кодових точок Unicode, які треба залишити, і вона поверне мінімальний шрифт, що все ще задовольняє кожне обмеження формату шрифту: перенумеровані індекси гліфів, glyf і loca, перебудовані лише навколо збережених контурів, переписані hmtx і cmap. HotXLS оголошує тип вказівника на функцію прямо за цим контрактом
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;
Написання роботи CreateFontPackage вручну замість її виклику означало б реалізацію коректного субсетера TrueType: обхід композитних гліфів, щоб підтягнути кожен компонентний гліф, на який посилається збережений гліф, перебудову зсувів loca після відкидання контурів, дотримання бітів дозволу на вбудовування в таблиці OS/2 шрифту, і все це правильно на будь-яких дивних шрифтах, які випадково встановлені на машині клієнта. Microsoft уже розв'язала цю проблему й постачає рішення як частину самої Windows, тож виклик системної DLL, яку вона підтримує, тестує проти власного стека рендерингу шрифтів і безкоштовно розповсюджує на кожну машину, коштує HotXLS одного динамічного завантаження й одного вказівника на функцію; повторна реалізація тієї самої логіки означала б володіти парсером бінарного формату з десятиліттями крайових випадків заради функції, яка важить лише тоді, коли шрифт виявляється великим
Побудова списку збереження з гліфів, що справді відрендерені
HotXLS будує список збереження для субсетингу з карти, яку вже підтримував з іншої причини, тож облік не коштує нічого додаткового. Щоразу, коли код рендерингу сторінки малює символ, що потребує вбудованого шрифту Unicode, він шукає індекс гліфа цього символу й записує пару в FUnicodeGlyphMap — таблицю гліф-до-кодової-точки, яка також керує CMap ToUnicode у PDF, щоб копіювання й вставлення з готового документа повертало оригінальний текст, а не сирі ідентифікатори гліфів. До того часу, коли потоки вмісту сторінки готові, ця карта вже перелічує саме той набір кодових точок Unicode, який використав документ, не більше й не менше
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;
На етапі завершення HotXLS обходить ту саму карту вдруге, щоб побудувати список збереження, якого очікує CreateFontPackage, — простий масив кодових точок Unicode для збереження у 16-бітній формі, якої вимагає аргумент списку збереження API. Оскільки цей аргумент — масив 16-бітних слів, він чисто адресує базову багатомовну площину (BMP), що покриває звичайний CJK-текст, кирилицю, грецьку та арабську без ускладнень; аркуш, що спирається на символи додаткових площин, певні емодзі чи рідкісні історичні писемності, опиняється поза тим, що може безпосередньо назвати один запис списку збереження, і це варто знати як межу, а не як дефект, оскільки переважна більшість ділових електронних таблиць із важким Unicode ніколи навіть не наближається до цієї площини
Що відбувається, коли fontsub.dll відсутня?
HotXLS ніколи не припускає, що fontsub.dll присутня, і експорт у PDF ніколи не провалюється через її відсутність. Бібліотека завантажується динамічно в момент, коли потрібен субсет, через SafeLoadLibrary і GetProcAddress, а не через статичний імпорт, саме тому, що fontsub.dll не є задокументованим, гарантовано присутнім публічним API так, як kernel32.dll: це вбудований інструментарій вбудовування шрифтів, і ніщо в контракті Microsoft не обіцяє, що він виживе на кожному SKU, кожній гілці обслуговування чи кожному шарі сумісності, що намагається емулювати 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;
Кожен шлях невдачі згортається до того самого результату. Відсутня DLL, відсутній експорт, ненульовий код повернення чи шрифт, чия таблиця OS/2 забороняє субсетинг через свої біти дозволу на вбудовування, — HotXLS просто залишає повний шрифт, який уже вбудував, і продовжує роботу. Ніщо не піднімає виняток, ніщо не перериває експорт, і коду, що викликає, ніколи не доводиться огортати оптимізацію шрифту власною обробкою винятків; експортований PDF дійсний в обох випадках, і єдина змінна — чи вийде він малим, чи дещо більшим
Наскільки справді меншим стає PDF?
Субсетинг шрифтів TrueType у HotXLS зазвичай скорочує експортований PDF аркуша з важким Unicode десь до одної двадцятої — однієї восьмої від нессубсетизованого розміру, зменшення у 8-20 разів, масштаб якого відстежує, наскільки повного шрифту насправді торкається даний документ: замовлення на закупівлю, побудоване навколо кількох сотень окремих китайських символів, зберігає лише ці кілька сотень гліфів із десятків тисяч, які постачає гарнітура CJK, тоді як аркуш, що охоплює ширшу суміш символів, зберігає пропорційно більше. HotXLS накладає ще один прохід стиснення Flate поверх байтів субсету шрифту перед записом їх у потік /FontFile2 PDF — те саме стиснення, через яке вже проходять потоки вмісту решти документа, — і жодна частина цього не вимагає нічого додаткового від коду, що викликає: аркуш, що ніколи не виходить за межі WinAnsi, ніколи не торкається цього шляху й продовжує експортуватися через звичайну Helvetica, тоді як аркуш, що справді спричиняє шлях шрифту Unicode, отримує субсетинг автоматично, без властивості для встановлення й без окремого виклику, а єдина залучена властивість, UnicodeFontFile, лише обирає, який шрифт буде вбудовано й субсетизовано, а не чи відбудеться субсетинг узагалі
Субсетинг шрифтів — одна деталь усередині ширшої поверхні експорту PDF у компоненті HotXLS Delphi Excel, поряд з пагінацією, метаданими друку аркуша та шляхами експорту в CSV, HTML і RTF, з якими він постачається