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

Рендиране на вградени PDF шрифтове в Delphi с HotPDF

HotPDF рендира вградени PDF шрифтове в Delphi, без да инсталира нищо на машината: вграденият тръбопровод за рендиране на глифове (glyph rendering pipeline) в HPDFGlyphRender.pas анализира програмите за шрифтове, съхранени в самия PDF файл – TrueType glyf контури от FontFile2, CFF Type 2 символни низове (charstrings) от FontFile3 и потоци от съдържание на глифове от тип 3 – и ги възпроизвежда как запълнени векторни пътеки на GDI. Тази статия е подробно техническо изследване на точността на шрифтовете зад рендирането на PDF страници в TBitmap с HotPDF: онази статия покрива визуализатора като цяло, а тази описва как текстът на тези страници получава точните си форми

Защо PDF файл се рендира с квадратчета вместо с текст?

Квадратчета, празни пространства или леко сгрешени символи в рендирания PDF резултат почти винаги означават, че визуализаторът е поискал шрифт от операционната система, вместо да използва вградения във файла. Оплакването пристига по един и същ начин всеки път: документът изглежда перфектно на машината, която го е създала, но след това клиент го отваря на празен сървър или защитен десктоп и японската фактура показва квадратчета („тофу“), или заменен подобен шрифт измества всяко прекъсване на ред. Шрифтовете никога не са били на тази машина – те са само в PDF файла – и визуализатор, който спира до замяна на системните шрифтове, не може да ги види. Подмножествата от шрифтове (subset fonts) влошават ситуацията: едно подмножество може да носи четиридесет глифа под кодове на знаци, присвоени частно само за този конкретен файл – асоциация, която никой инсталиран шрифт не споделя

Стандартът ISO 32000-1 §9.9 дефинира три носителя за вградена програма за шрифт в дескриптора на шрифта: FontFile държи оригинална програма от тип 1, FontFile2 държи TrueType програма, а FontFile3 съдържа чист CFF (Type1C или CIDFontType0C) или OpenType обвивка. Четвърти вид – шрифтът от тип 3 съгласно ISO 32000-1 §9.6.5 – изобщо не вгражда нищо двоично: всеки глиф е малък поток от PDF съдържание, изпълняван на място. Трите носителя се различават по математиката на контурите (квадратични B-сплайни срещу кубични символни низове срещу произволни оператори на страници), така че точният визуализатор се нуждае от отделен интерпретатор за всеки от тях, плюс кодиращ слой, който превръща кодовете на знаци в правилния индекс на глиф, преди да бъде докоснат какъвто и да е контур

Как HotPDF преобразува TrueType glyf контури в GDI пътища?

THPDFEmbeddedTTF в HPDFGlyphRender.pas чете таблицата loca, за да намери записа на всеки глиф, обхожда контурите glyf точка по точка и генерира GDI път. Две TrueType конвенции се нуждаят от изрична обработка. Първо, последователни точки извън кривата (off-curve) предполагат точка върху кривата (on-curve) в тяхната средна точка, а контур, чиито точки са изцяло извън кривата, започва в средната точка на последната и първата си точка – ако пропуснете някое от тези правила, заоблените глифове ще получат плоски повърхности или ще се сринат. Второ, TrueType кривите са квадратични на Безие, докато GDI функцията PolyBezierTo приема кубични криви, така че всеки квадратичен сегмент се повдига по степен точно, вместо да се начупва на линейни сегменти

Повдигането на степента е без загуба на качество: кубичната крива описва идентична крива, така че рендираният контур съвпада с това, което стандартен четец изчертава от същата таблица при всяко мащабиране. Останалата работа е позиционирането. Всеки глиф е създаден в единици за шрифт (обикновено мрежа от 1000 или 2048 единици на em), а визуализаторът композира матрицата на мащабиране, матрицата на текста и текущата матрица на трансформация в една обща трансформация от глиф към устройство, преди пътят да бъде запълнен. Подредбата тук е по-важна, отколкото изглежда: композирайте същите три матрици в обратен ред и всеки глиф ще се свие към началото на координатната система – дефект, водещ до празна страница, чийто скрит бъг е просто един ред с грешна матрична алгебра

Как интерпретаторът на Type 2 charstring обработва CFF шрифтове

THPDFEmbeddedCFF предоставя на програмите в FontFile3 истински интерпретатор на Type 2 charstring: той анализира CFF INDEX структурите, Top DICT и Private DICT, след което изпълнява всеки символен низ и изпраща сегментите от пътя директно към GDI. Първо се премахва OpenType обвивката (контейнерът OTTO), за да се достигне до чистата CFF таблица; чистите CIDFontType0C и Type1C потоци се консумират директно. Символните низове са компактен стеков език и три от неговите конвенции определят дали интерпретаторът ще остане синхронизиран с байтовия поток. Незадължителният префикс за ширина означава, че първият изчистващ стека оператор може да носи един допълнителен водещ операнд. Операторът hintmask предполага vstemhm, когато операндите все още са в стека, а броят на байтовете с маска за пропускане зависи от натрупания брой стъбла (stem count) – сгрешете броя веднъж и всеки следващ код на операция (opcode) ще бъде прочетен грешно. И извикванията на подпрограми добавят отместване (bias) към своя индекс (107, 1131 или 32768 в зависимост от броя на подпрограмите) преди търсенето, така че извикване без такова отместване попада на напълно грешна подпрограма

