Odborný článok

Nevložené PDF fonty vykresľované systémovými fontmi v Delphi

Keď PDF nevloží font, komponent HotPDF vykreslí ten text inštalovaným Windows fontom vybraným cez HPDFMapBaseFontToSystem: dekóduje názov /BaseFont, odstrihne časť so štýlom, skúša niekoľko hláskovaní, kým GDI potvrdí, že rodina je nainštalovaná, meria chýbajúce šírky štandardnej 14-ky z metricky kompatibilných fontov a pred kreslením prevádza jednobajtové kódy na Unicode. Každý z týchto krokov existuje preto, lebo naivná verzia na reálnych súboroch zlyhala. Page renderer RenderLoadedPageToBitmap zvláda vložené programy dobre; toto je príbeh fontov, ktoré v súbore vôbec nie sú

Prečo GDI potichu nakreslí nesprávny rez písma pre nevložený font?

GDI nikdy nehlási chýbajúci font: podajte CreateFontIndirect názov rezu, ktorý nepozná, a potichu vyberie náhradu, často iné serifo písanie bez tučného rezu. Skorý renderer podával PDF názov takmer slovo od slova, takže TimesNewRoman,Bold, TimesNewRomanPS-BoldMT aj SegoeUI-Semibold nič nechytili a vyšli v tom, čo GDI vybral. Názvy môžu byť ešte horšie. ISO 32000-1 §7.3.5 dovoľuje názvu zapísať ľubovoľný bajt ako #xx a CJK producenti bežne hláskujú názvy fontov ako escaped UTF-8 alebo bajty starej kódovej stránky; pred v2.766.69 sa samotné escape sekvencie stali názvom rezu. HPDFMapBaseFontToSystem teraz najprv dekóduje escape sekvencie, vráti platnú UTF-8 bajtovú sekvenciu ako svoje znaky a ostatné vysoké bajty číta v systémovej kódovej stránke

Odstrihnutie štýlu je miesto, kde hryzú heuristiky. Čiarka vždy ukončí rodinu (Arial,Bold dá Arial), ale pomlčka len vtedy, keď slovo za ňou je štýl: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra či Condensed. To pravidlo nechá MS-Mincho celé, zatiaľ čo z Calibri-Light spraví Calibri. HotPDF potom skúša hláskovania rodiny a za nimi hláskovania rodiny s odstránenou príponou PSMT, MT či PS. Od v2.768.18 tie hláskovania pokrývajú každú voľbu medzier na miestach, kde môže začínať slovo: pred veľkým písmenom nasledujúcim po malom (MyriadPro sa stane Myriad Pro), na poslednom veľkom písmene behu, za ktorým nasleduje malé (UIGothic), a po úvodnom MS (MSPGothic), od úplne rozmedzenej podoby až po názov tak, ako je zapísaný; za štyrmi takými miestami sa skúša len úplne rozmedzená a spojená podoba. Medzery sa dať slepo nedajú, lebo Windows niektoré slová drží spojené: SimSun je nainštalovaný presne pod tým hláskovaním, zatiaľ čo MicrosoftYaHei, MicrosoftJhengHei a MSPGothic patria k Microsoft YaHei, Microsoft JhengHei a MS PGothic. Pred v2.768.18 dával mapper medzeru pred každé vnútorné veľké písmeno, takže MicrosoftYaHei sa hľadal ako Microsoft Ya Hei a nikdy sa nenašiel. Kandidát sa počíta ako nainštalovaný, keď CreateFontIndirect nasledované GetTextFace vráti požadovaný názov alebo, od v2.768.18, keď tabuľka name vybraného fontu uvádza ako rodinu, plnú či typografickú rodinu v ľubovoľnom jazyku; odpoveď sa cacheuje podľa názvu, takže dokumenty s mnohými nenainštalovanými fontmi už nepybajú Windows pod každý názov na každej strane

