Tehnički članak

Neugrađeni PDF fontovi i sistemski fontovi u Delphiju

Kad PDF ne ugradi font, HotPDF komponenta renderira taj tekst instaliranim Windows fontom kojeg bira HPDFMapBaseFontToSystem: dekodira ime /BaseFonta, odreže dio sa stilom, isprobava više pravopisa dok GDI ne potvrdi da je obitelj instalirana, mjeri nedostajuće širine standardnih 14 s metrički kompatibilnih fontova i pretvara jednobajtne kodove u Unicode prije crtanja. Svaki od tih koraka postoji jer je naivna verzija padala na stvarnim datotekama. Page renderer RenderLoadedPageToBitmap dobro snalazi s ugrađenim programima; ovo je priča o fontovima kojih u datoteci uopće nema

Zašto GDI tiho crta krivo slovno lice za neugrađeni font?

GDI nikad ne javi nedostajući font: uručite CreateFontIndirectu ime lica koje ne poznaje i on tiho odabere zamjenu, često drugi serif bez bold težine. Rani je renderer predavao PDF ime gotovo doslovno, pa su se TimesNewRoman,Bold, TimesNewRomanPS-BoldMT i SegoeUI-Semibold svi poklopili s ničim i izašli u onome što je GDI izabrao. Imena mogu biti i gora. ISO 32000-1 §7.3.5 dopušta imenu da bilo koji bajt napiše kao #xx, i CJK proizvođači redovito pišu imena fontova kao escapirani UTF-8 ili legacy code-page bajtove; prije v2.766.69 same su escape sekvence postale ime lica. HPDFMapBaseFontToSystem sada najprije dekodira escape sekvence, vraća valjanu UTF-8 bajt sekvencu kao svoje znakove i čita svaki drugi visoki bajt u code pageu sustava

Odsijecanje stila je mjesto gdje heuristike grizu. Zareza uvijek završava obitelj (Arial,Bold daje Arial), ali crtica to čini samo kad je riječ iza nje stil: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra ili Condensed. To pravilo drži MS-Mincho na okupu dok Calibri-Light pretvara u Calibri. HotPDF zatim isprobava pravopise obitelji, pa pravopise obitelji s uklonjenim sufiksom PSMT, MT ili PS. Od v2.768.18 ti pravopisi pokrivaju svaki izbor razmaka na mjestima gdje riječ može početi: ispred velikog slova koje slijedi malo slovo (MyriadPro postaje Myriad Pro), na zadnjem velikom slovu niza kojim slijedi malo slovo (UIGothic), i iza vodećeg MSa (MSPGothic), od potpuno razmaknutog oblika do imena kako je zapisano; iza četiriju takvih mjesta isprobavaju se samo potpuno razmaknuto i slijepljeno ime. Razmaci se ne mogu primjenjivati naslijepo, jer Windows neke riječi drži slijepljenima: SimSun je instaliran pod upravo tim pravopisom, dok MicrosoftYaHei, MicrosoftJhengHei i MSPGothic pripadaju Microsoft YaHei, Microsoft JhengHei i MS PGothic. Prije v2.768.18 mapper je stavljao razmak ispred svakog unutarnjeg velikog slova, pa se MicrosoftYaHei tražio kao Microsoft Ya Hei i nikad nije nađen. Kandidat se računa kao instaliran kad CreateFontIndirect iza kojeg slijedi GetTextFace vrati traženo ime ili, od v2.768.18, kad name tablica odabranog fonta izlistava ime kao obitelj, puno ili tipografsko ime obitelji na bilo kojem jeziku; odgovor se cacheira po imenu, pa dokumenti s mnoštvom neinstaliranih fontova više ne ispiju Windows za svako ime na svakoj stranici

HotPDF pipeline koji renderira neugrađene PDF fontove sistemskim fontovima u Delphiju: HPDFMapBaseFontToSystem dekodira #xx escapirane bajtove u imenu /BaseFonta, odreže stil sufikse Bold, Italic i Light uz MS-Mincho ostavljen cijelim, gradi kandidatske pravopise poput Myriad Pro i Microsoft YaHei, i prihvaća jednog samo kad GetTextFace ili name tablica fonta potvrdi instalirano ime
GDI nikad ne javi nedostajući font, tiho zamijeni — mapiranje isprobava kandidate po redu i vjeruje samo imenu koje GDI vrati ili koje name tablica odabranog fonta izlistava

