Technical Article

Arabų kalbos ir RTL teksto formavimas Delphi PDF failuose su HotPDF

Perduokite arabišką frazę يوضح ملف PDF metodui TextOut ir atidarykite rezultatą. Raidės bus surašytos neteisinga kryptimi, o kiekviena iš jų bus izoliuotos formos su matomu tarpu prieš kitą, tarsi kas nors būtų parašęs anglų kalbos tekstą atbulai ir įterpęs tarpą tarp kiekvieno simbolio. Jokios klaidos nebuvo išmesta, jokio įspėjimo neišspausdinta. Rezultatas tiesiog neteisingas, nes nebuvo atliktos dvi atskiros transformacijos, nuo kurių priklauso arabų kalbos tekstas. Žinoti, kas yra šios dvi transformacijos ir kuris iškvietimas jas atlieka, yra svarbiausia dalis dirbant su sudėtingų raštų sistemų PDF išvestimi.

HotPDF yra gimtasis VCL PDF komponentas, skirtas Delphi ir C++Builder, ir jis atlieka rašymo iš dešinės į kairę darbą už jus per specialų iškvietimą. Jis taip pat turi tam tikrų apribojimų keliose specifinėse vietose, apie kurias norėsite sužinoti prieš pradedami dirbti su konkrečia lokalizacija, todėl atviros šių apribojimų ribos yra aprašytos atskirame skyriuje pabaigoje.

Kodėl teisinga eilutė vis tiek atspausdinama neteisingai

Unicode išsaugo tekstą logine tvarka – ta tvarka, kuria jį vedate ir skaitote garsiai. Atvaizdavimo variklis turi išdėstyti glifus vizualia tvarka. Kairiųjų-dešiniųjų raštų atveju šios tvarkos sutampa, ir niekas apie tai negalvoja. Arabų ir hebrajų kalbų atveju taip nėra, o kai vienoje eilutėje susimaišo kryptys (pavyzdžiui, arabiškame sakinyje yra lotyniškas žodis „PDF“ arba skaitmenimis užrašyta kaina), „Unicode Bidirectional Algorithm“ (UAX #9) tiksliai nustato, kaip kairieji-dešinieji fragmentai įsiterpia į dešiniųjų-kairiųjų eilutę. Tai yra pirmoji transformacija – tvarkos keitimas, o jos praleidimas ir yra priežastis, kodėl eilutė apsiverčia.

Antroji transformacija yra kontekstinis formavimas. Arabiška raidė braižoma skirtingai, priklausomai nuo to, kurioje žodžio vietoje ji yra: pradžioje, viduryje, pabaigoje arba stovi viena. Kodo taškas (codepoint) visada išlieka toks pat, keičiasi tik glifas. Procesas, kuris kiekvieną kodo tašką perduoda tiesiai jo numatytajam glifui, sukuria būtent tokią nesusijusią, izoliuotų raidžių išvestį, kokia aprašyta pirmoje pastraipoje. Hebrajų kalba šį žingsnį praleidžia, nes jos raidės nesijungia tarpusavyje, tačiau jai vis tiek reikia keisti tvarką. Arabų kalbai reikia abiejų etapų, todėl testavimui turėtumėte naudoti būtent arabų, o ne hebrajų kalbos eilutę.

Stalinėje aplinkoje visa tai nėra jūsų problema. Kai VCL forma piešia arabišką tekstą TEdit valdiklyje, operacinės sistemos teksto krūva tyliai pakeičia tvarką ir jį suformuoja – būtent todėl ekrane tobulai atrodanti eilutė paprastame PDF faile sugadinama. Turinio sraute nesaugomas redaguojamas tekstas. Jame saugomi pozicionuoti glifai, todėl tas, kas generuoja srautą, paveldi formavimo užduotį, kurią anksčiau atlikdavo operacinė sistema. RtLTextOut yra iškvietimas, kuris perima šį darbą.

RtLTextOut atlieka tvarkos keitimą ir jungimą

HotPDF lotyniškų simbolių kelią ir sudėtingo rašto kelią laiko kaip du skirtingus metodus. TextOut atspausdina tai, ką jam pateikiate, tokia tvarka, kokia pateikiate. RtLTextOut pirmiausia pakeičia tvarką ir atlieka kontekstinę analizę, o tik tada spausdina. Kurias rašto taisykles taikyti, nustatoma pagal SetFont koduotės (charset) argumentą: 178 reiškia arabų kalbą, 177 – hebrajų.

// Arabic: pass logical order; RtLTextOut reorders and joins
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF');

// Hebrew: reordering only, no contextual joining
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Viena klaida čia atima daugiau derinimo valandų nei bet kuri kita. RtLTextOut pati apverčia eilutę, todėl jei jai pateikiate tekstą, kurį jau apvertėte rankiniu būdu (paprastai tai būna likę iš ankstesnių bandymų naudoti paprastą TextOut), ji vėl jį apvers, ir grįšite į pradžią. Blogiausia yra tai, kad dvigubai apverstas tekstas gali atrodyti teisingai vienoje arabiškoje bandomojoje eilutėje, tačiau subyrės, kai eilutėje atsiras lotyniškas žodis arba skaičius, nes šie įterpti fragmentai nebbeatitinka UAX #9 standarto. Visada perduokite loginę tvarką ir leiskite metodui viską sutvarkyti.

Tas pats mišrios krypties elgesys peržiūros specialistus klaidina labiau nei patį kodą. Dešiniojo-kairiojo rašto eilutėje skaitmenys ir įterpti lotyniški žodžiai vis tiek skaitomi iš kairės į dešinę. Kas nors, kas nėra dirbęs su dvikrypčiu išdėstymu, pamatęs sugeneruotą sąskaitą-faktūrą, pamanys, kad sąskaitos numeris, parašytas „neteisinga“ kryptimi arabiško teksto atžvilgiu, yra klaida. Tačiau tai yra pagal specifikaciją teisingas rezultatas. Trumpa pastaba jūsų priėmimo kriterijuose, parašyta prieš pirmąjį gimtakalbio patikrinimą, padės išvengti šių nesusipratimų.

Glifų palaikymas nustatomas dar prieš pradedant formavimą

Formavimas parenka glifus iš šrifto. Jei šriftas jų neturi, nėra ko parinkti. Tai yra diegimo klaida, kuri gali atimti visą popietę: ataskaita atrodo puikiai programuotojo kompiuteryje, kuriame yra įdiegtas Arial Unicode MS, tačiau kliento serveryje atvaizduojama kaip tuščių kvadratėlių eilutė, nes Windows parinko kitą šriftą, kuris visiškai neturi arabų kalbos palaikymo. Sprendimas – nustoti pasitikėti bet kokiais kompiuteryje esančiais šriftais ir užregistruoti šriftą, kurį pristatote kartu su programa.

// Ship a known font instead of relying on installed system fonts
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12);

