Tehnički članak

Neugrađeni PDF fontovi kroz sistemske fontove u Delphiju

Kada PDF ne ugradi font, HotPDF komponenta taj tekst renderuje instaliranim Windows fontom koji odabere HPDFMapBaseFontToSystem: dekoduje /BaseFont ime, odseca deo sa stilom, proba više pravopisa dok GDI ne potvrdi da je porodica instalirana, izmeri nedostajuće širine standard 14 sa metrički kompatibilnih fontova i pretvori jednobajtne kodove u Unicode pre crtanja. Svaki od tih koraka postoji jer je naivna verzija padala na pravim fajlovima. RenderLoadedPageToBitmap page renderer ugrađene programe rešava dobro; ovo je priča o fontovima kojih u fajlu uopšte nema

Zašto GDI tiho crta pogrešno pismo za neugrađeni font?

GDI nikada ne prijavi font koji nedostaje: dajte CreateFontIndirect-u ime pisma koje ne poznaje i on tiho odabere zamenu, često drugi serif bez bold težine. Raniji renderer prosleđivao je PDF ime skoro doslovno, pa TimesNewRoman,Bold, TimesNewRomanPS-BoldMT i SegoeUI-Semibold nisu poklopili ništa i izašli su u onome što bi GDI izabrao. Imena mogu biti i gora. ISO 32000-1 §7.3.5 dopušta da ime upiše bilo koji bajt kao #xx, i CJK proizvođači rutinski ispisuju imena fontova kao escapovani UTF-8 ili bajtove nasleđene kodne strane; pre v2.766.69 sami escape-ovi postali su ime pisma. HPDFMapBaseFontToSystem sada prvo dekoduje escape-ove, ispravan UTF-8 bajt niz vraća kao svoje znakove, a ostale visoke bajtove čita u sistemskoj kodnoj strani

Odsecanje stila je mesto gde heuristike grizu. Zarez uvek zatvara porodicu (Arial,Bold daje Arial), ali crtica to čini samo kad je reč posle nje stil: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra ili Condensed. To pravilo MS-Mincho čuva celim dok Calibri-Light pretvara u Calibri. HotPDF zatim proba pravopise porodice, pa pravopise porodice bez PSMT, MT ili PS sufiksa. Od v2.768.18 ti pravopisi pokrivaju svaki izbor razmaka na mestima gde reč može početi: ispred velikog slova koje prati malo slovo (MyriadPro postaje Myriad Pro), na poslednjem velikom slovu niza za kojim ide malo slovo (UIGothic), i posle vodećeg MS-a (MSPGothic), od potpuno razmaknutog oblika do imena kakvo je upisano; iznad četiri takva mesta probaju se samo potpuno razmaknuto i sastavljeno ime. Razmaci se ne mogu slepo ubacivati, jer Windows neke reči drži sastavljene: SimSun je instaliran pod baš tim pravopisom, dok MicrosoftYaHei, MicrosoftJhengHei i MSPGothic pripadaju Microsoft YaHei, Microsoft JhengHei i MS PGothic. Pre v2.768.18 maper je stavljao razmak ispred svakog unutrašnjeg velikog slova, pa se MicrosoftYaHei tražio kao Microsoft Ya Hei i nikada nije nađen. Kandidat se računa kao instaliran kad CreateFontIndirect pa GetTextFace vrate traženo ime ili, od v2.768.18, kad name tabela odabranog fonta izlista to ime kao porodicu, punu ili tipografsku, na bilo kom jeziku; odgovor se kešira po imenu, pa dokumenti sa mnogo neinstaliranih fontova više ne ispitaju Windows za svako ime na svakoj stranici

