Dokumentas, kuris jūsų kompiuteryje atrodo puikiai, o kito asmens įrenginyje atvaizduojamas kaip tuščių kvadratėlių eilutė, yra dažniausia šriftų klaida dirbant su PDF. Tai beveik niekada nereiškia, kad pats tekstas yra neteisingas. Simboliai yra nepažeisti, kodavimas tvarkingas – tiesiog nėra pačių glifų (glyph). Šiuose dviejuose įrenginiuose skiriasi operacinėje sistemoje įdiegti šriftai. Skirtumą tarp stabilaus nešiojamojo dokumento ir neatsparaus failo lemia vienas sprendimas, priimtas puslapio kūrimo metu: ar šriftas buvo įrašytas pačiame PDF faile, ar buvo daroma prielaida, kad jis jau yra gavėjo sistemoje.
Norint suprasti, kodėl taip nutinka ir kodėl dėl kitos klaidos tekstas atrodo tinkamas paieškai, bet nukopijuotas virsta neįskaitomomis šiukšlėmis, reikia pasidomėti, kaip PDF saugo tekstą. Jame nesaugomi sakiniai. Jame saugomi glifų kodai, šrifto programa ir lentelės, kurios juos susieja tarpusavyje. Kiekviena atvaizdavimo ar teksto išgavimo klaida atsiranda dėl nesuderinamumo tarp šių trijų elementų. Toliau pateikiame šio mechanizmo apžvalgą pagal standartą ISO 32000, kartu nurodydami atitinkamus „Delphi“ programinio kodo iškvietimus.
Simboliai, kodai ir glifai yra trys skirtingi dalykai
Sąvokos dažnai painiojamos, nes kasdienėje kalboje trys skirtingos idėjos sujungiamos į vieną žodį „raidė“. Simbolis (character) yra abstraktus rašto vienetas, pavyzdžiui, didžioji raidė A, Unikode žymima kaip U+0041. Glifas (glyph) yra nupiešta forma – kreivėmis ir brūkšniais aprašytas kontūras, kurį konkretus šriftas naudoja tam simboliui pavaizduoti. Tarp jų yra kodas: baitas arba baitai turinio sraute, nurodantys peržiūros programai, kurį dabartinio šrifto glifą reikia nupiešti.
PDF veikia kodais. Kai turinio sraute pateikiama eilutė, tie baitai yra aktyvaus šrifto indeksai, o ne Unikodo reikšmės. Šrifto kodavimas lemia, kad kodas 65 reiškia „nupiešti glifą, esantį po indeksu 65“, ir niekas šioje operacijoje nežino, kad rezultatas žmogui atrodo kaip raidė A. Būtent dėl to PDF atvaizduojamas identiškai visur, kur tik pavyksta rasti reikiamus glifus. Dėl tos pačios priežasties teksto išgavimas yra visiškai atskira problema nuo jo rodymo: piešimui reikia tik kodo susiejimo su glifu (code-to-glyph), o skaitymui – kodo susiejimo su Unikodu (code-to-Unicode). Tai yra dvi skirtingos lentelės, kurios gali nesutapti arba egzistuoti atskirai viena nuo kitos.
Šriftų tipai, su kuriais susidursite praktikoje
Standartas ISO 32000 apibrėžia keletą šriftų žodynų tipų, o praktikoje gaunamas ar kuriamas dokumentas naudoja vieną iš trijų. Žinant, kurį tipą naudojate, galima paaiškinti didžiąją dalį kylančių problemų.
Type 1 yra pradinis „Adobe“ „PostScript“ kontūrų formatas, sukurtas naudojant kubines Bezjė (Bezier) kreives. Keturiolika standartinių šriftų, kuriuos privalo turėti kiekviena standartą atitinkanti peržiūros programa („Helvetica“, „Times“, „Courier“, „Symbol“ ir „ZapfDingbats“ šeimos), yra „Type 1“ tipo. Šriftų žodyne, nurodančiame vieną iš jų, galima legaliai nepateikti paties šrifto programinio kodo. Tai vienintelis atvejis, kai neįterpti šrifto yra saugu pagal specifikaciją, o ne dėl sėkmės. Bet kuriam kitam „Type 1“ šriftui programa privalo būti įterpta, kitaip peržiūros programa ją pakeis kitu – dažniausiai metriškai panašiu, bet vizualiai besiskiriančiu šriftu.
TrueType naudoja kvadratines kreives ir atkeliavo iš „Apple“ bei „Microsoft“ ekosistemų. Tokie yra dauguma sisteminių šriftų, ir juos įterpsite dažniausiai. Paprastas „TrueType“ šriftas PDF dokumente apsiriboja vieno baito kodais, todėl vienas toks šriftas gali pasiekti ne daugiau kaip 256 glifus. Šis ribojimas yra struktūrinė priežastis, kodėl CJK (kinų, japonų, korėjiečių) ir kiti dideli rašmenys negali būti pateikiami naudojant paprastą šriftą.
Type 0 (sudėtinis arba CID šriftas) yra atsakymas į šį apribojimą. Jame naudojami kelių baitų kodai ir „CMap“ lentelė, nukreipianti juos į dukterinį „CIDFont“ šriftą, kurio kontūrai patys yra „TrueType“ arba „CFF/Type 1“. Tai vienintelis šrifto tipas, galintis talpinti tūkstančius glifų, todėl bet kokiame PDF faile, kuriame yra kinų, japonų, korėjiečių rašmenų arba didelė daugiakalbė įvairovė, naudojamas „Type 0“ šriftas, nepriklausomai nuo to, ar autorius apie tai pagalvojo. Kaina už tai yra sudėtingumas: daugiau judančių dalių, kurios visos privalo veikti teisingai tiek atvaizduojant, tiek išgaunant tekstą.