Pipeline HotPDF, ktorá vykresľuje nevložené PDF fonty systémovými fontmi v Delphi: HPDFMapBaseFontToSystem dekóduje escaped bajty #xx v názve /BaseFont, odstrihne prípony štýlov Bold, Italic a Light, pritom nechá MS-Mincho celé, postaví kandidátske hláskovania ako Myriad Pro a Microsoft YaHei a prijme jedno len vtedy, keď GetTextFace alebo tabuľka názvov fontu potvrdí nainštalovaný názov
GDI nikdy nehlási chýbajúci font, potichu ho nahradí — mapovanie skúša svojich kandidátov v poradí a verí len názvu, ktorý GDI podá späť alebo ktorý uvádza tabuľka názvov vybraného fontu

Keďže mapovacia funkcia je verejná v jednotke HPDFRenderFontMetrics, preflight report dokáže ukázať, s ktorou nainštalovanou rodinou sa každý nevložený font vykreslí, a to pomocou enumerácie fontov, ktorú THotPDF už vystavuje pre načítané dokumenty:

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;

Ako HotPDF meria štandardné 14 fontov bez /Widths?

HotPDF meria chýbajúce posuny na nainštalovanom fonte s rovnakými metrikami, lebo ISO 32000-1 §9.6.2.2 dovoľuje štandardným 14 fontom vynechať /Widths a knižnica nepridáva žiadne AFM tabuľky. Arial nesie metriky Helvetica, Times New Roman nesie Times a Courier New nesie Courier, takže HPDFMeasureBaseFontWidths vytvorí zodpovedajúci rez pri lfHeight = -1000 a zavolá GetCharWidth32W; v tej výške je výsledok už v jednotkách 1/1000 em, ktoré PDF šírky používajú. Renderer najprv prevedie každý kód na Unicode cez /Encoding, /BaseEncoding a /Differences s predvoleným StandardEncoding. SVG export a extrakcia textu narazili na ďalšiu pascu: štandardný font Type 1 bez akéhokoľvek /Encoding vyrobil dekodér bez informácie o kódovaní, SVG export ho nikdy nezaregistroval a každá nameraná šírka odišla nazmar. Doplnenie implikovaného StandardEncoding to opravilo, pokiaľ je označené ako preddefinované kódovanie; nasmerovanie na cestu názvov CMap dekóduje každý kód ako 0 a každá šírka po nej ide zle

Bold, kurzíva a off-by-one vo flagoch font deskriptoru

Záznam /Flags font deskriptoru čísluje svoje bity od 1, nie od 0, takže ForceBold je bit 19 ($40000) a Italic bit 7 ($40) podľa tabuľky 123 ISO 32000-1. Starý kód testoval $20000, čo je bit 18, SmallCap. Chyba prežila od v2.345.0 po v2.766.53 preto, lebo /FontDescriptor je takmer vždy nepriama referencia a staviteľ fontov čítal len priame objekty, takže celá vetva flagov sa nikdy nespustila, a tá istá slepotá ignorovala /Widths 12 0 R a vyskladala text so záložným posunom 500 jednotiek. Keď v2.766.53 začala riešiť nepriame referencie cez renderer, bit sa musel opraviť v tej istej zmene, inak by každý rez small-caps zrazu vykresľoval tučne:

const
  // ISO 32000-1 tabuľka 123 počíta bitové pozície od 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, nie váha
  FD_FORCEBOLD  = $40000;  // bit 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;
Číslovanie bitov záznamu /Flags PDF font deskriptoru: ISO 32000-1 tabuľka 123 počíta od bitu 1, takže Italic je $40 na bite 7, SmallCap $20000 na bite 18 a ForceBold $40000 na bite 19, takže test deskriptoru $20000 v HotPDF mieril na SmallCap a ostal neškodný len dovtedy, kým sa nepriame referencie /FontDescriptor nikdy neriešili
Vetva flagov bola mŕtvy kód štyridsať verzií, lebo deskriptor bol nepriamy — akonáhle sa referencie začali riešiť, bit posunutý o jednu sa stal viditeľným textom small-caps

Prečo majú CJK fonty s UCS2 CMap nesprávne šírky?