// Audit coverage for the codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

Čia susiduriama su dviem apribojimais. Šriftas, užregistruotas per RegisterUnicodeTTF, yra įterpiamas į failą, o HotPDF įterpto Unicode palaikymui reikalingas PDF 1.5 ar vėlesnio formato dokumentas. Tai gali trukdyti tik tada, jei kokia nors kita sistema reikalauja PDF 1.4 formato, tačiau jei taip nutinka, jokių pranešimų apie klaidą negaunate. Kitas apribojimas yra teisinio pobūdžio: TrueType failai turi įterpimo leidimų bitus, o šriftas, kuris puikiai atrodo ekrane, gali turėti licenciją, draudžiančią jį platinti klientų dokumentuose. Patikrinkite tai prieš išsiųsdami programą, o ne po to, kai gausite skundą.

Antrasis iškvietimas, GetUnicodeGlyphForCodepoint, yra jūsų ankstyvojo įspėjimo sistema. Paleidžiant paslaugą pereikite per kodo taškų diapazonus, kuriuos iš tikrųjų naudoja jūsų duomenys, ir užregistruokite, kokie glifų ID grąžinami. Taip glifų trūkumas bus matomas paleidimo žurnale diegimo metu, o ne kaip trūkstami simboliai sąskaitoje-faktūroje, kuri jau pasiekė klientą.

Tekstas, kuris yra Unicode, bet ne rašomas iš dešinės į kairę (CJK eilutės, vietnamiečių kalba su sudėtiniais diakritiniais ženklais, mišrus Europos kalbų tekstas), eina įprastu keliu. TextOut priima WideString ir nupiešia jį per užregistruotą šriftą netaikydama jokios dvikryptės analizės. Ataskaitos kode verta fiziškai atskirti šiuos du kelius: vieną funkciją RTL tekstui, o kitą – visam likusiam, kad lokalės logika būtų aiškiai matoma iškvietimo vietoje, o ne paslėpta už vėliavėlės (flag), kurią kas nors galiausiai pamirš nustatyti.

Skaitymo kryptis priklauso dokumentui, o ne glifams

