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

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

Когато PDF не вгражда шрифт, HotPDF компонентата рендира този текст с инсталиран Windows шрифт, избран от HPDFMapBaseFontToSystem: тя декодира името /BaseFont, отрязва стиловата част, пробва няколко изписвания, докато GDI потвърди, че семейството е инсталирано, мери липсващите ширини на стандартните 14 от метрично съвместими шрифтове и превръща еднобайтовите кодове в Unicode преди чертане. Всяка от тези стъпки съществува, защото наивната версия се проваляше на реални файлове. Page renderer-ът RenderLoadedPageToBitmap се справя добре с вградени програми; това е историята на шрифтовете, които изобщо не са във файла

Защо GDI тихо рисува грешния шрифт за невграден шрифт?

GDI никога не докладва липсващ шрифт: подайте на CreateFontIndirect име на шрифт, което не познава, и той тихо избере заместител — често друг serif без bold тежест. Ранният renderer подаваше PDF името почти дословно, така че TimesNewRoman,Bold, TimesNewRomanPS-BoldMT и SegoeUI-Semibold всички не съвпадаха с нищо и излизаха в каквото GDI беше избрало. Имена могат да са и по-лоши. ISO 32000-1 §7.3.5 позволява име да запише всеки байт като #xx, а CJK производители редовно изписват имена на шрифтове като екраниран UTF-8 или байтове от legacy code page; преди v2.766.69 самите екранирания ставаха името на шрифта. HPDFMapBaseFontToSystem вече декодира първо екраниранията, връща валидна UTF-8 байтова последователност като своите символи и чете всички други високи байтове в системния code page

Отрязването на стила е мястото, където хевристиката хапе. Запетая винаги приключва семейството (Arial,Bold дава Arial), но тире — само когато думата след него е стил: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra или Condensed. Това правило пази MS-Mincho цял, докато превръща Calibri-Light в Calibri. HotPDF после пробва изписвания на семейството, следвани от изписвания на семейството с махнат суфикс PSMT, MT или PS. От v2.768.18 тези изписвания покриват всеки избор на интервали на местата, където дума може да започне: пред главна буква, следвана от малка (MyriadPro става Myriad Pro), на последната главна на пасаж, следван от малка буква (UIGothic), и след водещо MS (MSPGothic), от напълно разредената форма до името, както е записано; след четири такива места се пробват само напълно разреденото и слепеното име. Разредяване не може да се прилага сляпо, защото Windows държи някои думи слепени: SimSun е инсталиран точно с тази изписка, докато MicrosoftYaHei, MicrosoftJhengHei и MSPGothic принадлежат на Microsoft YaHei, Microsoft JhengHei и MS PGothic. Преди v2.768.18 mapper-ът слагаше интервал пред всяка вътрешна главна буква, така че MicrosoftYaHei се търсеше като Microsoft Ya Hei и никога не се намираше. Кандидат се брои за инсталиран, когато CreateFontIndirect, последвано от GetTextFace, върне поисканото име, или — от v2.768.18 — когато name таблицата на избрания шрифт го изброява като семейство, пълно или типографско име на семейство на всеки език; отговорът се кешира на име, така че документи с много неинсталирани шрифтове вече не сондат Windows за всяко име на всяка страница

Pipeline-ът на HotPDF, който рендира невградени PDF шрифтове със системни шрифтове в Delphi: HPDFMapBaseFontToSystem декодира #xx екранирани байтове в името /BaseFont, отрязва стилови суфикси Bold, Italic и Light, докато пази MS-Mincho цял, строи кандидат изписвания като Myriad Pro и Microsoft YaHei и приема едно само когато GetTextFace или name таблицата на шрифта потвърди инсталираното име
GDI никога не докладва липсващ шрифт, той тихо замества — мапването пробва кандидатите си по ред и вярва само на име, което GDI върне или което name таблицата на избрания шрифт изброява

Понеже мапващата функция е публична в юнита HPDFRenderFontMetrics, preflight доклад може да покаже с кое инсталирано семейство ще се рендерира всеки невграден шрифт, ползвайки изброяването на шрифтове, което THotPDF вече излага за заредени документи:

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

Как HotPDF мери стандартните 14 шрифта, които нямат /Widths?

HotPDF мери липсващите advance-и на инсталирания шрифт със същите метрики, защото ISO 32000-1 §9.6.2.2 позволява на стандартните 14 шрифта да пропускат /Widths, а библиотеката не доставя AFM таблици. Arial носи метриките на Helvetica, Times New Roman носи Times, а Courier New носи Courier, така че HPDFMeasureBaseFontWidths създава съответното лице при lfHeight = -1000 и вика GetCharWidth32W; на тази височина резултатът вече е в 1/1000 em единиците, които PDF ширините ползват. Renderer-ът първо превръща всеки код в Unicode чрез /Encoding, /BaseEncoding и /Differences, с подразбиране StandardEncoding. SVG експортът и извличането на текст удрят още един капан: стандартен Type 1 шрифт без никакъв /Encoding произвеждаше декодер без информация за кодировка, SVG експортът никога не го регистрираше и всяка мерена ширина отиваше напразно. Поддането на подразбиращото се StandardEncoding го оправи, при условие че е маркирано като предварително дефинирана кодировка; маршрутизирането му по CMap-имения път декодира всеки код като 0 и всяка ширина тръгва след него

Bold, italic и отместване с едно във флаговете на font descriptor

