Odborný článok

Úrovne vloženia BiDi pre text PDF bez Uniscribe

Uniscribe robí viac práce, než si väčšina volajúcich uvedomuje. ScriptItemize vykonáva bidirekcionálnu analýzu a segmentáciu scriptov v jednom priebehu a ScriptLayout produkuje vizuálne poradie výsledných behov. HarfBuzz, prenosná náhrada, po ktorej ľudia siahajú, nespraví ani jedno: tvaruje jediný beh, ktorého smer a script už niekto iný rozhodol. Ťažká časť previesť textovú pipeline PDF z Windows na Linux či macOS preto nie je viazanie tvarovacieho enginu. Je to dodať bidirekcionálny algoritmus, ktorý Uniscribe potichu poskytoval, a v komponente PDFium je na to FPdfBidi

Jednotka implementuje UAX #9 priamo: pravidlá P2 a P3 pre smer odseku, X1 až X10 pre explicitné vloženia a izoláty, W1 až W7 pre slabé typy, N0 až N2 pre neutrály a zátvorky, I1 a I2 pre implicitné úrovne a L1 a L2 pre finálne preusporiadanie. Niosu to dve funkcie: PdfResolveBidiLevels vracia jednu úroveň vloženia na jednotku kódu UTF-16 a PdfBidiVisualOrder mení tie úrovne na permutáciu, ktorá ukladá jednotky kódu zľava doprava

Čo algoritmus dáva a čo nie

Dáva vám čísla. Sudé úrovne sú zľava doprava, nepárne sprava doľava a úroveň každého znaku kóduje zanorenie smerových behov, v ktorých sa znak nachádza. Z tých čísel odvodzuje L2 permutáciu. To, čo algoritmus zámerne nespraví, je rozhodnúť, ktorý font použiť, tvoriť ligatúry alebo preusporiadať glyfy vnútri klastra; toto sú starosti tvarovania a patria do etapy po tejto

Pipeline FPdfBidi pre text PDF bez Uniscribe: PdfResolveBidiLevels priraďuje úroveň vloženia UAX #9 na jednotku kódu UTF-16 a PdfBidiVisualOrder aplikuje pravidlo L2 na výrobu vizuálneho poradia
Úrovne kódujú zanorenie behov a pravidlo L2 mení úrovne na permutáciu, ktorá sa číta zľava doprava
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto aplikuje P2-P3: prvý silný znak rozhodne
  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 sa teraz číta zľava doprava; Levels[] stále hovorí, ktoré
    // behy sú RTL, takže shaperovi možno odovzdať správne smery
  end;
end;

Tabuľka tried znakov sa generuje, nepíše sa

Každý kódový bod má vlastnosť Bidi_Class a algoritmus sa na ňu neustále pýta, takže tabuľka je základňa, na ktorej stojí všetko ostatné. Generuje sa z Unicode Character Database namiesto ručnej údržby: pole päť z UnicodeData.txt dáva pridelené triedy a deklarácie @missing v DerivedBidiClass.txt dávajú predvolené pre kódové body, ktoré databáza nepridelí, čo je spôsob, akým nepridelené bloky správne prepadajú na R, AL, ET či BN namiesto na L

Trikom kompresie je emitovať len rozsahy, ktorej trieda nie je L. Čokoľvek padajúce mimo každý rozsah je L, čo je aj predvolené Unicode aj trieda prevažnej väčšiny kódových bodov. Tým sa tabuľka, ktorá by inak bežala do tisícok záznamov, zníži na 745 rozsahov a približne 6,7 KB. Prevádzkový dôsledok stojí za vyslovenie: keď prechádzate na novú verziu Unicode, spustite generátor znovu. Ručná úprava include súboru bude fungovať a tiež sa potichu odkloní od databázy pri ďalšom upgrade

L2 musí preusporiadať kódové body, nie jednotky kódu UTF-16

Toto je chyba, ktorá produkuje skutočne poškodený výstup, a prvá implementácia ju urobila. L2 hovorí prevrátiť súvislé behy na každej úrovni od najvyššej po najnižšiu nepárnu úroveň. Napísané voči reťazcu UTF-16 znamená „prevrátiť beh“ prirodzene prevrátenie jednotiek kódu v ňom. Pre znaky v Basic Multilingual Plane je to v poriadku. Pre znak RTL v astrálnej rovine, ako sú tie v blokoch kyperskej či starojuhoarabskej abecedy blízko U+10800, nie: znak je dvojica surrogátov, prevrátenie behu dá nízky surrogát pred vysoký a reťazec teraz obsahuje dva nespárované surrogáty namiesto jedného znaku. Nič po prúde to nedokáže obnoviť

Opravou je robiť L2 na jednotkách kódových bodov. Implementácia zlučuje jednotky kódu na jednotky kódových bodov, vykonáva prevrátenia na týchto jednotkách a na konci rozšíri výsledok späť na indexy jednotiek kódu. Preto PdfBidiVisualOrder berie text, nie len pole úrovní: z úrovní samotných nedokáže povedať, kde sú hranice surrogátov. Tá istá disciplína dvojíc surrogátov prebieha textovými API všeobecne, ako popisuje článok o emodži, CJK a dvojiciach surrogátov

Poškodenie dvojice surrogátov pri bidirekcionálnom preusporiadaní: prevrátenie jednotiek kódu UTF-16 rozdelí astrálny znak blízko U+10800 na nespárované surrogáty, zatiaľ čo prevrátenie zlúčených jednotiek kódových bodov ho udrží neporušený
Pravidlo L2 musí zlúčiť jednotky kódu na kódové body pred prevrátením a potom ich rozšíriť späť