Pošto je funkcija mapiranja javna u jedinici HPDFRenderFontMetrics, preflight izvješće može prikazati s kojom će instaliranom obitelji svaki neugrađeni font renderirati, koristeći enumeraciju fontova koju THotPDF već izlaže za učitane dokumente:

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;

Kako HotPDF mjeri standardne 14 fontova bez /Widthsa?

HotPDF mjeri nedostajuće pomake na instaliranom fontu s istim metrikama, jer ISO 32000-1 §9.6.2.2 dopušta standardnim 14 fontovima da izostave /Widths, a biblioteka ne isporučuje AFM tablice. Arial nosi metrike Helvetica, Times New Roman nosi Times i Courier New nosi Courier, pa HPDFMeasureBaseFontWidths stvara pripadajuće lice na lfHeight = -1000 i zove GetCharWidth32W; na toj visini rezultat je već u 1/1000 em jedinicama koje PDF širine koriste. Renderer najprije pretvara svaki kod u Unicode kroz /Encoding, /BaseEncoding i /Differences, uz zadanu StandardEncoding. SVG izvoz i izvlačenje teksta naišli su na još jednu zamku: standardni Type 1 font potpuno bez /Encodinga proizveo je dekoder bez informacije o kodiranju, SVG izvoz ga nikad nije registrirao, i svaka izmjerena širina otišla je neupotrijebljena. Dodavanje podrazumijevane StandardEncoding to je popravilo, uz uvjet da se označi kao predefinirano kodiranje; usmjeravanje kroz put CMap imena dekodira svaki kod kao 0 i svaka širina ih prati

Bold, italic i off-by-one u flagovima font deskriptora

Unos /Flags font deskriptora broji svoje bitove od 1, ne od 0, pa je ForceBold bit 19 ($40000) a Italic bit 7 ($40), po ISO 32000-1 Tablici 123. Stari je kod testirao $20000, što je bit 18, SmallCap. Pogreška je preživjela od v2.345.0 do v2.766.53 jer je /FontDescriptor gotovo uvijek indirektna referenca, a fontovski graditelj čitao je samo direktne objekte, pa se cijela grana flagova nikad nije ni vrtjela, i ista je sljepoća ignorirala /Widths 12 0 R i posložila tekst na fallback pomak od 500 jedinica. Čim je v2.766.53 počela razrješivati indirektne reference kroz renderer, bit se morala ispraviti u istoj izmjeni, inače bi svako small-caps lice odjednom renderiralo boldom:

const
  // ISO 32000-1 Tablica 123 broji bitne pozicije od 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, nije težina
  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;
Numeriranje bitova unosa /Flags PDF font deskriptora: ISO 32000-1 Tablica 123 broji od bita 1, pa je Italic $40 na bitu 7, SmallCap $20000 na bitu 18 i ForceBold $40000 na bitu 19, pa je deskriptorski test HotPDF-a od $20000 gađao SmallCap i ostao bezazlen samo dok se indirektne reference /FontDescriptora nikad nisu razrješivale
Grana flagova bila je mrtav kod četrdeset verzija jer je deskriptor bio indirektan — čim su se reference razriješile, off-by-one bit postao je vidljiv small-caps tekst

Zašto CJK fontovi s UCS2 CMapom dobivaju krive širine?

