„HotPDF“ atvaizduoja įterptuosius PDF šriftus Delphi aplinkoje nieko nediegiant kompiuteryje: įterptųjų glifų atvaizdavimo konvejeris faile „HPDFGlyphRender.pas“ išanalizuoja pačiame PDF faile išsaugotas šriftų programas — „TrueType“ glifų kontūrus iš „FontFile2“, CFF Type 2 simbolių eilutes iš „FontFile3“ ir Type 3 glifų turinio srautus — bei atkuria juos kaip užpildytus GDI vektorinius kelius. Šis straipsnis yra išsamus šriftų atitikimo tyrimas, susijęs su PDF puslapių atvaizdavimu bitų žemėlapyje (TBitmap) naudojant HotPDF: anas straipsnis apžvelgia visą atvaizdavimo procesą, o šis nagrinėja, kaip tekstas tuose puslapiuose įgauna tikslią formą
Kvadratėliai (tofu), tušti tarpai arba nežymiai neteisingi simboliai atvaizduotame PDF faile beigeveik visada reiškia, kad atvaizdavimo variklis paprašė operacinės sistemos šrifto, užuot naudojęs įterptąjį faile. Skundas kiekvieną kartą būna vienodas: dokumentas atrodo tobulai kompiuteryje, kuriame buvo sukurtas, bet klientas atidaro jį švariame serveryje arba ribotame darbalaukyje ir sąskaitoje faktūroje pasirodo tušti kvadratėliai arba pakeistas panašus šriftas pastumia kiekvieną eilutės lūžį. Šriftų niekada nebuvo tame kompiuteryje — jie yra tik PDF faile — o atvaizdavimo variklis, kuris apsiriboja tik sisteminiu šrifto parinkimu, negali jų matyti. Daliniai šriftai (subset fonts) padėtį dar labiau apsunkina: toks šriftas gali turėti tik keturiasdešimt glifų, priskirtų privatiems kodams šiame faile, o tokio priskyrimo neturi joks įdiegtas šriftas
ISO 32000-1 §9.9 apibrėžia tris įterptosios šrifto programos nešėjus šrifto aprašyme: „FontFile“ saugo originalią Type 1 programą, „FontFile2“ — „TrueType“ programą, o „FontFile3“ — gryną CFF („Type1C“ arba „CIDFontType0C“) arba „OpenType“ apvalkalą. Ketvirtasis tipas — Type 3 šriftas pagal ISO 32000-1 §9.6.5 — išvis neįterpia jokių dvejetainių duomenų: kiekvienas glifas yra nedidelis PDF turinio srautas, vykdomas vietoje. Šie trys nešėjai skiriasi savo kontūro matematika (kvadratiniai B-splainai prieš kubines simbolių eilutes ir prieš savavališkus puslapio operatorius), todėl tiksliam atvaizdavimui reikalingas atskiras interpretatorius kiekvienam iš jų ir kodavimo sluoksnis, kuris paverčia simbolių kodus į teisingą glifo indeksą prieš paliečiant patį kontūrą
Kaip „HotPDF“ konvertuoja „TrueType“ glifų kontūrus į GDI kelius?
„THPDFEmbeddedTTF“ faile „HPDFGlyphRender.pas“ nuskaito „loca“ lentelę, kad surastų kiekvieno glifo įrašą, taškas po taško pereina „glyf“ kontūrus ir sukuria GDI kelią. Dvi „TrueType“ taisyklės reikalauja aiškaus apdorojimo. Pirma, vienas po kito einantys taškai ne ant kreivės (off-curve) reiškia tašką ant kreivės ties jų viduriu, o kontūras, kurio visi taškai yra ne ant kreivės, prasideda nuo paskutinio ir pirmojo taško vidurio — praleidus bet kurią taisyklę, suapvalinti glifai įgauna plokščias briaunas arba subyra. Antra, „TrueType“ kreivės yra kvadratinės Bezjė (Bézier) kreivės, o GDI „PolyBezierTo“ priima kubines, todėl kiekvienas kvadratinis segmentas tiksliai paverčiamas aukštesnio laipsnio segmentu (degree elevation), o ne susmulkinamas į tiesias atkarpas
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Laipsnio pakėlimas yra neprarandantis kokybės: kubinė Bezjė kreivė nubrėžia visiškai identišką kreivę, todėl atvaizduotas kontūras bet kokiu masteliu atitinka tai, ką iš tos pačios lentelės nupiešia standartinė peržiūros programa. Likęs darbas yra išdėstymas. Kiekvienas glifas yra aprašytas šrifto vienetais (paprastai 1000 arba 2048 vienetų tinklelyje), o atvaizdavimo variklis sujungia mastelio matricą, teksto matricą ir esamą transformacijos matricą į vieną glifo-įrenginio transformaciją prieš užpildant kelią. Matricų sujungimo tvarka čia yra itin svarbi: sujungus tas pačias tris matricas atvirkštine tvarka, visi glifai susitraukia link koordinačių pradžios — puslapis atrodo sugadintas, o tikroji klaida yra vos viena matricos algebros eilutė
Kaip Type 2 simbolių eilučių interpretatorius tvarko CFF šriftus
„THPDFEmbeddedCFF“ suteikia „FontFile3“ programoms tikrą Type 2 simbolių eilučių (charstring) interpretatorių: jis išanalizuoja CFF INDEX struktūras, „Top DICT“ bei „Private DICT“, o tada vykdo kiekvieną simbolių eilutę ir siunčia kelio segmentus tiesiai į GDI. „OpenType“ apvalkalas („OTTO“ konteineris) pirmiausia yra pašalinamas, kad būtų pasiekta gryna CFF lentelė; gryni „CIDFontType0C“ ir „Type1C“ srautai nuskaitomi tiesiogiai. Simbolių eilutės yra kompaktiška steko (stack) kalba, o trys jos taisyklės nulemia, ar interpretatorius išlaiko sinchronizaciją su baitų srautu. Pasirenkamas pločio priešdėlis reiškia, kad pirmasis steką išvalantis operatorius gali turėti vieną papildomą priekinį operandą. Operatorius „hintmask“ numato „vstemhm“, kai operandai vis dar yra steke, o praleidžiamų kaukės baitų skaičius priklauso nuo sukauptų stiebų (stem) skaičiaus — vieną kartą suklydus su skaičiumi, visi vėlesni kodai bus perskaityti klaidingai. O kreipimaisi į paprogrames (subroutine) prieš paiešką prideda poslinkį (bias) prie savo indekso (107, 1131 arba 32768, priklausomai nuo paprogramių skaičiaus), todėl kreipimasis be poslinkio pataiko į visiškai neteisingą paprogramę
CID raktu koduotas CFF prideda vieną netiesioginį kreipimąsi, kuris suklaidina paprastas realizacijas: simbolio kodas pasirenka CID, bet simbolių eilutės indeksas yra GID, o šrifto simbolių rinkinys susieja GID su CID — so atvaizdavimo variklis prieš piešdamas sukuria atvirkštinį CID-to-GID susiejimą, o šriftams, turintiems kelis žodynus, pasirenka konkrečiam glifui skirtą „Private DICT“ per „FDSelect“. Pavadinimais koduotos „Type1C“ programos — įprastas paprastų Type 1 šriftų nešėjas — vieno baito kodus išsprendžia per pačios CFF programos vidinį kodavimą arba per PDF lygio kodavimo mechanizmą, aprašytą toliau. Viena pastaba: interpretatorius skaito užuominų (hint) operatorius, kad išlaikytų srauto sinchronizaciją, bet pačių užuominų nevykdo (tai aptarta pabaigoje)
Kas yra Type 3 šriftas ir kaip jis piešiamas?
Type 3 glifas išvis nėra kontūras — ISO 32000-1 §9.6.5 apibrėžia jį kaip turinio srautą, todėl „HotPDF“ jį atvaizduoja išsaugodamas grafikos būseną, uždėdamas šrifto matricą, šrifto dydį ir teksto matricą ant CTM bei vykdydamas glifo procedūrą per tą patį operatorių interpretatorių, kuris piešia puslapius, naudojant paties šrifto „/Resources“. Dvi specifikacijos detalės yra svarbios teisingumui. Type 3 „/Widths“ (pločiai) išreiškiami glifo erdvėje, o ne 1/1000 teksto erdvėje, kurią naudoja visi kiti šriftų tipai, todėl poslinkiai turi praeiti per „/FontMatrix“ — brūkšninių kodų šriftai su 0.01 matrica kitaip pasislenka neteisingai visa eile. O glifo procedūra, kuri prasideda „d1“ operatoriumi, duoda du pažadus, kuriuos atvaizdavimo variklis privalo įgyvendinti: piešimas yra apribojamas (clipped) iki deklaruoto rėmelio (bounding box), ir pagal ISO 32000-1 §9.6.5.2 glifas nepaiso savo paties spalvų operatorių ir piešia su aktyvia iškvietėjo užpildymo spalva, todėl „rg“, „g“, „k“ ir kiti spalvinimo operatoriai procedūros viduje yra ignoruojami. Praleidus spalvos taisyklę, „d1“ brūkšninio kodo šriftas, puslapyje turėjęs būti mėlynas, bus nupieštas juodai; praleidus apribojimą, neteisingas glifas išsilies už savo langelio ribų
Kaip simbolių kodai tampa glifų ID
Kontūrų interpretatoriai yra tik pusė darbo, nes PDF teksto eilutės baitai yra simbolių kodai, o ne glifų indeksai, ir ISO 32000-1 tam skiria du poskyrius. Paprastiems šriftams §9.6.6 nurodo griežtą prioritetą: masyvas „/Differences“ perrašo bazinį kodavimą („WinAnsiEncoding“, „MacRomanEncoding“ arba „StandardEncoding“), kuris perrašo paties šrifto programos žemėlapį. „HotPDF“ išsprendžia šią grandinę į 256 įrašų kodų-GID lentelę, paversdama glifų pavadinimus glifų indeksais trimis būdais: tiksliu simbolių rinkinio atitikimu CFF programoje, skaitiniais pavadinimais „gNN“/„glyphNN“, laikomais tiesioginiais indeksais, ir „Adobe Glyph List“ pavadinimų vertimu į unikodą, po kurio seka „cmap“ paieška „TrueType“ programoms. Sudėtiniams šriftams §9.7 paveda atsakomybę „CIDToGIDMap“: įprastas atvejis yra „/Identity“, tačiau įrašas gali būti ir „big-endian“ baitų porų srautas, indeksuojamas pagal CID — o paties „HotPDF“ unikodo išvestis naudoja būtent šį srauto formatą kompaktiškiems daliniams šriftams
// /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
Kai reikalinga „TrueType“ „cmap“ paieška, „HotPDF“ eina per atsarginių lentelių grandinę, užuot pasitikėjęs viena lentele: pirmenybė teikiama „Windows Unicode“ lentelėms (4 formatas, tada 12 formatas papildomiems lygiams), po to seka (3,0) simbolių lentelė su savo „F000“ privačios srities taisykle, atspindima žemyn iki žemiausio baito — dėl šios priežasties simbolių šriftas, toks kaip „Wingdings“, atsako į paprastus ASCII kodus — tada seka senosios 6 ir 0 formatų lentelės. 2 formato lentelės sąmoningai neinterpretuojamos: jos susieja senus kelių baitų kodus (pvz., Shift-JIS ir Big5), o ne unikodą, o modernūs CJK šriftai bet kokiu atveju turi 4 arba 12 formato lentelę. Bet koks kodas, kuris neranda atitikmens nė viename iš šių būdų, sugrįžta prie GDI piešimo tam konkrečiam glifui, todėl vienas nesusietas simbolis sugadina tik vieną glifą, o ne visą teksto eilutę
Ko nedaro įterptasis kelias
Ribas verta įvardyti aiškiai. Užuominos (hinting) nevykdomos — kontūrai užpildomi taip, kaip aprašyti, kas nesiskiria nuo užuominas naudojančio vaizdo esant 150 DPI ir daugiau, tačiau gali skirtis vienu pikseliu esant labai mažiems dydžiams. Originalios Type 1 programos „FontFile“ faile (eexec šifruotos simbolių eilutės) neinterpretuojamos, o „OpenType“ kintamųjų šriftų (variable fonts) ašys netaikomos; abu šie atvejai, kaip ir sugadinta šrifto programa arba „glyf“ lentelė be jokios naudojamos „cmap“, sugrįžta prie sisteminio šrifto parinkimo, užuot nutraukę puslapio piešimą. Tas pats tikslumo siekis galioja ir kitur atvaizdavimo variklyje — ašinio ir radialinio šešėliavimo šablonai gauna tokį patį apdorojimą, kokio nusipelno gradientai — o generavimo pusė turi savo šriftų niuansų istoriją straipsnyje apie tai, kaip „EndDoc“ rikiuoja šriftų dalis
Šio konvejerio naudojimui nereikia jokio konkretaus šrifto kodo — visi minėti mechanizmai suveikia automatiškai iškviečiant puslapio atvaizdavimą
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;
Praktinė pasekmė yra būtent ta, kuri rūpi jūsų palaikymo tarnybai: PDF failas, turintis savo šriftus, atvaizduojamas būtent su tais šriftais surinkimo serveryje, „Windows“ konteineryje arba kliento kompiuteryje, kuriame šis šriftas niekada nebuvo įdiegtas. Įterptųjų glifų atvaizdavimo konvejeris tiekiamas kaip dalis „HotPDF Component“ komponento, skirto Delphi ir C++Builder — tai vietinė VCL biblioteka, apimanti PDF kūrimą, redagavimą, teksto išgavimą ir puslapių atvaizdavimą be jokių išorinių DLL priklausomybių