Techninis straipsnis

Arabiškas ir RTL teksto formavimas Delphi PDF failuose su HotPDF

Perduokite arabišką frazę يوضح ملف PDF į TextOut ir atidarykite rezultatą. Raidės eina neteisinga kryptimi, o kiekviena iš jų yra atskiroje (izoliuotoje) formoje su matomu tarpu prieš kitą raidę, tarsi kas nors būtų rašęs angliškai atvirkščiai ir po kiekvieno simbolio spaudęs tarpą. Jokios išimties (exception) nebuvo išmesta. Joks įspėjimas neatspausdintas. Išvestis tiesiog neteisinga, ir ji neteisinga, nes dvi atskiros transformacijos, nuo kurių priklauso arabų kalba, taip ir neįvyko. Žinoti, kokios tai transformacijos ir kokia komanda (call) jas atlieka, yra tai, kuo iš esmės ir pasižymi sudėtingų raštų (complex-script) PDF išvestis

HotPDF yra natūralus (native) VCL PDF komponentas, skirtas Delphi ir C++Builder, ir jis atlieka iš dešinės į kairę (right-to-left) rašymo darbą už jus per atskirą komandą. Tačiau jis sustoja ties keliomis konkrečiomis vietomis, apie kurias norėsite sužinoti prieš patvirtindami lokalę, todėl šiame straipsnyje apžvelgiamos sąvokos ir tikrosios ribos; praktinis pačios komandos nustatymas aprašytas RtLTextOut informaciniame straipsnyje

Kodėl teisinga eilutė vis tiek atspausdinama klaidingai