Teisingai sudėliojus visus glifus, dar lieka vienas nebaigtas darbas. ISO 32000-1 §12.2 apibrėžia peržiūros programos nuostatą, vadinamą /Direction, kuri nurodo bendrą dokumento skaitymo kryptį. Tai neliečia glifų. Ši nuostata nurodo peržiūros programai, kaip išdėstyti dviejų puslapių atvartus, nuo kurios pusės turėtų prasidėti dvipusis maketas ir į kurią pusę turėtų krypti skaitymo vartotojo sąsaja. Nieko iš to nematyti viename puslapyje, todėl tai dažnai pamirštama.

Nustatyti Direction yra visa užduotis: savybės nustatymas prideda vpDirection į dokumento ViewerPreferences, todėl viena kodo eilutė perkelia šią nuostatą į failą. Klaidos pasireiškimas yra šios eilutės nebuvimas, kas atrodo visiškai nekenksminga, nes vieno puslapio juodraštis, į kurį žiūrite, abiem atvejais atrodo identiškai. Tačiau kai kas nors atspausdina dvipusę knygelę, atvartai išeina veidrodiniai, o to priežastis – prieš kelias savaites pamiršta viena kodo eilutė.

Kur baigiasi HotPDF formavimas

Sąžiningas apribojimų įvertinimas yra vertas savaitės tyrimų. RtLTextOut pati atlieka dvikryptį tvarkos keitimą ir arabišką kontekstinį jungimą. Ko ji nedaro automatiškai – tai bendrojo OpenType funkcijų pritaikymo. Pasirenkamos ligatūros ir panašios tipografinės funkcijos apdorojamos per GetSingleSubstituteGlyph(GID, 'liga'), kuri vienu metu išsprendžia vieną pakeitimą (pirmiausia glifo ID, o po to funkcijos žymę) ir grąžina pradinį glifą nepakeistą, jei funkcija netaikoma. To pakanka žinomam, ribotam ligatūrų sąrašui, kurį prižiūrite patys, palaikyti. Tai nėra pilnavertis GSUB variklis. Rašmenims, kuriems reikia daugiau nei tvarkos keitimo ir jungimo (pavyzdžiui, indų raštams su jų keičiama balsių tvarka), atlikite realius bandymus su tikromis klientų eilutėmis prieš pažadėdami tos šalies lokalizaciją. Tai, kad veikia arabų kalba, negarantuoja, kad veiks devanagari.

Patikrinkite procesą nuo pradžios iki galo, nes puslapis gali atrodyti teisingai, tačiau būti visiškai nenaudingas kitoms sistemoms. Trys patikrinimai padeda rasti daugumą problemų. Nukopijuokite tekstą iš Acrobat programos ir palyginkite kodo taškus su savo šaltinio eilute. Paleiskite paiešką peržiūros programoje, ieškodami žodžio, kurį matote puslapyje. Ir atidarykite rezultatą kompiuteryje, kuriame nėra jūsų kūrimo šriftų – tai geriausias būdas pastebėti šrifto pakeitimus. Niekas nepakeičia gimtakalbio, vertinančio vieną tikrą dokumentą, kas padeda pastebėti tokius dalykus, kurių joks sintetinis rinkinys neparodys. Suplanuokite šį patikrinimą prieš pradėdami platinti šį formatą.

Bandomąsias eilutes rinkitės sąmoningai, o ne naudokite tai, ką vertėjas atsiuntė praėjusiais metais. Tinkamas minimumas kiekvienai lokalei: sakinys tik tiksline kalba, sakinys su įterptais lotyniškais prekių ženklų pavadinimais, eilutė su skaitmenimis ir valiuta bei vardai su diakritiniais ar jungiamaisiais ženklais. Tikri klientų vardai griauna prielaidas, kurias užpildo tekstas (filler text) palieka nepaliestas, todėl leiskite regresinių testų rinkiniui pasipildyti viena nauja eilute kiekvieną kartą, kai klientų aptarnavimo skyrius praneša apie dar nematytą šabloną.

Šriftų užregistravimas, poaibių kūrimas ir kasdienis teksto piešimo API yra aprašyti straipsnyje apie ataskaitų išvestį, šriftus ir vaizdus su HotPDF. Kai tie patys dokumentai turi atitikti prieinamumo reikalavimus, kalbų žymėjimas ir struktūros taisyklės iš PDF/A ir PDF/UA patikros straipsnio papildo čia aprašytą teksto formavimo darbą.

Aukščiau aprašyti rašymo iš dešinės į kairę ir Unicode šriftų API pateikiami kartu su HotPDF komponentu, skirtu Delphi ir C++Builder; produkto puslapyje pateikiama nuoroda į pilną teksto išvesties specifikaciją.