HotXLS, компонент Excel для Delphi и C++Builder, сокращает размер встроенного шрифта PDF через субсеттинг шрифтов TrueType: в момент экспорта в PDF он вызывает функцию CreateFontPackage системной библиотеки Windows fontsub.dll, чтобы перестроить встроенный шрифт TrueType вокруг только тех кодовых точек Unicode, которые реально использовал лист, вместо того чтобы поставлять весь файл гарнитуры целиком. Отчёту с двумя сотнями строк китайских названий товаров может понадобиться всего несколько сотен различных иероглифов хань, тогда как шрифты CJK, поставляемые с Windows, обычно весят от 5 до 20 МБ каждый. Встройте один такой целиком — и один только шрифт может перевесить все остальные объекты в PDF вместе взятые
fontsub.dll — библиотека, о которой большинство разработчиков на Delphi никогда не слышали, и тому есть причина: Microsoft поставляет её как небольшую, скудно задокументированную служебную DLL, а не как флагманский API Win32. HotXLS относится к ней как к опциональной возможности, а не как к жёсткой зависимости, так что то, как экспортёр её загружает, вызывает и откатывается в случае её отсутствия, говорит о защитном программировании под Windows не меньше, чем о форматах шрифтов, и обе половины этой истории стоит рассмотреть
Почему юникодный текст раздувает экспорт HotXLS в PDF?
Экспортёр PDF в HotXLS обращается к встроенному шрифту TrueType только тогда, когда текст листа выходит за пределы WinAnsi, а в остальное время остаётся на встроенном семействе Helvetica — путь по умолчанию, подробно описанный в руководстве по экспорту листа в PDF. WinAnsi покрывает западноевропейский текст достаточно хорошо, так что множество книг вообще никогда не запускают встраивание шрифта: PDF просто ссылается на Helvetica по имени, а программа чтения предоставляет её локально, так что файл остаётся небольшим. В момент, когда ячейка содержит что-то, что WinAnsi не может представить, — китайское название товара, корейскую заметку, случайный символ в комментарии, — экспортёру приходится встраивать настоящую программу шрифта, потому что у программы чтения PDF нет запасного источника глифов для символов вне стандартных 14 шрифтов
HotXLS автоматически находит этот шрифт, сканируя папку шрифтов Windows на предмет короткого списка установленных кандидатов, включая CJK-совместимые гарнитуры, которые Windows поставляет для отрисовки китайского и корейского языков, если только свойство UnicodeFontFile экспортёра уже не указывает на конкретный файл, и какой бы шрифт ни был выбран, он встраивается целиком ещё до того, как вообще запустится субсеттинг. Это требование встраивания специфично для PDF: пути экспорта в RTF и HTML в HotXLS сохраняют юникодный текст нетронутым, экранируя кодовые точки прямо в поток байтов, а не поставляя программу шрифта, поэтому проблема размера, описываемая в этой статье, не имеет аналога для этих двух форматов
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 строит список сохранения для субсеттинга из карты, которую он уже вёл по другой причине, так что этот учёт не стоит ничего дополнительно. Каждый раз, когда код отрисовки страницы рисует символ, требующий встроенного юникодного шрифта, он находит индекс глифа этого символа и записывает пару в 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-битных слов, он чисто адресует основную многоязычную плоскость (Basic Multilingual Plane), которая без осложнений покрывает обычный 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, тогда как лист, действительно запускающий путь юникодного шрифта, получает субсеттинг автоматически, без свойства для установки и без отдельного вызова, а единственное задействованное свойство, UnicodeFontFile, лишь выбирает, какой шрифт будет встроен и субсеттирован, а не то, произойдёт ли субсеттинг вообще
Субсеттинг шрифтов — одна деталь внутри более широкой поверхности экспорта в PDF компонента Excel для Delphi HotXLS, наряду с постраничной разбивкой, метаданными печати листа и путями экспорта в CSV, HTML и RTF, с которыми он поставляется