Техническа статия

Подмножество на TrueType шрифтове с fontsub.dll в Delphi

HotXLS, Excel компонентът за Delphi и C++Builder, намалява размера на вградените PDF шрифтове чрез подмножество на TrueType шрифта: при експортиране към PDF той извиква функцията CreateFontPackage от системната библиотека на Windows fontsub.dll, за да изгради наново вградения TrueType шрифт само с Unicode кодовите точки, които работният лист действително използва, вместо да включва целия файл на шрифта. Отчет с двеста реда китайски имена на продукти може да използва само няколкостотин различни хански знака, докато CJK шрифтовете, доставяни с Windows, обикновено заемат по 5 до 20 MB всеки. Вграждането на един цял шрифт може да го направи по-голям от всички останали обекти в 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 Fonts за кратък списък от инсталирани кандидати, включително CJK шрифтовете, които Windows доставя за визуализиране на китайски и корейски текст, освен ако свойството UnicodeFontFile на експортьора вече не сочи към конкретен файл, и избраният шрифт се вгражда изцяло, преди да се стартира подмножеството. Това изискване за вграждане е специфично за PDF: пътищата за експортиране към RTF и HTML на HotXLS запазват Unicode текста, като записват кодовите точки чрез escape последователности в байтовия поток, вместо да пренасят програма на шрифт, поради което проблемът с размера в тази статия няма еквивалент в тези два формата

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, която Microsoft поддържа, тества със собствения си стек за визуализиране на шрифтове и разпространява безплатно на всяка машина, струва на HotXLS динамично зареждане и указател към функция, докато повторното реализиране би означавало да поддържа парсер за двоичен формат с десетилетия натрупани крайни случаи за функция, която е важна само когато даден шрифт е голям

Изграждане на списъка за запазване от реално визуализираните глифове

HotXLS изгражда списъка за подмножество от карта, която вече поддържа по друга причина, така че отчитането не струва нищо допълнително. Всеки път, когато кодът за визуализиране на страницата изчертае знак, изискващ вградения Unicode шрифт, той намира индекса на глифа за този знак и записва съответствието в FUnicodeGlyphMap, таблица глиф–кодова точка, която управлява и PDF ToUnicode CMap, така че копирането и поставянето от готовия документ да връща оригиналния текст, а не необработени идентификатори на глифове. Когато потоците със съдържание на страниците са готови, тази карта вече съдържа точно набора от използвани в документа 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-битови думи, той обработва чисто Основната многоезична равнина, която обхваща обичайните CJK, кирилски, гръцки и арабски текстове без усложнения. Работен лист, който използва знаци от допълнителни равнини, например някои emoji или редки исторически писмености, излиза извън това, което един елемент от списъка може да назове директно, и това е граница, която е добре да познавате, а не дефект, тъй като огромното мнозинство от бизнес таблиците с много Unicode текст никога не стигат до тази равнина

Какво се случва, когато fontsub.dll липсва

HotXLS никога не приема, че fontsub.dll присъства, а PDF експортирането никога не се проваля заради липсата ѝ. Библиотеката се зарежда динамично в момента, в който е необходимо подмножество, чрез SafeLoadLibrary и GetProcAddress, а не чрез статичен импорт, именно защото fontsub.dll не е документиран, гарантирано наличен публичен API като kernel32.dll: тя е помощен инструмент за вграждане на шрифтове и договорът на Microsoft не обещава, че ще присъства във всяко издание, всеки клон за обслужване или всеки слой за съвместимост, който се опитва да емулира 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, липсващ export, ненулев код за връщане или шрифт, чиято таблица OS/2 забранява подмножеството чрез битовете за разрешение за вграждане, HotXLS просто запазва целия шрифт, който вече е вградила, и продължава. Нищо не хвърля изключение, експортирането не се прекъсва, а извикващият код не трябва да обвива оптимизацията на шрифта в собствена обработка на изключения. Експортираният PDF е валиден и в двата случая, като единствената разлика е дали ще бъде малък или малко по-голям

Колко реално се намалява размерът на PDF файла

Подмножеството на TrueType шрифтове в HotXLS обикновено намалява експортирания PDF на работен лист с много Unicode текст до между една двадесета и една осма от размера му без подмножество, тоест намаление от 8 до 20 пъти, чийто мащаб зависи от това каква част от пълния шрифт действително използва конкретният документ. Поръчка, съставена от няколкостотин различни китайски знака, запазва само тези няколкостотин глифа от десетките хиляди, които се доставят с CJK шрифта, докато лист с по-широк набор знаци запазва пропорционално повече. HotXLS прилага и допълнително Flate компресиране върху байтовете на шрифта след подмножеството, преди да ги запише в потока /FontFile2 на PDF, тоест същото компресиране, през което вече преминават останалите потоци със съдържание на документа, и нищо от това не изисква допълнителни действия от извикващия код. Работен лист, който никога не напуска WinAnsi, не докосва този път и продължава да се експортира чрез обикновен Helvetica, а лист, който задейства пътя с Unicode шрифт, получава подмножество автоматично, без свойство за настройване и без отделно извикване. Единственото свързано свойство, UnicodeFontFile, само избира кой шрифт да бъде вграден и подложен на подмножество, а не дали подмножеството да се изпълни

Подмножеството на шрифтове е един детайл от по-широката повърхност за PDF експортиране на компонента HotXLS Delphi Excel Component, заедно с пагинацията, метаданните за печат на работния лист и пътищата за експортиране към CSV, HTML и RTF