Uniscribe върши повече работа, отколкото повечето извикващи осъзнават; ScriptItemize извършва bidirectional анализ и скриптова сегментация в един проход, а ScriptLayout произвежда визуалния ред на получените runs; HarfBuzz, портабилната замяна, към която хората се хващат, не прави нито едното: той оформя единичен run, чиято посока и скрипт вече са били решени от някой друг; Така че трудната част от занасянето на Windows PDF текстов конвейер на Linux или macOS не е обвързване на shaping engine; Това е доставянето на bidirectional алгоритъма, който Uniscribe тихо предоставяше, а в PDFium компонента точно за това е FPdfBidi
Модулът имплементира UAX #9 директно: правила P2 и P3 за посока на абзац, X1 до X10 за изрични embeddings и isolates, W1 до W7 за слаби типове, N0 до N2 за неутрални и скоби, I1 и I2 за имплицитни нива и L1 и L2 за финалното пренареждане; Две функции го носят: PdfResolveBidiLevels връща едно embedding ниво на UTF-16 кодова единица, а PdfBidiVisualOrder превръща тези нива в пермутацията, която поставя кодови единици отляво надясно
Какво алгоритъмът ви дава и какво не
Дава ви числа; Четните нива са отляво надясно, нечетните отдясно наляво и нивото на всеки символ кодира влагането на направени run-ове, вътре в които символът седи; От тези числа L2 извежда пермутация; Това, което алгоритъмът нарочно не прави, е да реши кой шрифт да използва, да образува лигатури или да пренареди глифи вътре в cluster; тези са shaping грижи и принадлежат на етапа след този
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto прилага P2-P3: първият силен символ решава
if PdfResolveBidiLevels(Text, pbdAuto, Levels, ParagraphLevel) then
begin
Order := PdfBidiVisualOrder(Text, Levels);
SetLength(Visual, Length(Order));
for I := 0 to High(Order) do
Visual[I + 1] := Text[Order[I] + 1];
// Visual вече се чете отляво надясно; Levels[] все още казва кои
// runs са RTL, така че shaper може да получи правилни посоки
end;
end;
Таблицата със символни класове се генерира, не се пише
Всеки кодов пункт има свойство Bidi_Class и алгоритъмът се консултира с него постоянно, така че таблицата е фундаментът, върху който всичко останало стои; Тя се генерира от Unicode Character Database, а не се поддържа на ръка: поле пет от UnicodeData.txt дава възложените класове, а декларациите @missing в DerivedBidiClass.txt дават подразбиращите се за кодови пунктове, които базата данни не възлага, което е начинът неразпределените блокове правилно да подразбират R, AL, ET или BN, а не L
Трикът за компресия е да се излъчат само диапазоните, чийто клас не е L; Всичко, което пада извън всеки диапазон, е L, което е едновременно Unicode подразбирането и класът на подавляващото мнозинство кодови пунктове; Това взема таблица, която иначе би се изтеглила до хиляди записи, надолу до 745 диапазона и около 6.7 KB; Оперативната последица си заслужава да се каже: когато преминете към нова Unicode версия, пуснете наново генератора; Ръчното редактиране на include файла ще работи и то също ще се размине мълшиво с базата данни при следващото надграждане
L2 трябва да пренарежда кодови пунктове, не UTF-16 кодови единици
Това е грешката, която произвежда наистина повреден изход и първата имплементация я направи; L2 казва да се обърнат съседни runs на всяко ниво от най-високото надолу до най-ниското нечетно ниво; Написано спрямо UTF-16 низ, „обърни run“ естествено значи обръщане на кодовите единици в него; За символи в Basic Multilingual Plane това е добре; За RTL символ в астрална равнина, като тези в блоковете кипрски или староюжноарабски около U+10800, не е: символът е surrogate двойка, обръщането на run слага ниската surrogate преди високата и низът сега съдържа две несдвоени surrogates вместо един символ; Нищо надолу по веригата не може да го възстанови
Поправката е L2 да се прави върху кодово-пунктови единици; Имплементацията слива кодови единици в кодово-пунктови единици, извършва обръщанията върху тези единици и разширява резултата обратно към кодово-единични индекси накрая; Това е причината PdfBidiVisualOrder да приема текста, а не само масива с нива: той не може да каже къде са surrogate границите само от нивата; Същата surrogate-двойкова дисциплина минава през текстовите API изобщо, както е описано в статията за emoji, CJK и surrogate двойки
Спускането през нива трябва да включва нива, които не се срещат
Втората грешка е по-фина и не произвежда срив, само текст, който не е пренареден; L2 казва да се започне от най-високото налично ниво и да се работи надолу до най-ниското нечетно ниво; Естествена оптимизация е да се събере множеството от нива, които действително се срещат и да се итерира върху това множество; Тя е грешна
Вземете ред латински текст вътре в отдясно-наляво embedding; Нивото на абзаца е 0, embedding-ът бута латинските символи на ниво 2 и никой символ не седи на ниво 1; Итерирането върху срещащи се нива намира само 0 и 2 и изобщо няма нечетно ниво, така че цикълът не извършва обръщане; Този отговор е правилен, но по причина, която оптимизацията не знае: обръщане на ниво 2, последвано от обръщане на ниво 1, би се отменило точно, така че изпълнението на нито едното е правилният изход; Променете входа леко, така че да съществуват и символи на ниво 1, и на ниво 3, но ниво 2 да няма, и цикълът, базиран на множество, пропуска обръщането на ниво 2, което алгоритъмът изисква
// Правилно: обходете всяко ниво от максималното надолу до най-ниското
// нечетно ниво, включително нива, които никой символ действително няма
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op, когато нито един run отговаря
Dec(Level);
end;
Написано като обикновен намаляващ цикъл поведението изтича безплатно и no-op итерациите не струват нищо измеримо; Това е случай, в който очевидната оптимизация не е леко грешна, тя е грешна по вход-зависим начин, който малък тестов корпус никога няма да разкрие
Скоби: BD16 с прагматична таблица
Правило N0 и BD16 алгоритъмът за скобни двойки съществуват, така че скоба в текст с смесени посоки резолвира към посоката на това, което обгръща, а не към каквото случайно е съседно; Това се нуждае от таблица със скобни двойки; Имплементацията носи двойките в обща употреба, а не пълното съдържание на Unicode скобния файл: ASCII, CJK, fullwidth, математически и орнаментални скоби
Неизброена скоба не е грешка; Тя резолвира като обикновен неутрален през N1 и N2, което е точно поведението, което всяка имплементация е имала, преди Unicode 6.3 да въведе N0; Така че границата е „по-малко изтънена за редки скоби“, не „неправилно“; Един детайл се нуждае от изрична обработка: каноничната еквивалентност между ъгловите скоби на U+2329 и U+232A и тези на U+3008 и U+3009 трябва да бъде слята при съпоставяне на двойки, иначе отваряща скоба, написана по един начин, няма да се сдвои със затваряща скоба, написана по другия
Как тествате трийсет взаимодействащи правила
Не с голям корпус, поне не първо; Продуктивният подход беше шестнайсет ръчно верифицирани случая, всеки избран да упражни конкретно правило и всеки проверен спрямо нивата, които UAX #9 казва, че трябва да произведе: откриване на посока на абзац под P2 и P3, правилата за слаби типове W2, W3 и W7, правилата за имплицитни нива I1 и I2, изричен embedding чрез X2 и X7, isolates чрез X5a и X6a, L1 нулирането на завършващи интервали и разделители, N0 скобен случай и един случай с астрален символ, за да закарае surrogate обработката
Шестнайсет случая с известни-правилни очаквани нива хващат повече от шестнайсетстотин случая с правдоподобно изглеждащ изход, защото режимът на отказ на bidirectional имплементация е текст, който се чете почти право; Щом тези минат, корпус е полезен за намиране на дупки в таблицата и проблеми с производителността, които са различни класове дефекти
Вътре в PDFium компонента нивата захранват два консуматора; От страната на писането те казват на shaping бекенда посоката на всеки run, която е входът, който HarfBuzz изисква; От страната на четенето те информират геометрията на селекция и реда на четене, тъй като клик в RTL текст трябва да се съпостави на логическа позиция, а не визуална; това съпоставяне е разгледано в статията за визуална селекция на редове а моделът ред на четене в структурни текстови блокове и ред на четене; Детайлите за поддръжка на платформи за компонента са на продуктовата страница PDFium Delphi component