HotPDF cevovod koji renderuje neugrađene PDF fontove sistemskim fontovima u Delphiju: HPDFMapBaseFontToSystem dekoduje #xx escapovane bajtove u /BaseFont imenu, skida Bold, Italic i Light stil sufikse uz MS-Mincho sačuvano celo, gradi kandidat pravopise poput Myriad Pro i Microsoft YaHei, i prihvata jednog samo kad GetTextFace ili name tabela fonta potvrdi instalirano ime
GDI nikada ne prijavi font koji nedostaje, on tiho zameni — mapiranje proba kandidate redom i veruje samo imenu koje GDI vrati ili koje name tabela odabranog fonta izlista

Pošto je funkcija mapiranja javna u HPDFRenderFontMetrics jedinici, preflight izveštaj može da pokaže kojim instaliranim pismom će se svaki neugrađeni font renderovati, 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 meri fontove standard 14 koji nemaju /Widths?

HotPDF meri nedostajuća napredovanja na instaliranom fontu sa istim metrikama, jer ISO 32000-1 §9.6.2.2 dopušta fontovima standard 14 da izostave /Widths, a biblioteka ne isporučuje AFM tabele. Arial nosi Helvetica metrike, Times New Roman nosi Times a Courier New nosi Courier, pa HPDFMeasureBaseFontWidths kreira odgovarajuće pismo na lfHeight = -1000 i zove GetCharWidth32W; na toj visini rezultat je već u 1/1000 em jedinicama koje PDF širine koriste. Renderer prvo svaki kod pretvara u Unicode kroz /Encoding, /BaseEncoding i /Differences, uz podrazumevani StandardEncoding. SVG izvoz i izvlačenje teksta udaraju u još jednu zamku: standardni Type 1 font bez ikakvog /Encoding-a proizveo je dekoder bez informacija o enkodovanju, SVG izvoz ga nikada nije registrovao, i svaka izmerena širina ostala je neiskorišćena. Dodela impliciranog StandardEncoding-a to je popravila, uz uslov da se označi kao predefinisano enkodovanje; rutiranje kroz CMap-ime putanju dekoduje svaki kod kao 0 i svaka širina sledi

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

/Flags unos font deskriptora broji bitove od 1, ne od 0, pa je ForceBold bit 19 ($40000) a Italic bit 7 ($40), po ISO 32000-1 tabeli 123. Stari kod testirao je $20000, što je bit 18, SmallCap. Greška je preživela od v2.345.0 do v2.766.53 jer je /FontDescriptor skoro uvek indirektna referenca a graditelj fonta čitao je samo direktne objekte, pa ceo flag grana nikada nije proradila, i ista slepila je ignorišila /Widths 12 0 R i slagala tekst na rezervno napredovanje od 500 jedinica. Kad je v2.766.53 počela kroz renderer da razrešava indirektne reference, bit je morao da se ispravi u istoj izmeni, inače bi svako small-caps pismo odjednom bilo iscrano bold:

const
  // ISO 32000-1 tabela 123 broji bit 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;
Numerisanje bitova /Flags unosa PDF font deskriptora: ISO 32000-1 tabela 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 HotPDF-ov deskriptorski test $20000 gađao SmallCap i bio bezopasan samo dok se indirektne /FontDescriptor reference nikada nisu razrešavale
Flag grana bila je mrtav kod četrdeset verzija jer je deskriptor bio indirektan — čim su se reference razrešile, off-by-one bit postao je vidljiv small-caps tekst

Zašto CJK fontovi sa UCS2 CMap-om dobijaju pogrešne širine?

CJK tekst sa predefinisanim UCS2 CMap-om crta prave glifove ali pogrešan razmak kad renderer tretira kod kao CID, jer je /W indeksiran CID-om, a ne znakovnim kodom. Sa STSong-Light i UniGB-UCS2-H, kod slučajno jednak je Unicode vrednosti, pa GDI crta ispravne znakove a bug se krije u napredovanjima: mala slova stižu kao kodovi 97 i više, ispadaju van /W unosa poput [1 95 500], i svi dobiju podrazumevanu širinu /DW od 1000. Od v2.766.56 HotPDF renderer čita kodove kroz CMap codespace opsege (ISO 32000-1 §9.7.6.2) i mapira ih u CID-ove pre traženja širina. Koriste se samo ugrađene UCS2 i UTF16 tabele i ugrađeni CMap streamovi; identitetska aproksimacija za nešto poput GBK-EUC-H tek bi izgledala podržana uz pogrešan izlaz, pa se renderer ne pretvara

