Техническа статия

Арабски и RTL Text Shaping в PDF на Delphi с HotPDF

Подайте арабската фраза يوضح ملف PDF на TextOut и отворете резултата. Буквите вървят в грешната посока и всяка една седи в своята изолирана форма с видима празнина преди следващата, сякаш някой е написал английски назадно и е ударил интервал между всеки символ. Никакво изключение не се е задействало. Никакво предупреждение не се е отпечатало. Изходът просто е грешен, и е грешен, защото две отделни трансформации, от които зависи арабският, никога не са се случили. Знанието какви са тези две трансформации и кое извикване ги изпълнява, е по-голямата част от това, до което се свежда изходът на PDF със сложни скриптове (complex-script)

HotPDF е нативен VCL PDF компонент за Delphi и C++Builder, и той върши работата от-дясно-наляво (right-to-left) вместо вас чрез отделно извикване. Той също така спира дотам на няколко специфични места, за които искате да знаете, преди да се ангажирате с даден локал, така че това парче картографира концепциите и честните граници; практическата настройка за самото извикване живее в референтната статия за RtLTextOut

Защо правилен низ все още се отпечатва грешно

Unicode запазва текста в логически ред – редът, в който го въвеждате и четете на глас. Рендерерът трябва да постави глифовете (glyphs) във визуален ред. За скриптове от-ляво-надясно тези редове съвпадат и никой не мисли за това. За арабски и иврит не е така и когато един единствен ред смесва посоки, например арабско изречение, носещо латинския тоукън (token) "PDF" или цена, изписана в цифри, двупосочният алгоритъм на Unicode (Unicode Bidirectional Algorithm, UAX #9) решава точно как фрагментите от-ляво-надясно се влагат (nest) вътре в реда от-дясно-наляво. Това е първата трансформация, пренареждане (reordering), и нейното пропускане е това, което обръща реда

Втората е контекстуалното оформяне (contextual shaping). Арабска буква се чертае различно в зависимост от това къде попада в дадена дума: начална, средна, крайна или стояща сама. Кодовата точка (codepoint) остава същата през цялото време; променя се само глифът. Конвейер, който предава всяка кодова точка направо на нейния глиф по подразбиране, произвежда точно изхода от прекъсната, изолирана форма от началния абзац. Ивритът пропуска тази стъпка, тъй като неговите букви не се съединяват, но той все пак се нуждае от пренареждането. Арабският се нуждае и от двете и точно затова арабският, а не ивритът, е низът, с който тествате

На десктопа нищо от това не е ваш проблем. Когато VCL форма рисува (paints) арабски в TEdit, текстовият стек на операционната система тихо го пренарежда и оформя, което е точно причината низът, който изглежда перфектно на екрана, да излиза счупен в наивен PDF. Потокът от съдържание (content stream) не съхранява редактируем текст. Той съхранява позиционирани глифове, така че който излъчва потока, наследява работата по оформянето, с която ОС-ът преди е се справял. RtLTextOut е извикването, което си връща тази работа

Какво оформя (shapes) RtLTextOut за вас

HotPDF запазва латинския път и пътя на сложните скриптове като два различни метода. TextOut отпечатва това, което му дадете, в реда, в който го дадете. RtLTextOut първо извършва и двете трансформации — двупосочно пренареждане (bidirectional reordering) през целия ред, контекстуален анализ за свързващите се скриптове — и след това отпечатва. Правилата на кой скрипт се прилагат, пътува през набора от знаци (charset) на шрифта, а не чрез самото извикване, така че посоката е изричен избор на всяко място на извикване (call site) вместо предположение, направено от знаците. Настройката параметър по параметър, стойностите на charset-а, стъпките за регистрация на шрифтове и пълен компилируем пример са в референтната статия за RtLTextOut; това парче остава при това какво означават трансформациите, къде спират и как да се докаже, че са работили

Едно правило за използване има значение дори на тази височина: входът трябва да бъде в логически ред, защото RtLTextOut извършва самото обръщане (reversal) и низ, който вече сте обърнали на ръка, излиза двойно обърнат — референтната статия преминава през този капан и неговото почистване. Това, което печели споменаването на капана тук, е защо той преживява тестването. Един двойно обърнат чисто-арабски низ може да изглежда перфектно правилен и се разпада само когато даден ред носи латинска дума или число, защото тези вложени рънове (embedded runs) вече не се влагат по начина, който UAX #9 диктува. Бъгът не е в рендирането; той е в подаването към алгоритъма на текст, който вече е бил наполовина обработен

Същото това поведение на смесена посока (mixed-direction) спъва проверяващите (reviewers) повече, отколкото спъва кода. Вътре в ред от-дясно-наляво цифрите и вложените латински думи все още се четат от-ляво-надясно. Някой, който не е работил с двупосочно оформление, ще погледне рендирана фактура, ще види номера на акаунта да се чете в "грешната" посока спрямо арабския около него и ще го отбележи като бъг (bug). Това е правилният според спецификацията резултат. Кратка бележка във вашите критерии за приемане (acceptance criteria), написана преди първия пас на носител на езика, спестява това пътуване напред-назад

Когато пренареждането и съединяването (joining) са достатъчни, и когато не са

За течащ (running) текст на арабски и иврит — отчети, фактури, договори, писма — пренареждането плюс контекстуалното съединяване е цялата работа, и RtLTextOut я носи самостоятелно. Границата се появява, когато типографията изисква повече от съединяване. Отговорът на HotPDF от арабската страна е opt-in shaper от страната на производителя: задайте AutoShapeArabic := True и компонентът пренаписва ръна с логически ред (logical-order run) към Unicode Presentation Forms преди двупосочния пас, така че съединяващите се форми се изчисляват спрямо логическите съседи, а сгъвките на лигатурите се изпичат в кодовите точки, които PDF реално носи, вместо да се оставят на viewer-а да ги разреши. Превключвателят (switch) по подразбиране е изключен и изходът е стабилен по байтове (byte-stable), когато остане изключен, така че включването му е умишлено решение на конвейер за документи, а не глобално надграждане (upgrade). Същият opt-in модел се разширява към останалите скриптове от-дясно-наляво, които се съединяват, които HotPDF оформя: сирийски (Syriac), Н'Ко (N'Ko), адлам (Adlam) и ханифи рохингя (Hanifi Rohingya), всеки от които има свой собствен флаг за автоматично оформяне, който отразява арабския