Записът /Flags на font descriptor номерира битовете си от 1, не от 0, така че ForceBold е бит 19 ($40000), а Italic е бит 7 ($40), по ISO 32000-1 Table 123. Старият код тестваше $20000, което е бит 18, SmallCap. Грешката оцеля от v2.345.0 до v2.766.53, защото /FontDescriptor е почти винаги indirect референция, а font строителят четеше само директни обекти, така че целият клон с флагове изобщо не се пускаше, а същата слепота игнорираше /Widths 12 0 R и подреждаше текст с fallback advance от 500 единици. Щом v2.766.53 започна да разрешава indirect референции през renderer-а, битът трябваше да се поправи в същата промяна, иначе всяко лице с малки главни букви изведнъж щеше да се рендерира bold:

const
  // ISO 32000-1 Table 123 брои бит позиции от 1
  FD_ITALIC     = $00040;  // бит 7
  FD_SMALLCAP   = $20000;  // бит 18, не е тежест
  FD_FORCEBOLD  = $40000;  // бит 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Битова номерация на записа /Flags на PDF font descriptor: ISO 32000-1 Table 123 брои от бит 1, правейки Italic $40 на бит 7, SmallCap $20000 на бит 18 и ForceBold $40000 на бит 19, така че тестът $20000 на descriptor-а в HotPDF цели SmallCap и си оставаше безвреден, докато indirect /FontDescriptor референции изобщо не се разрешаваха
Клонът с флагове беше мъртъв код четиридесет версии, защото descriptor-ът беше indirect — щом референциите се разрешаваха, битът с отместване с едно ставаше видими малки главни букви в текста

Защо CJK шрифтове с UCS2 CMap получават грешните ширини?

CJK текст с предварително дефиниран UCS2 CMap рисува правилните глифи, но грешните разстояния, когато renderer третира кода като CID, защото /W е индексиран по CID, не по символен код. С STSong-Light и UniGB-UCS2-H кодът случайно съвпада със стойността Unicode, така че GDI рисува правилните знаци и бъгът се крие в advance-ите: малки букви пристигат като кодове 97 и нагоре, падат извън запис /W като [1 95 500] и всички получават подразбиращата се ширина /DW от 1000. От v2.766.56 HotPDF renderer-ът чете кодовете през codespace диапазоните на CMap (ISO 32000-1 §9.7.6.2) и ги мапва към CIDs, преди да търси ширини. Ползват се само вградените UCS2 и UTF16 таблици и вградени CMap stream-ове; identity приближение за нещо като GBK-EUC-H би изглеждало поддържано, докато произвежда грешен изход, така че renderer-ът не се преструва

Защо CJK текст с UCS2 CMap рисува правилните глифи на грешните advance-и: /W е индексиран по CID, докато кодовете са стойности Unicode, така че с STSong-Light и UniGB-UCS2-H малки кодове от 97 нагоре пропускат записа /W [1 95 500] и взимат подразбиращото се /DW, оправено в HotPDF с мапване на кодове към CIDs през codespace диапазоните на CMap
Бъгът се крие, защото тук кодът е равен на Unicode — глифите изглеждат правилни, докато всеки advance тихо поема подразбирането, затова съдете CJK рендирането по разстоянията, не по формите

Защо ударените знаци се превръщат във въпросителни на китайски Windows?

Еднобайтови кодове никога не бива да стигат до ANSI („A") GDI функциите, защото GetGlyphOutlineA и GetGlyphIndicesA интерпретират байтовете в системния code page, докато TextOutA ползва знаковия набор на избрания шрифт. На китайска система Arial байт $A9 (знакът за авторско право в Windows-1252) ставаше GBK водещ байт и се рендерираше като „?", капан, в който пътят с нехинтираните очертания, добавен в v2.766.83, влезе право. v2.767.3 пита реализирания шрифт за неговия знаков набор с GetTextCharset, превръща го в code page чрез TranslateCharsetInfo, прокарва байта през MultiByteToWideChar и вика W функциите; symbol шрифтове ползват U+F000 плюс кода вместо това. Кодировки, които не съвпадат с Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — се мапват към Unicode, преди някой системен шрифт да ги е видял

Какви са границите на чертането със системни шрифтове?

Рендирането със системни шрифтове е приближение и HotPDF компонентата е честна за това къде спира. Преди v2.768.18 инсталационната проверка сравняваше само името, което GetTextFace връща, а на локализиран Windows тази функция докладва името на семейството на системния език, така че Microsoft YaHei на китайски Windows или Yu Mincho на японски Windows се отсъждаха за липсващи и се чертаеха в GDI заместител; от v2.768.18 лице, върнало се под друго име, също се търси в name таблицата на шрифта и такива шрифтове се намират. Метричната съвместимост е гарантирана само за семействата Helvetica, Times и Courier; Symbol се мапва към Symbol, а ZapfDingbats към Wingdings, което е междинна кръпка, а не съвпадение. Preflight-ът по-горе вижда също само шрифтове в /Resources речника на всяка страница, не тези, сочени отвътре от form XObjects. Когато код пак не може да се нарисува, тракингът на нерешени глифи по време на чертане го докладва, което е по-добър сигнал от гледане на миниатюри с око

Трайната поправка е от страната на авторството. Самият HotPDF пише с FontEmbedding True по подразбиране, замествайки вграден Arial дори когато код вика SetFont с Helvetica, а вграденият текст минава през glyph renderer-а за вградени шрифтове вместо през някое от отгатванията по-горе. Евтина предпазна мярка за входящи файлове е да предупреждавате преди рендиране, когато мапнато семейство го няма в списъка на екранните шрифтове:

// VCL: Screen.Fonts изброява инсталирани имена на семейства (Forms unit)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

За пълната компонента — включително рендиране на страници, извличане на текст и font subsetting на страната на записа — вижте продуктовата страница HotPDF Delphi PDF компонента