Zašto CJK tekst sa UCS2 CMap-om crta prave glifove na pogrešnim napredovanjima: /W je indeksiran CID-om dok su kodovi Unicode vrednosti, pa sa STSong-Light i UniGB-UCS2-H mala slova od koda 97 naviše promašuju /W unos [1 95 500] i uzimaju /DW podrazumevanu, ispravljeno u HotPDF-u mapiranjem kodova u CID-ove kroz CMap codespace opsege
Bug se krije jer je kod ovde jednak Unicode-u — glifovi izgledaju ispravno dok svako napredovanje tiho pada na podrazumevano, pa CJK render procenjujte po razmaku, ne po oblicima

Zašto znakovi sa akcentima postaju znakovi pitanja na kineskom Windowsu?

Jednobajtni kodovi nikada ne smeju stići do ANSI („A“) GDI funkcija, jer GetGlyphOutlineA i GetGlyphIndicesA tumače bajtove u sistemskoj kodnoj strani dok TextOutA koristi znakovni skup odabranog fonta. Na kineskom sistemu, Arial bajt $A9 (znak autorskog prava u Windows-1252) postao je GBK vodeći bajt i renderovao se kao „?“, zamka u koju je pravo ušao put neohinčanih kontura dodat u v2.766.83. v2.767.3 pita realizovani font za njegov znakovni skup preko GetTextCharset-a, pretvara ga u kodnu stranu kroz TranslateCharsetInfo, provlači bajt kroz MultiByteToWideChar i zove W funkcije; simbolički fontovi umesto toga koriste U+F000 plus kod. Enkodovanja koja se ne slažu sa Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — mapiraju se u Unicode pre nego što bilo koji sistemski font vidi

Koje su granice crtanja sistemskim fontovima?

Renderovanje sistemskim fontovima je aproksimacija, i HotPDF komponenta je iskrena oko toga gde staje. Pre v2.768.18 provera instaliranosti poredila je samo ime koje vrati GetTextFace, a na lokalizovanom Windowsu ta funkcija javlja ime porodice na jeziku sistema, pa je Microsoft YaHei na kineskom Windowsu ili Yu Mincho na japanskom proglašavan nedostajućim i crtan je GDI zamenom; od v2.768.18 pismo koje se vrati pod drugim imenom traži se i u name tabeli fonta, i takvi fontovi se pronalaze. Metrička kompatibilnost garantovana je samo za porodice Helvetica, Times i Courier; Symbol se mapira na Symbol a ZapfDingbats na Wingdings, što je krpa a ne poklapanje. Preflight iznad vidi i samo fontove u /Resources rečniku svake stranice, ne i one referencirane iz unutrašnjosti form XObjecta. Kad se kod i dalje ne može iscrtati, praćenje nerešenih glifova u trenutku crtanja to prijavi, što je bolji signal nego gledanje sličica po očima

Trajnija popravka sedi na strani autorstva. HotPDF sam piše sa FontEmbedding postavljenim na True po podrazumevanoj vrednosti, ugrađujući Arial zamenika čak i kad kod zove SetFont sa Helvetica, a ugrađeni tekst prolazi kroz renderer glifova ugrađenih fontova umesto kroz bilo koje nagađanje iznad. Jeftina odbrana za dolazne fajlove je upozorenje pre renderovanja kad mapirana porodica nije u listi ekranskih fontova:

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

Za kompletnu komponentu, uključujući renderovanje stranica, izvlačenje teksta i font subsetting na strani upisa, pogledajte HotPDF Delphi PDF komponentu na stranici proizvoda