Uniscribe robi więcej roboty, niż większość wywołujących sobie uświadamia. ScriptItemize wykonuje analizę dwukierunkową i segmentację skryptów w jednym przebiegu, a ScriptLayout produkuje kolejność wizualną powstałych przebiegów. HarfBuzz, przenośny zamiennik, po który sięgają ludzie, nie robi ani jednego, ani drugiego: kształtuje pojedynczy przebieg, którego kierunek i skrypt ktoś inny już zadecydował. Trudną częścią przeniesienia windowsowego potoku tekstu PDF na Linuksa albo macOS nie jest więc podpięcie silnika kształtowania. To dostarczenie algorytmu dwukierunkowego, który Uniscribe po cichu dostarczał, a w komponencie PDFium po to właśnie jest FPdfBidi
Jednostka implementuje UAX #9 wprost: reguły P2 i P3 dla kierunku akapitu, X1 przez X10 dla jawnych zagnieżdżeń i izolatów, W1 przez W7 dla typów słabych, N0 przez N2 dla neutralnych i nawiasów, I1 i I2 dla poziomów niejawnych oraz L1 i L2 dla końcowego przestawiania. Dwie funkcje to niosą: PdfResolveBidiLevels zwraca jeden poziom zagnieżdżenia na jednostkę kodową UTF-16, a PdfBidiVisualOrder zamienia te poziomy w permutację, która kładzie jednostki kodowe od lewej do prawej
Co algorytm ci daje, a czego nie
Daje ci liczby. Poziomy parzyste to od lewej do prawej, nieparzyste od prawej do lewej, a poziom każdego znaku koduje zagnieżdżenie przebiegów kierunkowych, w których ten znak siedzi. Z tych liczb L2 wyprowadza permutację. Tego, czego algorytm celowo nie robi, to decydowanie, którego fontu użyć, tworzenie ligatur ani przestawianie glifów wewnątrz klastra; to są sprawy kształtowania i należą do etapu po tym
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto stosuje P2-P3: pierwszy mocny znak decyduje
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 czyta się teraz od lewej do prawej; Levels[] wciąż mówi,
// które przebiegi są RTL, więc kształtownik dostanie poprawne kierunki
end;
end;
Tabela klas znaków jest generowana, nie pisana
Każdy punkt kodowy ma własność Bidi_Class, a algorytm konsultuje ją nieustannie, więc tabela jest fundamentem, na którym stoi wszystko inne. Jest generowana z Unicode Character Database, a nie pielęgnowana ręcznie: pole piąte UnicodeData.txt daje klasy przydzielone, a deklaracje @missing w DerivedBidiClass.txt dają domyślne dla punktów kodowych, których baza nie przydziela, i dzięki temu nieprzydzielone bloki domyślnie słusznie do R, AL, ET albo BN, a nie do L
Trik kompresji to emitowanie tylko zakresów, których klasa nie jest L. Cokolwiek wypadnie poza każdy zakres, jest L, co jest i domyślnym Unicode, i klasą przytłaczającej większości punktów kodowych. To zbiera tabelę, która inaczej biegłaby do tysięcy wpisów, do 745 zakresów i około 6,7 KB. Konsekwencja operacyjna warta powiedzenia: gdy przechodzisz na nową wersję Unicode, uruchom generator ponownie. Ręczna edycja pliku include zadziała i po cichu też rozjedzie się z bazą przy następnej aktualizacji
L2 musi przestawiać punkty kodowe, nie jednostki kodowe UTF-16
To jest błąd produkujący naprawdę uszkodzone wyjście i pierwsza implementacja go popełniła. L2 mówi o odwracaniu ciągłych przebiegów na każdym poziomie od najwyższego w dół do najniższego nieparzystego. Zapisane względem łańcucha UTF-16 „odwróć przebieg” naturalnie znaczy odwrócenie jednostek kodowych w nim. Dla znaków w Basic Multilingual Plane to w porządku. Dla znaku RTL w płaszczyźnie astralnej, jak te w blokach cypryjskim albo starożytnego południowoarabskiego koło U+10800, już nie: znak jest parą surogatów, odwrócenie przebiegu stawia surogat niski przed wysokim, a łańcuch zawiera teraz dwa niesparowane surogaty zamiast jednego znaku. Nic dalej w potoku tego nie naprawi
Poprawka to robienie L2 na jednostkach punktów kodowych. Implementacja scala jednostki kodowe w jednostki punktów kodowych, wykonuje odwrócenia na tych jednostkach i rozwija wynik z powrotem do indeksów jednostek kodowych na końcu. Dlatego PdfBidiVisualOrder bierze tekst, a nie tylko tablicę poziomów: nie potrafi powiedzieć z samych poziomów, gdzie są granice surogatów. Ta sama dyscyplina par surogatów przechodzi przez API tekstu ogólnie, co opisuje artykuł o emoji, CJK i parach surogatów
Zejście przez poziomy musi obejmować poziomy, które nie występują
Drugi błąd jest subtelniejszy i nie produkuje awarii, tylko tekst, który nie został przestawiony. L2 mówi, by zacząć od najwyższego obecnego poziomu i zejść do najniższego nieparzystego. Naturalna optymalizacja to zebranie zbioru poziomów faktycznie występujących i iterowanie po tym zbiorze. To jest złe
Rozważ linię tekstu łacińskiego wewnątrz zagnieżdżenia od prawej do lewej. Poziom akapitu to 0, zagnieżdżenie pcha znaki łacińskie na poziom 2 i żaden znak nie siedzi na poziomie 1. Iterowanie po występujących poziomach znajduje tylko 0 i 2, a nieparzystego poziomu nie ma wcale, więc pętla nie wykonuje żadnego odwrócenia. Ta odpowiedź jest poprawna, ale z powodu, którego optymalizacja nie zna: odwrócenie na poziomie 2, po którym następuje odwrócenie na poziomie 1, anuluje się dokładnie, więc niewykonanie żadnego to właściwy wynik. Zmień wejście nieznacznie, tak by istniały znaki poziomu 1 i poziomu 3, ale nie poziomu 2, a pętla oparta na zbiorze pomi odwrócenie poziomu 2, którego algorytm wymaga
// Poprawnie: przechodź każdy poziom od maksimum w dół do najniższego
// nieparzystego, łącznie z poziomami, których żaden znak faktycznie nie ma
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op, gdy żaden przebieg się kwalifikuje
Dec(Level);
end;
Zapisany jako zwykła pętla dekrementująca zachowanie wypada samo i iteracje no-op nie kosztują nic mierzalnego. To przypadek, gdzie oczywista optymalizacja nie jest lekko zła, jest zła w sposób zależny od wejścia, którego mały korpus testowy nigdy nie ujawni
Nawiasy: BD16 z pragmatyczną tabelą
Reguła N0 i algorytm par nawiasów BD16 istnieją po to, by nawias w tekście o mieszanych kierunkach rozwiązywał się do kierunku tego, co obejmuje, a nie do tego, co akurat jest obok. To potrzebuje tabeli par nawiasów. Implementacja niesie pary w powszechnym użyciu, a nie pełną zawartość pliku nawiasów Unicode: ASCII, CJK, pełnej szerokości, matematyczne i ozdobne
Nawias niewymieniony nie jest błędem. Rozwiązuje się jako zwyczajny neutralny przez N1 i N2, czyli dokładnie zachowanie, jakie miała każda implementacja, zanim Unicode 6.3 wprowadził N0. Granica jest więc „mniej dopracowana dla rzadkich nawiasów”, a nie „niepoprawna”. Jeden szczegół wymaga jawnego traktowania: kanoniczna równoważność między nawiasami kątowymi na U+2329 i U+232A a tymi na U+3008 i U+3009 musi być złożona przy dopasowywaniu par, bo nawias otwierający zapisany jednym sposobem nie sparuje się z nawiasem zamykającym zapisanym drugim
Jak testować trzydzieści współdziałających reguł
Nie dużym korpusem, przynajmniej nie na początku. Produktywne podejście to szesnaście przypadków zweryfikowanych ręcznie, każdy wybrany, by ćwiczyć konkretną regułę, i każdy sprawdzony względem poziomów, które UAX #9 mówi, że powinien wyprodukować: detekcja kierunku akapitu pod P2 i P3, reguły typów słabych W2, W3 i W7, reguły poziomów niejawnych I1 i I2, jawne zagnieżdżenie przez X2 i X7, izolaty przez X5a i X6a, reset L1 końcowej białej spacji i separatorów, przypadek nawiasu N0 oraz jeden przypadek ze znakiem astralnym, by przypiąć obsługę surogatów
Szesnaście przypadków ze znanymi poprawnymi oczekiwanymi poziomami łapie więcej niż szesnaścieset przypadków z wyjściem wyglądającym wiarygodnie, bo trybem awarii implementacji dwukierunkowej jest tekst czytający się prawie dobrze. Gdy te przejdą, korpus jest użyteczny do znajdowania luk tabeli i problemów wydajności, które są innymi klasami defektów
Wewnątrz komponentu PDFium poziomy zasilają dwóch konsumentów. Po stronie zapisu mówią backendowi kształtowania kierunek każdego przebiegu, co jest wejściem, którego wymaga HarfBuzz. Po stronie odczytu informują geometrię zaznaczenia i kolejność czytania, bo kliknięcie w tekście RTL musi mapować się na pozycję logiczną, a nie wizualną; to mapowanie omawia artykuł o zaznaczaniu linii wizualnych, a model kolejności czytania bloki tekstu strukturalnego i kolejność czytania. Szczegóły wsparcia platform komponentu są na stronie produktu PDFium Delphi component