Опционалните OpenType характеристики (features) са отново различен механизъм. Лигатури по избор (Discretionary ligatures) и подобни характеристики за единична замяна минават през GetSingleSubstituteGlyph(GID, 'liga'), което разрешава една замяна в даден момент — първо ID на входен глиф (input glyph ID), второ етикет на характеристика (feature tag) — и връща входния глиф непроменен, когато характеристиката не се прилага. Това е достатъчно, за да задвижи известен, краен списък с лигатури, който вие самите поддържате. Това не е пълен GSUB енджин, и разликата е точно там, където амбициозните планове за локали (locale plans) се объркват: конвейер за оформяне, който се справя с арабски безупречно, е демонстрирал пренареждане и съединяване, нищо повече

Покритие сред скриптовете (scripts)

Арабският упражнява и двете трансформации, поради което това е низът, с който трябва да се тества, и защо един арабски пас (Arabic pass) е най-силното единично доказателство, че конвейерът работи. Ивритът се нуждае от пренареждането, но не и от съединяването, тъй като неговите букви стоят самостоятелно; ако иврит се рендира правилно, но арабският излиза несвързан, двупосочната половина е добре и контекстуалната половина никога не е работила. Персийският и урду (Urdu) се движат върху арабския скрипт и наследяват неговото поведение, въпреки че предпочитанието на урду към стила Насталик (Nastaliq) е решение за шрифт с последствия за четливостта, които роден читател трябва да прецени

Тайландският седи изцяло от другата страна на линията. Той върви от-ляво-надясно, така че не се нуждае от двупосочна работа и буквите му не се съединяват, така че не се нуждае от контекстуален анализ; тайландските низове преминават през обикновения път TextOut като латиница. Това, което тайландският има, са подредени една върху друга марки (stacked marks) — гласни и тонални знаци над и под основната съгласна — и това дали те седят правилно зависи от шрифта, който изгражда своите комбиниращи се марки, за да се подреждат (stack) без помощта на shaping engine (енджин за оформяне). Повечето специализирани тайландски шрифтове го правят. Тествайте с точния шрифт, който ще вградите (embed), а не с негов двойник

Деванагари и останалата част от индийското (Indic) семейство са честната твърда спирка. Техните знаци за гласни се пренареждат около съгласните клъстери, а техните конюнкти (conjuncts) се формират чрез вериги от зависими от контекста замествания, което е пълна GSUB територия, отвъд пренареждане и съединяване. Ако в пътната карта има индийски локал, пуснете реален пилотен проект (pilot) върху истински клиентски низове, преди да го обещаете — това, че арабският работи, не е доказателство, че деванагари ще работи. CJK низове, виетнамски с неговите подредени диакритични знаци и смесен европейски текст всички минават по обикновения път без двупосочен анализ, и си струва да държите двата пътя физически разделени в кода на отчета – една рутина за RTL рънове (runs) и една за всичко останало, така че логиката на локала да е видима на мястото на извикване (call site), вместо да е скрита зад флаг, който някой забравя да зададе

Покритието на глифове (Glyph coverage) се решава дори преди оформянето (shaping) да стартира