Unikodas (Unicode) laiko tekstą logine tvarka, t. y. tokia tvarka, kokia jį įvedate ir skaitote garsiai. Atvaizduoklis (renderer) turi išdėstyti glifus (glyphs) vizualine tvarka. Skaitant iš kairės į dešinę šios tvarkos sutampa ir niekas apie tai nesusimąsto. Arabų ir hebrajų kalbose taip nėra, ir kai vienoje eilutėje susimaišo kryptys, pavyzdžiui, arabiškame sakinyje yra lotyniškas žodis „PDF“ arba skaitmenimis užrašyta kaina, unikodo dvikryptis algoritmas (UAX #9) tiksliai nustato, kaip iš kairės į dešinę skaitomi fragmentai įsiterpia į iš dešinės į kairę skaitomą eilutę. Tai yra pirmoji transformacija – pertvarkymas (reordering), ir ją praleidus eilutė apsiverčia

Antroji – kontekstinis formavimas (contextual shaping). Arabiška raidė piešiama skirtingai, priklausomai nuo to, kurioje žodžio vietoje ji yra: pradžioje, viduryje, pabaigoje ar stovi atskirai. Kodo taškas (codepoint) visą laiką išlieka tas pats; keičiasi tik glifas. Apdorojimo konvejeris (pipeline), kuris kiekvieną kodo tašką perduoda tiesiai į jo numatytąjį glifą, sukuria būtent tokią atjungtą, izoliuotos formos išvestį, kokia aprašyta pradinėje pastraipoje. Hebrajų kalba šį žingsnį praleidžia, nes jos raidės nesijungia, bet jai vis tiek reikia pertvarkymo. Arabų kalbai reikia abiejų, ir būtent todėl testuojama naudojant arabų, o ne hebrajų kalbos eilutę

Kompiuterio darbalaukyje visa tai nėra jūsų problema. Kai VCL forma nupiešia arabišką tekstą į TEdit, operacinės sistemos teksto krūva (stack) tyliai jį pertvarko ir suformuoja, ir būtent todėl ekrane puikiai atrodanti eilutė naiviame PDF faile išeina sugadinta. Turinio sraute nesaugomas redaguojamas tekstas. Jame saugomi išdėstyti glifai, todėl tas, kuris išveda šį srautą, perima formavimo darbą, kurį anksčiau atlikdavo OS. RtLTextOut yra komanda, kuri sugrąžina šį darbą atgal

Ką RtLTextOut formuoja už jus

HotPDF lotyniško rašto kelią ir sudėtingų raštų (complex-script) kelią išlaiko kaip du skirtingus metodus. TextOut spausdina tai, ką jam duodate, ta tvarka, kuria duodate. RtLTextOut iš pradžių atlieka abi transformacijas – dvikryptį pertvarkymą per visą eilutę, kontekstinę analizę besijungiantiems raštams – ir tik tada spausdina. Tai, kurio rašto taisyklės taikomos, perduodama per šrifto koduotę (charset), o ne per pačią komandą, todėl kryptis yra aiškus pasirinkimas kiekvienoje iškvietimo vietoje, o ne spėjimas iš simbolių. Nustatymas pagal kiekvieną parametrą, koduotės reikšmės, šrifto registravimo žingsniai ir išsamus kompiliuojamas pavyzdys pateikiami RtLTextOut informaciniame straipsnyje; šis straipsnis apsiriboja tuo, ką reiškia transformacijos, kur jos baigiasi ir kaip įrodyti, kad jos suveikė

Viena naudojimo taisyklė svarbi net ir šiame lygmenyje: įvestis turi būti loginės tvarkos, nes RtLTextOut apvertimą atlieka pati, ir jūsų jau rankiniu būdu apversta eilutė gaunasi apversta dukart – informaciniame straipsnyje aptariami šie spąstai ir jų ištaisymas. Šie spąstai čia paminėti todėl, kad jie dažnai praslysta testavimo metu. Dukart apversta grynai arabiška eilutė gali atrodyti visiškai teisingai, o sugriūva tik tada, kai eilutėje yra lotyniškas žodis ar skaičius, nes šie įterpti srautai nebegali įsiterpti taip, kaip diktuoja UAX #9 algoritmas. Klaida slypi ne atvaizdavime; klaida yra tai, kad algoritmui pateikiamas tekstas, kuris jau buvo pusiau apdorotas

Tas pats mišrios krypties elgesys dažniau suklaidina tikrintojus, nei pačią programą. Iš dešinės į kairę skaitomoje eilutėje, skaitmenys ir įterpti lotyniški žodžiai vis tiek skaitomi iš kairės į dešinę. Tas, kas nėra dirbęs su dvikrypčiu išdėstymu, pažvelgęs į sugeneruotą sąskaitą faktūrą pamatys sąskaitos numerį skaitomą „neteisinga“ kryptimi lyginant su aplink esančiu arabišku tekstu, ir praneš apie tai kaip apie klaidą. Nors tai yra specifikaciją atitinkantis rezultatas. Trumpa pastaba jūsų priėmimo kriterijuose, parašyta dar prieš atiduodant testuoti gimtakalbiams, sutaupo šį bereikalingą darbą

Kada pertvarkymo ir sujungimo pakanka, o kada ne

Arabų ir hebrajų kalbų bėgančiam tekstui – ataskaitoms, sąskaitoms faktūroms, sutartims, laiškams – pertvarkymas ir kontekstinis sujungimas yra visas darbas, ir RtLTextOut jį atlieka vienas. Riba atsiranda tada, kai tipografija reikalauja kažko daugiau nei sujungimas. HotPDF atsakymas arabų kalbos atveju yra pasirenkamas (opt-in) gamintojo pusės formuotojas (producer-side shaper): nustatykite AutoShapeArabic := True ir komponentas prieš dvikryptį perėjimą perrašo loginės tvarkos seką į „Unicode Presentation Forms“, kad jungiamosios formos būtų apskaičiuojamos pagal loginius kaimynus, o ligatūrų susiliejimai būtų įrašyti tiesiai į kodo taškus, kuriuos PDF faktiškai savyje neša, o ne palikti peržiūros programai išspręsti. Pagal nutylėjimą šis jungiklis išjungtas ir išvestis yra baitų lygmeniu stabili (byte-stable), kai jis lieka išjungtas, todėl jo įjungimas yra sąmoningas sprendimas kiekvienam dokumentų konvejeriui atskirai, o ne visuotinis atnaujinimas. Tas pats pasirinkimo modelis taikomas ir kitiems besijungiantiems iš dešinės į kairę rašomiems raštams, kuriuos HotPDF formuoja: Sirijos (Syriac), N'Ko, Adlamo ir Hanifi Rohingjų raštai turi savo automatinio formavimo vėliavėles, kurios atspindi arabų kalbos vėliavėlę

Papildomos „OpenType“ funkcijos vėlgi yra kitoks mechanizmas. Pasirenkamos ligatūros ir panašios vieno pakeitimo funkcijos eina per GetSingleSubstituteGlyph(GID, 'liga'), kuris vienu metu išsprendžia vieną pakeitimą – pirmiausia įvesties glifo ID, po to funkcijos žymė – ir grąžina nepakeistą įvesties glifą, kai funkcija nėra pritaikoma. To pakanka valdyti žinomą, baigtinį ligatūrų sąrašą, kurį prižiūrite patys. Tai nėra pilnas GSUB variklis, ir šis skirtumas yra būtent ta vieta, kur žlunga ambicingi lokalizacijos planai: formavimo konvejeris, kuris nepriekaištingai apdoroja arabų kalbą, pademonstruoja tik pertvarkymą ir sujungimą, nieko daugiau

Skirtingų raštų aprėptis

Arabų kalba naudoja abi transformacijas, todėl būtent su šia eilute ir testuojama, ir kodėl sėkmingas arabų kalbos apdorojimas yra stipriausias pavienis įrodymas, kad konvejeris veikia. Hebrajų kalbai reikia pertvarkymo, bet ne sujungimo, nes jos raidės stovi atskirai; jei hebrajų kalba atvaizduojama teisingai, bet arabiška išeina atjungta, vadinasi, dvikryptė dalis veikia gerai, o kontekstinė taip ir nebuvo paleista. Persų ir urdu kalbos naudoja arabišką raštą ir paveldi jo elgseną, nors urdu kalbos pirmenybė „Nastaliq“ stiliui yra šrifto pasirinkimas, turintis pasekmių įskaitomumui, kurias turėtų įvertinti gimtakalbis skaitytojas

Tajų kalba yra visiškai kitoje linijos pusėje. Ji skaitoma iš kairės į dešinę, todėl jai nereikia jokių dvikrypčių veiksmų, o jos raidės nesijungia, todėl jai nereikia jokios kontekstinės analizės; tajų kalbos eilutės pereina per įprastą TextOut kelią kaip ir lotyniškos. Ką tajų kalba turi, tai sukrautus ženklus (stacked marks) – balses ir tono ženklus virš ir po pagrindinio priebalsio – ir tai, ar jie išsidėstys teisingai, priklauso nuo to, ar šriftas sukuria savo jungiamuosius ženklus, kad jie susidėtų be formavimo variklio pagalbos. Dauguma specialiai tajų kalbai skirtų šriftų tai daro. Testuokite su tiksliu šriftu, kurį ketinate įterpti, o ne į jį panašiu

Devanagari ir likusi indų kalbų šeima (Indic family) yra tikras ir griežtas sustojimas. Jų balsių ženklai persitvarko aplink priebalsių samplaikas, o jų jungtys formuojasi per kontekstinių pakeitimų grandines, kas priklauso išimtinai GSUB teritorijai, toliau nei pertvarkymas ir sujungimas. Jei indų lokalė yra įtraukta į planus, paleiskite realų bandomąjį projektą naudodami autentiškas kliento eilutes prieš ką nors pažadėdami – tai, kad veikia arabų kalba, nėra įrodymas, kad veiks ir Devanagari. CJK eilutės, vietnamiečių kalba su jos sukrautais diakritiniais ženklais ir mišrus europietiškas tekstas keliauja įprastu keliu be jokios dvikryptės analizės, todėl ataskaitų kode apsimoka išlaikyti šiuos du kelius fiziškai atskirtus: vieną rutiną RTL srautams ir kitą viskam kitam, kad lokalės logika būtų matoma iškvietimo vietoje, o ne paslėpta už vėliavėlės, kurią kas nors pamiršta nustatyti

Glifų aprėptis nustatoma dar prieš pradedant formavimą

Formavimas parenka glifus iš šrifto. Jei šriftas jų neturi, nėra ko rinktis, todėl klasikinė diegimo klaida – nepriekaištinga vystytojo kompiuteryje, tušti langeliai kliento serveryje po tylaus šrifto pakeitimo – yra aprėpties, o ne formavimo problema. Praktiškas vaistas, t.y. jūsų pateikiamo šrifto registravimas užuot pasikliovus tuo, kas įdiegta kompiuteryje, žingsnis po žingsnio aprašomas informaciniame straipsnyje. Konceptuali esmė yra ta, kad aprėptis turi būti nustatyta dar prieš kylant bet kokiam su formavimu susijusiam klausimui, ir kad tai galima nustatyti programiškai, o ne vien vizualiai peržiūrint išvestį

// Po RegisterUnicodeTTF, atlikite aprėpties auditą tiems
// kodo taškams, kuriuos jūsų duomenys faktiškai naudoja
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

Pati registracija turi du apribojimus – ne senesnė nei PDF 1.5 versija skirta įterptam unikodo apdorojimui, bei šrifto įterpimo leidimo bitai – abu šie aspektai, kartu su sąrankos žingsniais, aprašyti RtLTextOut informaciniame straipsnyje. Čia svarbus audito įprotis: GetUnicodeGlyphForCodepoint yra jūsų ankstyvojo perspėjimo sistema. Paslaugai paleidžiant, pereikite per tuos kodo taškų diapazonus, kuriuos faktiškai naudoja jūsų duomenys, ir įrašykite į žurnalą, kokie glifų ID sugrįžta. Tuomet aprėpties trūkumas pasirodo kaip eilutė paleidimo žurnale diegimo metu, o ne kaip trūkstami simboliai sąskaitoje faktūroje, kuri jau pasiekė klientą

Skaitymo tvarka priklauso dokumentui, o ne glifams

Gavus kiekvieną teisingą glifą vis tiek lieka vienas nepadarytas darbas. ISO 32000-1 §12.2 apibrėžia peržiūros programos nuostatą pavadintą /Direction, nustatančią bendrą dokumento skaitymo tvarką. Ji neliečia jokių glifų. Ką ji daro, tai nurodo peržiūros programai, kaip išdėstyti dviejų puslapių atvartus, nuo kurios pusės turėtų prasidėti atverstų puslapių išdėstymas, ir į kurią pusę turėtų linkti skaitymo vartotojo sąsaja. Niekas iš to neatsispindi viename puslapyje, ir būtent todėl tai yra pamirštama

// Paskelbkite skaitymo iš dešinės į kairę tvarką dokumento lygiu
Pdf.Direction := RightToLeft;  // prideda vpDirection į ViewerPreferences

Direction nustatymas yra visas darbas: savybės nustatymo metodas prideda vpDirection prie dokumento ViewerPreferences, taigi viena eilutė perkelia šią nuostatą į failą. Jei tekstas išvedamas per RtLTextOut, tai gaunate nemokamai, nes ši komanda apverčia dokumento kryptį kaip šalutinį poveikį – informacinis straipsnis aprašo, kada mišriam dokumentui šį pakeitimą reikia atšaukti. Atvejis, kai privalote jį nustatyti patys, yra iš dešinės į kairę skaitomas dokumentas, sukurtas bet kokiu kitu būdu, pavyzdžiui, iš įvesties, kurią iš anksto suformavote ir nupiešėte įprastu būdu. Praleiskite tai, ir vieno puslapio pavyzdys, į kurį žiūrite, abiem atvejais atrodys identiškai; tuomet kažkas atspausdins dvipusę knygelę, atvartai išeis veidrodiniu principu atspindėti, o to priežastis bus praleista viena eilutė kodo prieš kelias savaites

Suformuotos išvesties patikrinimas

Patikrinkite nuo pradžios iki galo, nes puslapis gali atrodyti teisingai, bet vis tiek būti nenaudingas viskam, kas vyksta vėliau. Trys patikrinimai padeda rasti daugumą problemų. Nukopijuokite tekstą atgal iš „Acrobat“ ir palyginkite kodo taškus su savo pradine eilute. Paleiskite peržiūros programos paiešką dokumente ir ieškokite žodžio, kurį matote puslapyje. Ir atidarykite išvestį kompiuteryje, kuriame nėra jūsų kūrimo aplinkos šriftų – tai kompiuteris, kuriame didžiausia tikimybė atskleisti šrifto pakeitimą. Nė vienas iš šių veiksmų neatstoja gimtakalbio skaitytojo, vertinančio vieną realų dokumentą – jis pastebi dalykus, kurių joks sintetinis tekstų rinkinys nepamatys. Įtraukite šią peržiūrą į kalendorių dar prieš išleidžiant formatą

Testavimo eilutes pasirinkite tikslingai, užuot pakartotinai naudoję tai, ką vertėjas atsiuntė praėjusiais metais. Tinkamas minimumas kiekvienai lokalei: sakinio su grynuoju raštu eilutė, sakinys su įterptais lotyniškais prekių ženklų pavadinimais, eilutė su skaitmenimis ir valiuta bei vardai su diakritiniais ar jungiamaisiais ženklais. Realūs klientų vardai sulaužo prielaidas, kurių užpildomasis tekstas nepaliečia, todėl leiskite regresijos rinkiniui išaugti viena eilute kiekvieną kartą, kai klientų aptarnavimo atvejis atskleidžia naują šabloną, kurio anksčiau nematėte

Šriftų registravimas, pogrupių formavimas ir kasdienis teksto piešimo API aprašomi straipsnyje apie ataskaitų išvestį, šriftus ir vaizdus su HotPDF. Kai tie patys dokumentai taip pat turi atitikti prieinamumo profilius, kalbos žymėjimo ir struktūros taisyklės, aprašytos PDF/A ir PDF/UA validavimo straipsnyje, yra pridedamos ant čia aprašyto formavimo darbo viršaus

Aukščiau aprašyti iš dešinės į kairę ir unikodo šriftų API pristatomi kartu su HotPDF komponentu skirtu Delphi ir C++Builder; produkto puslapyje pateikiama nuoroda į pilną teksto išvesties žinyną