Viena iš priežasčių, kodėl failo dydis išlieka nedidelis, yra ta, kad šriftas yra kontūrų rinkinys, o ne fiksuoto dydžio bitų žemėlapiai. Todėl ta pati įterpta programa aptarnauja visus puslapyje naudojamus šrifto dydžius. Mastelio keitimas yra transformacija, pritaikoma piešimo metu, todėl antraštė ir pagrindinis tekstas dalijasi tuo pačiu įterptu šriftu, o įterpimo sąnaudos skaičiuojamos vienam šriftui, o ne kiekvienam dydžiui atskirai.
Įterpimas – skirtumas tarp stabilaus ir neatsparaus dokumento
Įterpimas reiškia, kad šrifto programa (tikrieji kontūrų duomenys) yra įrašoma į PDF failą kaip srautas. Gavėjo įrenginio peržiūros programa, net jei ji nieko nežino apie jūsų naudotą šriftą, skaito šiuos kontūrus tiesiai iš failo ir nupiešia tikslias glifų formas. Jei atsisakote įterpimo, rizikuojate, kad gavėjo sistemoje bus įdiegtas to paties pavadinimo šriftas. Jei jo nėra, peržiūros programa naudoja pakaitinį šriftą. Keturiolikos standartinių šriftų atveju šis pakeitimas yra apibrėžtas ir nesukelia didelių problemų. Visais kitais atvejais rezultatas gali skirtis: nuo šiek tiek kitokio šrifto iki tuščių kvadratėlių, jei joks pakaitinis šriftas nepalaiko atitinkamų rašmenų.
Bibliotekoje „HotPDF“ tai valdoma viena savybe, kurią reikia nustatyti prieš atveriant dokumentą. Savybė FontEmbedding nurodo bibliotekai supakuoti naudojamus šriftus į failą:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Compression := cmFlateDecode;
Pdf.FontEmbedding := True; // outlines travel inside the file
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Calibri', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Ši eiliškumo taisyklė nėra tik kosmetinis reikalavimas. Funkcija BeginDoc įrašo pagrindinę dokumento struktūrą, todėl savybė FontEmbedding privalo turėti reikšmę „True“ dar prieš šį iškvietimą. Nustačius ją vėliau, negausite jokios klaidos ar įspėjimo, tačiau failas bus sukurtas be įterptų šriftų. Tai pati nemaloniausia klaida: kūrėjo kompiuteryje, kur šriftas yra įdiegtas, viskas veikia puikiai, o problema išryškėja tik pas klientą, kur šio šrifto nėra.
Be to, įterpimas yra sritis, kurioje susikerta teisiniai licencijavimo ir inžineriniai aspektai. Šrifto programoje yra nurodytos vėliavėlės (flags), aprašančios, ar jį galima įterpti laisvai, tik peržiūrai, ar išvis draudžiama įterpti. Šių sąlygų laikymasis yra jūsų, o ne atvaizdavimo programos atsakomybė – vien tai, kad programiškai pavyko įterpti šriftą, nereiškia, kad tai daryti buvo leidžiama.
Požodinis įterpimas (subsetting): įterpkite tik naudojamus glifus
Pilnas įterpimas įrašo visą šrifto programą į failą. Didelis CJK TrueType šriftas gali sverti kelis megabaitus, o įterpti jį visą dėl keliolikos simbolių yra labai neekonomiška, ypač didesnės apimties dokumentuose. Šriftų apkarpymas (subsetting) išsprendžia šią problemą įrašant tik tuos glifus, kurie yra naudojami dokumente, ir pervardinant šriftą naudojant šešių atsitiktinių raidžių prefiksą bei pliuso ženklą (pavyzdžiui, ABCDEF+Calibri). Taip peržiūros programa niekada nesupainios apkarpyto šrifto su pilnu sisteminiu šriftu tokiu pačiu pavadinimu.
Daugumai kuriamų dokumentų šriftų apkarpymas yra geriausias numatytasis nustatymas. Jis leidžia išlaikyti failo dydį proporcingą turiniui, o ne pradiniam šriftui. Tai ypač svarbu naudojant didelius daugiakalbius šriftus, kurie priešingu atveju užimtų didžiąją dalį failo vietos. Vienintelis įspėjimas: apkarpytas rinkinys turi tik tuos simbolius, kurie buvo panaudoti dokumento kūrimo metu. Jei vėliau kitas procesas bandys papildyti tekstą naudojant tą patį šriftą, reikiamų glifų faile gali nebūti, o tai sukelia sunkumų atliekant papildomus redagavimus.
Unikodo šriftai ir CJK tuščių kvadratėlių problema
Kai tekstas nėra vien tik lotyniškas, paprastų šriftų galimybės baigiasi. Tokiu atveju reikia aiškiai užregistruoti Unikodą palaikantį šriftą, kad „HotPDF“ galėtų sukurti „Type 0“ šriftą. Funkcija RegisterUnicodeTTF įkelia „TrueType“ failą pagal jo kelią, o po to užregistruotas pavadinimas gali būti naudojamas funkcijoje SetFont kaip ir bet kuris kitas:
Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;
Kad tai veiktų, svarbūs du dalykai. Pirmiausia, šriftas turi palaikyti dokumento simbolius: vien tik lotyniškas „TrueType“ šriftas nesukurs kiniškų glifų vien dėl to, kad to paprašėte, ir rezultatas vėl bus tušti kvadratėliai (nes tų glifų tiesiog nėra tame šrifte). Antra, įterpimas privalo išlikti įjungtas, nes „Type 0“ šriftas, surinktas iš užregistruoto TTF failo, yra nenaudingas peržiūros programai, jei ji negali rasti pačių kontūrų. Mišriam turiniui geriausias pasirinkimas yra plataus profilio šriftai, pavyzdžiui, „Noto“ arba „Arial Unicode MS“ šeimos, kurie yra įterpiami ir apkarpyti.
Teksto rašymas iš dešinės į kairę (RtL) ir sudėtingi rašmenys reikalauja papildomo glifų formavimo lygmens. Biblioteka „HotPDF“ siūlo funkciją RtLTextOut arabų ir hebrajų kalboms, kuri valdo krypties keitimą, todėl galite perduoti loginę simbolių seką, o biblioteka ją teisingai išdėsto ekrane. Norint teisingai atvaizduoti, pavyzdžiui, arabų kalbos tekstą, reikia suderinti simbolių palaikymą, formavimą ir kryptį – tai yra trys skirtingi dalykai, ir klaida bet kuriame iš jų gali virsti tuščiu kvadratėliu.
ToUnicode lentelė: kur veikia teksto kopijavimas
Visa tai, kas aptarta viršuje, yra susiję su teksto braižymu. Teksto išgavimas yra veidrodinis procesas ir jis gali nepavykti dėl savų priežasčių. Peržiūros programa puslapį atvaizduoja naudodama šrifto kodų susiejimą su glifais, tačiau kai vartotojas bando pažymėti ir nukopijuoti tekstą, programai reikia tuos pačius kodus paversti atgal į Unikodą. Šiam atvirkštiniam susiejimui naudojama ToUnicode lentelė (CMap) – neprivalomas srautas, pridedamas prie šrifto.
Kai ši lentelė yra ir yra teisinga, nukopijuotas tekstas išsaugomas kaip teisingi simboliai. Kai jos nėra arba ji klaidinga (arba šriftas buvo apkarpytas naudojant pasirinktinius glifų kodus ir be ToUnicode lentelės), puslapis atrodys puikiai, tačiau nukopijuotas tekstas bus visiškos šiukšlės, nes glifų kodai bus skaitomi kaip Unikodas, nors taip nėra. Dėl šios priežasties nuskenuotas dokumentas su OCR teksto sluoksniu gali būti tinkamas paieškai, o skaitmeniniu būdu neatsargiai sugeneruotas PDF – ne. Kadangi atvaizdavimui ir teksto nukopijavimui naudojamos skirtingos lentelės, dokumentas gali puikiai atlikti vieną funkciją ir visiškai sugadinti kitą. Jei jūsų programinei įrangai svarbus teksto kopijavimas, teisingos ToUnicode lentelės kūrimas turi būti privalomas reikalavimas, kurį reikia patikrinti rankiniu būdu nukopijuojant tekstą.
Kaip greitai diagnozuoti šriftų klaidas
Klaidos pobūdis parodo, kur ieškoti problemos. Tušti kvadratėliai kitame kompiuteryje beveik visada reiškia, kad šriftas nebuvo įterptas (pirmiausia patikrinkite įterpimą, o tada simbolių palaikymą). Kvadratėliai, rodomi jūsų pačių kompiuteryje, rodo simbolių palaikymo (coverage) problemą: šrifte tiesiog nėra tų simbolių. Tekstas, kuris atvaizduojamas teisingai, bet nukopijuotas virsta nesąmonėmis, yra ToUnicode lentelės problema, todėl šriftų keitimas ar įterpimo nustatymai čia nepadės. Norėdami patikrinti failą, atidarykite jį programoje „Acrobat“ ir pasirinkite „Document Properties“ -> „Fonts“: teisingame įraše bus rodomas tipas, užrašas „Embedded“ arba „Embedded Subset“ bei nurodytas kodavimas. Šriftas, kuris turėjo būti įterptas, bet nebuvo, ten bus aiškiai matomas.
Visi šie dalykai tampa suprantami, kai aiškiai skiriate simbolį, kodą ir glifą. Įterpkite naudojamus šriftus, apkarpykite didelius failus, naudokite Unikodą palaikančius šriftus bei funkciją RegisterUnicodeTTF, kai tik tekstas peržengia lotynų kalbos ribas, ir išlaikykite teisingą ToUnicode lentelę. Atlikus šiuos žingsnius, tušti kvadratėliai išnyks. Jei norite sužinoti daugiau apie PDF struktūrą, straipsnis apie minimalaus PDF anatomiją rodo, kur objektų medyje yra šrifto žodynas, o dokumentų struktūros apžvalga paaiškina, kaip ištekliai dalijami tarp puslapių.
Čia parodyti iškvietimai (SetFont, FontEmbedding ir RegisterUnicodeTTF) yra dalis „Delphi“ ir „C++Builder“ skirto „HotPDF“ komponento.