Zostup cez úrovne musí zahŕňať úrovne, ktoré sa nevyskytujú

Druhá chyba je jemnejšia a neprodukuje pád, len text, ktorý nie je preusporiadaný. L2 hovorí začať od najvyššej prítomnej úrovne a pracovať nadol po najnižšiu nepárnu úroveň. Prirodzená optimalizácia je zozbierať sadu úrovní, ktoré sa skutočne vyskytujú, a iterovať po tejto sade. Je zlá

Zvážte riadok latinského textu vnútri vloženia sprava doľava. Úroveň odseku je 0, vloženie tlačí latinské znaky na úroveň 2 a žiadny znak nesedí na úrovni 1. Iterovanie po vyskytujúcich sa úrovniach nájde len 0 a 2 a žiadna nepárna úroveň nie je, takže cyklus nevykoná žiadne prevrátenie. Tá odpoveď je správna, ale z dôvodu, ktorý optimalizácia nevie: prevrátenie na úrovni 2 nasledované prevrátením na úrovni 1 by sa presne anulovalo, takže nevykonať ani jedno je správny výsledok. Zmente vstup mierne tak, aby existovali znaky úrovní 1 aj 3, ale úroveň 2 nie, a cyklus založený na sade preskočí prevrátenie úrovne 2, ktoré algoritmus vyžaduje

// Správne: kráčať každou úrovňou od maxima po najnižšiu nepárnu
// úroveň, vrátane úrovní, ktoré žiadny znak skutočne nemá
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // no-op, keď nevyhovuje žiadny beh
  Dec(Level);
end;

Napísané ako obyčajný zostupný cyklus správanie vypadá zadarmo a no-op iterácie nestoja nič merateľné. Toto je prípad, kde zjavná optimalizácia nie je mierne zlá, je zlá spôsobom závislým od vstupu, ktorý malý testovací korpus nikdy neodhalí

Pasca zostupu úrovní BiDi v UAX #9: iterovanie len vyskytujúcich sa úrovní preskočí potrebné prevrátenie úrovne 2, zatiaľ čo obyčajný zostupný cyklus z MaxLevel po najnižšiu nepárnu úroveň vždy preusporiada správne
Kráčanie každou úrovňou po najnižšiu nepárnu nestojí nič a nikdy nepreskočí potrebné prevrátenie

Zátvorky: BD16 s pragmatickou tabuľkou

Pravidlo N0 a algoritmus párov zátvoriek BD16 existujú preto, aby zátvorka v texte so zmiešanými smermi sa rozriešila na smer toho, čo obaľuje, namiesto toho, čo sa náhodou nachádza vedľa. To potrebuje tabuľku párov zátvoriek. Implementácia nesie páry vo všeobecnom použití namiesto plného obsahu súboru zátvoriek Unicode: ASCII, CJK, plnej šírky, matematické a ozdobné zátvorky

Nevypísaná zátvorka nie je chyba. Rozrieši sa ako obyčajný neutrálny cez N1 a N2, čo je presne správanie, ktoré mala každá implementácia predtým, než Unicode 6.3 zaviedol N0. Takže hranica je „menej rafinovaná pre zriedkavé zátvorky“, nie „nesprávna“. Jeden detail potrebuje explicitné zaobchádzanie: kanonická ekvivalencia medzi lomenými zátvorkami na U+2329 a U+232A a tými na U+3008 a U+3009 sa musí zložiť pri párovaní, inak otvárajúca zátvorka napísaná jedným spôsobom sa nezopáruje so zatvárajúcou napísanou druhým

Ako testujete tridsať interagujúcich pravidiel

Nie veľkým korpusom, aspoň nie najprv. Produktívny prístup bolo šestnásť ručne overených prípadov, každý zvolený na precvičenie konkrétneho pravidla a každý skontrolovaný voči úrovniam, ktoré UAX #9 hovorí, že by mal produkovať: detekcia smeru odseku pod P2 a P3, pravidlá slabých typov W2, W3 a W7, pravidlá implicitných úrovní I1 a I2, explicitné vloženie cez X2 a X7, izoláty cez X5a a X6a, reset L1 koncových bielych miest a oddeľovačov, prípad zátvorky N0 a jeden prípad s astrálnym znakom na zamknutie zaobchádzania so surrogátmi

Šestnásť prípadov so známo-správnymi očakávanými úrovňami chytí viac než tisícšeststo prípadov s výstupom vyzerajúcim pravdepodobne, pretože režim zlyhania bidirekcionálnej implementácie je text, ktorý sa číta takmer správne. Keď tie prejdú, korpus je užitočný na hľadanie medzier tabuľky a problémov výkonu, čo sú odlišné triedy defektov

Vnútri komponentu PDFium úrovne kŕmia dvoch konzumentov. Na strane zápisu hovoria tvarovaciemu backendu smer každého behu, čo je vstup, ktorý HarfBuzz vyžaduje. Na strane čítania informujú geometriu výberu a poradie čítania, keďže kliknutie v texte RTL sa musí namapovať na logickú pozíciu, nie vizuálnu; toto mapovanie pokrýva článok o výbere vizuálneho riadku a model poradia čítania štrukturálne textové bloky a poradie čítania. Detaily podpory platforiem komponentu sú na produktovej stránke PDFium Delphi component