CJK tekst s predefiniranim UCS2 CMapom crta prave glifove ali krive razmake kad renderer tretira kod kao CID, jer je /W indeksiran CID-om, ne kodom znaka. S STSong-Light i UniGB-UCS2-H kod slučajno jednak je Unicode vrijednosti, pa GDI crta prave znakove i bug se krije u pomacima: mala slova stižu kao kodovi 97 i više, padaju izvan /W unosa poput [1 95 500], i svi dobiju zadanu širinu /DW od 1000. Od v2.766.56 HotPDF renderer čita kodove kroz CMap codespace raspone (ISO 32000-1 §9.7.6.2) i mapira ih u CID-ove prije traženja širina. Koriste se samo ugrađene UCS2 i UTF16 tablice i ugrađeni CMap streamovi; identitetska aproksimacija za nešto poput GBK-EUC-H samo bi izgledala podržano dok proizvodi pogrešan izlaz, pa se renderer ne pretvara

Zašto CJK tekst s UCS2 CMapom crta prave glifove na krivim pomacima: /W indeksiran je CID-om dok su kodovi Unicode vrijednosti, pa s STSong-Light i UniGB-UCS2-H mala slova od koda 97 naviše promašuju /W unos [1 95 500] i uzimaju /DW zadanu vrijednost, ispravljeno u HotPDF-u mapiranjem kodova u CID-ove kroz CMap codespace raspone
Bug se krije jer je kod ovdje jednak Unicode vrijednosti — glifovi izgledaju ispravno dok svaki pomak tiho pada na zadanu vrijednost, pa CJK renderiranje ocjenjujte po razmacima, ne po oblicima

Zašto se znakovi s naglascima pretvaraju u upitnike na kineskom Windowsu?

Jednobajtni kodovi nikad ne smiju stići do ANSI („A") GDI funkcija, jer GetGlyphOutlineA i GetGlyphIndicesA interpretiraju bajtove u code pageu sustava dok TextOutA koristi znakovni skup odabranog fonta. Na kineskom sustavu Arialov bajt $A9 (znak autorskih prava u Windows-1252) postao je GBK vodeći bajt i renderirao se kao „?", zamka u koju je put bez hintanih oblika dodan u v2.766.83 ugazio ravno. v2.767.3 pita realizirani font za njegov znakovni skup s GetTextCharsetom, pretvara ga u code page kroz TranslateCharsetInfo, provlači bajt kroz MultiByteToWideChar i zove W funkcije; simbolni fontovi umjesto toga koriste U+F000 plus kod. Kodiranja koja se ne slažu s Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — mapiraju se u Unicode prije nego bilo koji sistemski font ih vidi

Koje su granice crtanja sistemskim fontovima?

Renderiranje sistemskim fontovima aproksimacija je, i HotPDF komponenta je iskrena oko toga gdje staje. Prije v2.768.18 provjera instalacije uspoređivala je samo ime koje vrati GetTextFace, a na lokaliziranom Windowsu ta funkcija javlja ime obitelji u jeziku sustava, pa je Microsoft YaHei na kineskom Windowsu ili Yu Mincho na japanskom ocijenjen nedostajućim i crtan GDI zamjenom; od v2.768.18 lice koje se vrati pod drugim imenom traži se i u name tablici fonta, i takvi se fontovi pronalaze. Metrička je kompatibilnost zajamčena samo za obitelji Helvetica, Times i Courier; Symbol mapira na Symbol a ZapfDingbats na Wingdings, što je privremeno rješenje više no poklapanje. Preflight gore vidi i samo fontove u rječniku /Resources svake stranice, ne one referencirane iz unutrašnjosti form XObjecta. Kad se kod i dalje ne može nacrtati, praćenje neriješenih glifova u trenutku crtanja to javi, što je bolji signal od gledanja sličica

Trajni je ispravak na strani autorstva. HotPDF sam piše s FontEmbedding postavljenim na True po zadanom, zamjenjujući ugrađeni Arial i kad kod zove SetFont s Helveticom, a ugrađeni tekst ide kroz render glifova ugrađenih fontova umjesto kroz bilo koju od gore navedenih nagađanja. Jeftina zaštita za dolazne datoteke jest upozoriti prije renderiranja kad mapirana obitelj nije u listi screen fontova:

// VCL: Screen.Fonts izlistava instalirana imena obitelji (jedinica Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Za punu komponentu, uključivo renderiranje stranica, izvlačenje teksta i font subsetting na strani pisanja, vidite HotPDF Delphi PDF komponentu na stranici proizvoda