Оформянето (Shaping) избира глифове от шрифт. Ако шрифтът не ги носи, няма какво да се избере, поради което класическият провал при внедряване (deployment) — безупречно на машината на разработчика, празни кутии на сървъра на клиента след безшумна замяна на шрифт — е проблем с покритието, а не проблем с оформянето. Практическото лекарство – регистриране на шрифт, който доставяте (ship), вместо да се доверявате на това, което дадена машина има инсталирано – е преминато стъпка по стъпка в референтната статия. Концептуалната точка е, че покритието трябва да се установи преди всеки въпрос за оформяне (shaping question) изобщо да е смислен, и че то може да се установи програмно, вместо чрез преценка на изхода на око

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

Самата регистрация носи две ограничения — долна граница PDF 1.5 за обработка на вграден (embedded) Unicode и битовете за разрешение за вграждане на шрифта — и двете обхванати заедно със стъпките за настройка в референтната статия за RtLTextOut. Това, което принадлежи тук, е навикът за одит: GetUnicodeGlyphForCodepoint е вашата система за ранно предупреждение. Преминете през обхватите от кодови точки, които вашите данни реално използват, когато услугата (service) стартира, и регистрирайте (log) какви ID-та на глифове се връщат назад. Пропускът в покритието след това се показва като ред в стартовия лог (startup log) по време на разпространение (rollout), а не като липсващи знаци във фактура, която вече е достигнала до клиент

Редът на четене принадлежи на документа, не на глифовете

Оправянето на всеки глиф все още оставя едно нещо несвършено. ISO 32000-1 §12.2 дефинира предпочитание за програмата за преглед (viewer preference), наречено /Direction, което указва цялостния ред на четене на документа. То не докосва глифове. Това, което прави, е да казва на програмата за преглед (viewer) как да подрежда разгъвания (spreads) от по две страници, от коя страна трябва да започне оформлението с обърнати една към друга страници (facing-page layout) и на коя страна трябва да се накланя потребителският интерфейс (UI) за четене. Нищо от това не се показва на една единствена страница, което е точно причината да се забравя

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

Задаването на Direction е цялата работа: setter-ът на свойството добавя vpDirection към ViewerPreferences на документа, така че един ред пренася предпочитанието във файла. Ако текстът излиза през RtLTextOut, получавате това безплатно, защото извикването обръща посоката на документа като страничен ефект — референтната статия покрива кога смесен документ се нуждае от отмяна на това (undone). Случаят, в който трябва да го зададете сами, е документ от-дясно-наляво, произведен по всякакъв друг начин, например от вход (input), който сте оформили (pre-shaped) предварително нагоре по веригата и сте начертали през обикновения път. Пропуснете го там и доказателството от една страница, в което се взирате, изглежда идентично и в двата случая; след това някой отпечатва дуплекс брошура, разгъванията излизат огледални и причината е липсващ един ред (one-liner) от преди седмици

Верифициране на оформен изход (shaped output)

Верифицирайте от край до край, защото една страница може да изглежда правилно и въпреки това да е безполезна за всичко надолу по веригата. Три проверки откриват повечето проблеми. Копирайте текста обратно от Acrobat и сравнете кодовите точки срещу вашия изходен низ (source string). Стартирайте търсенето в документа (in-document search) на програмата за преглед за дума, която можете да видите на страницата. И отворете изхода на машина, която няма вашите шрифтове за разработка, тази с най-голяма вероятност да разкрие подмяна (substitution). Нищо от това не замества роден читател, който разглежда един истински документ, което улавя неща, които никой синтетичен корпус няма да улови. Вкарайте този преглед (review) в календара, преди форматът да бъде пуснат (ships)

Избирайте тестови низове умишлено, вместо да рециклирате каквото е изпратил някой преводач миналата година. Работещ минимум за локал: изречение с чист скрипт, изречение с вложени латински имена на марки, ред, носещ цифри и валута, и имена с диакритични знаци или комбиниращи се марки (combining marks). Истински имена на клиенти разбиват предположения, които текстът за пълнеж (filler text) оставя недокоснати, така че оставете регресионният набор да расте с един низ всеки път, когато случай в поддръжката открие модел, който не сте виждали

Регистрацията на шрифтове, subsetting (създаване на подмножества) и всекидневното API за чертаене на текст са обхванати в статията за изход на отчети, шрифтове и изображения с HotPDF. Когато същите документи също трябва да отговарят на профили за достъпност, етикетирането на езика (language tagging) и правилата за структура в статията за валидиране на PDF/A и PDF/UA седят върху работата по оформянето (shaping) тук

APIs за от-дясно-наляво и Unicode шрифтове, описани по-горе, се доставят (ship) с компонента HotPDF за Delphi и C++Builder; продуктовата страница свързва към пълната справка за изход на текст