CFF с CID ключове добавя едно ниво на косвеност, което затруднява по-простите имплементации: кодът на знака избира CID, но индексът на символния низ е GID, а кодовата таблица (charset) на шрифта съпоставя GID към CID – така че визуализаторът изгражда обратното съпоставяне CID-към-GID преди чертане и избира Private DICT за всеки глиф чрез FDSelect за шрифтове, които съдържат няколко такива. Програмите Type1C с имеви ключове – обичайният носител за прости шрифтове Type 1 – вместо това разрешават еднобайтови кодове чрез вграденото кодиране на програмата CFF или чрез механизма за кодиране на ниво PDF, описан по-нататък. Едно честно предупреждение: интерпретаторът чете операторите за хинтинг (hint), за да поддържа потока синхронизиран, но не изпълнява самото подсказване (hinting) – граница, обсъдена в края

Какво представлява шрифтът от тип 3 и как се изчертава?

Глифът от тип 3 изобщо не е контур – ISO 32000-1 §9.6.5 го дефинира като поток от съдържание, така че HotPDF го рендира чрез вмъкване на графичното състояние, композиране на матрицата на шрифта, размера на шрифта и текстовата матрица върху CTM и изпълнение на процедурата за глиф през същия интерпретатор на оператори, който чертае страниците, със собствените ресурси /Resources на шрифта в обхват. Две подробности от спецификацията са важни за коректността. Ширините /Widths на тип 3 се изразяват в пространството на глифа, а не в текстовото пространство 1/1000, което използва всеки друг тип шрифт, така че отместванията трябва да преминат през /FontMatrix – шрифтовете за баркодове с матрица 0.01 иначе правят грешни стъпки с цял порядък. И процедура за глиф, която започва с оператора d1, прави две обещания, които визуализаторът налага: изчертаването се изрязва по декларираната ограничаваща кутия (bounding box) и според ISO 32000-1 §9.6.5.2 глифът игнорира собствените си оператори за цвят и се изчертава с текущия цвят за запълване на извикващия, така че операторите rg, g, k и техните съответствия за очертаване вътре в процедурата се потискат за времето на този глиф. Пропуснете правилото за цвета и шрифт за баркод d1, подпечатан в синьо от страницата, ще излезе черен; пропуснете изрязването и деформираният глиф ще излезе извън клетката си

Как кодовете на знаци стават идентификатори на глифове (Glyph IDs)

Интерпретаторите на контури са само половината от работата, тъй като байтовете в текстов низ в PDF са кодове на знаци, а не индекси на глифове, и ISO 32000-1 посвещава два подраздела на това съпоставяне. За прости шрифтове §9.6.6 предписва строг приоритет: масивът /Differences пренаписва базовото кодиране (WinAnsiEncoding, MacRomanEncoding или StandardEncoding), което от своя страна пренаписва собствената таблица на програмата за шрифтове. HotPDF превръща тази верига в таблица от код-към-GID с 256 записа, превеждайки имената на глифове в индекси на глифове по три начина: точно съвпадение на charset вътре в CFF програма, числови имена като gNN/glyphNN, приети като буквени индекси, и превод по Adobe Glyph List от име към Unicode, последван от търсене в cmap за TrueType програми. За композитни шрифтове §9.7 поставя CIDToGIDMap като водещ: най-честият случай е /Identity, но записът може да бъде поток от двойки в формат big-endian, индексирани по CID – а собственият Unicode изход на HotPDF използва точно тази форма на поток за компактни подмножества, така че този път не е екзотично изключение

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Когато е необходимо търсене в TrueType cmap, HotPDF преминава през верига от резервни варианти, вместо да се доверява на една подтаблица: подтаблиците на Windows Unicode (формат 4, след това формат 12 за допълнителни равнини) са на първо място, след това е подтаблицата за символи (3,0) с нейната конвенция за частна област на използване F000, огледално отразена до младшия байт – причината символен шрифт като Wingdings да отговаря на обикновени ASCII кодове – а след това са остарелите формати 6 и 0. Подтаблиците от формат 2 умишлено не се интерпретират: те картографират стари многобайтови кодови страници като Shift-JIS и Big5, а не Unicode, а съвременните CJK шрифтове във всеки случай носят подтаблица формат 4 или формат 12. Всеки код, който не съвпада с нито един от тези пътища, се връща към GDI изчертаване за този конкретен глиф, така че един некартографируем символ влошава качеството само на един глиф, а не на целия текстов масив

Какво НЕ прави вграденият път за рендиране

Ограниченията си струва да се кажат ясно. Хинтингът (hinting) не се изпълнява – контурите се запълват точно така, както са създадени, което е неразличимо от изход с хинтинг при 150 DPI и повече, но може да се различава с един пиксел при много малки размери. Оригиналните програми от тип 1 във FontFile (eexec-криптирани символни низове) не се интерпретират, както и не се прилагат осите на OpenType вариативните шрифтове; и в двата случая, както при повредена програма за шрифт или таблица glyf без използваема cmap, визуализаторът се връща към изчертаване със системен шрифт, вместо да срине рендирането на страницата. Същият подход „точността на първо място“ се прилага и другаде във визуализатора – аксиалните и радиалните шаблони за засенчване получават същата обработка, каквато заслужават градиентите – а генериращата страна има свои собствени тънкости при шрифтовете в начина, по който EndDoc подрежда подмножествата от шрифтове

Използването на тръбопровода не изисква никакъв специфичен код – всеки механизъм по-горе се задейства автоматично при извикване на рендирането

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Практическото следствие е онова, за което най-много се интересува екипът по поддръжка: PDF файл, който носи своите шрифтове, се рендира с тези шрифтове на сървър за изграждане, в Windows контейнер или на клиентски компютър, който никога не е виждал съответния шрифт. Вграденият механизъм за изчертаване на глифове се доставя като част от HotPDF Component за Delphi и C++Builder – библиотека, покриваща създаване на PDF, редактиране, извличане на текст и рендиране на страници без външни DLL зависимости