CJK text s preddefinovanou UCS2 CMap kreslí správne glyfy, ale nesprávne rozostupy, keď renderer berie kód ako CID, lebo /W je indexované CID, nie kódom znaku. Pri STSong-Light a UniGB-UCS2-H sa kód náhodou rovná Unicode hodnote, takže GDI kreslí správne znaky a chyba sa skrýva v posunoch: malé písmená prichádzajú ako kódy 97 a viac, padajú mimo záznamu /W ako [1 95 500] a všetky dostanú predvolenú šírku /DW 1000. Od v2.766.56 číta renderer HotPDF kódy cez rozsahy codespace CMap (ISO 32000-1 §9.7.6.2) a mapuje ich na CID skôr, než vyhľadáva šírky. Používajú sa len vstavané tabuľky UCS2 a UTF16 a vložené CMap streamy; identická aproximácia pre niečo ako GBK-EUC-H by len vyzerala podporovane pri výrobe zlého výstupu, takže renderer sa nehrá na niečo, čo nie je

Prečo CJK text s UCS2 CMap kreslí správne glyfy pri nesprávnych posunoch: /W je indexované CID, kým kódy sú Unicode hodnoty, takže pri STSong-Light a UniGB-UCS2-H malé kódy 97 a viac minú záznam /W [1 95 500] a dostanú predvolenú /DW, opravené v HotPDF mapovaním kódov na CID cez rozsahy codespace CMap
Chyba sa skrýva, lebo kód sa tu rovná Unicode — glyfy vyzerajú správne, kým každý posun sa potichu nastaví na predvolený, takže CJK vykresľovanie súďte podľa rozostupov, nie podľa tvarov

Prečo sa akcentované znaky menia na otázniky na čínskom Windows?

Jednobajtové kódy sa nikdy nesmú dostať k ANSI („A“) funkciám GDI, lebo GetGlyphOutlineA a GetGlyphIndicesA interpretujú bajty v systémovej kódovej stránke, zatiaľ čo TextOutA používa znakovú sadu vybraného fontu. Na čínskom systéme sa bajt $A9 fontu Arial (znak copyrightu vo Windows-1252) stal vedúcim bajtom GBK a vykreslil sa ako „?“, do tej pasce vyskočila rovno cesta nehintovaných obrysov pridaná vo v2.766.83. v2.767.3 sa spýta realizovaného fontu na jeho znakovú sadu cez GetTextCharset, prevedie ju cez TranslateCharsetInfo na kódovú stránku, preženie bajt cez MultiByteToWideChar a volá funkcie W; symbolové fonty používajú namiesto toho U+F000 plus kód. Kódovania nezhodné s Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — sa na Unicode mapujú skôr, než ich uvidí akýkoľvek systémový font

Aké sú hranice kreslenia systémovými fontmi?

Vykresľovanie systémovými fontmi je aproximácia a komponent HotPDF je úprimný v tom, kde končí. Pred v2.768.18 porovnávala kontrola inštalácie len názov, ktorý vráti GetTextFace, a na lokalizovanom Windows hlási tá funkcia názov rodiny v jazyku systému, takže Microsoft YaHei na čínskom Windows alebo Yu Mincho na japonskom Windows sa vyhlásilo za chýbajúce a kreslilo sa GDI náhradou; od v2.768.18 sa rez, ktorý sa vráti pod iným názvom, hľadá aj v tabuľke name fontu a také fonty sa nájdu. Metrická kompatibilita je garantovaná len pre rodiny Helvetica, Times a Courier; Symbol sa mapuje na Symbol a ZapfDingbats na Wingdings, čo je provizórium, nie zhoda. Preflight vyštie vidí len fonty v slovníku /Resources každej strany, nie tie referencované zvnútra form XObjects. Keď kód stále nie je možné nakresliť, nahlási to tracking nevyriešených glyfov pri kreslení, čo je lepší signál než obzeranie sa miniatúr

Trvalá oprava sedí na strane tvorby. Samotný HotPDF zapisuje s FontEmbedding nastaveným na True predvolene a dosadí vložený Arial, aj keď kód volá SetFont s Helvetica, a vložený text ide cez renderer glyfov vložených fontov namiesto ktoréhokoľvek z vyššie uvedeného tipovania. Lacná poistka pre prichádzajúce súbory je varovať pred vykreslením, keď mapovaná rodina nie je v zozname obrazových fontov:

// VCL: Screen.Fonts vypisuje nainštalované názvy rodín (jednotka Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Kompletný komponent, vrátane vykresľovania strán, extrakcie textu a font subsettingu na strane zápisu, nájdete na produktovej stránke HotPDF Delphi PDF component