Tehnički članak

BiDi nivoi ugrađivanja za PDF tekst bez Uniscribe-a

Uniscribe radi više posla nego što većina pozivalaca shvata. ScriptItemize izvodi dvosmjernu analizu i segmentaciju pisma u jednom prolazu, i ScriptLayout proizvodi vizuelni redosled rezultujućih nizova. HarfBuzz, prenosiva zamena za koju se ljudi okreću, ne radi ni jedno: oblikuje jedan niz čije su smer i pismo već odlučeni od nekog drugog. Pa teški deo nošenja Windows PDF tekst pipeline-a na Linux ili macOS nije vezivanje motora oblikovanja. To je snabdevanje dvosmjernim algoritmom koji je Uniscribe tiho obezbeđivao, i u PDFium komponenti to je ono čemu služi FPdfBidi

Jedinica implementira UAX #9 direktno: pravila P2 i P3 za smer pasusa, X1 do X10 za eksplicitna ugrađivanja i izolate, W1 do W7 za slabe tipove, N0 do N2 za neutrale i zagrade, I1 i I2 za implicitne nivoe, i L1 i L2 za konačno preuređivanje. Dve funkcije nose to: PdfResolveBidiLevels vraća jedan nivo ugrađivanja po UTF-16 kodnoj jedinici, i PdfBidiVisualOrder pretvara te nivoe u permutaciju koja postavlja kodne jedinice sleva-udesno

Šta algoritam vam daje, a što ne

Daje vam brojeve. Parani nivoi su sleva-udesno, neparni sdesna-ulevo, i nivo svakog znaka koduje ugnežđavanje dvosmjernih nizova u koje znak sedi. Iz tih brojeva L2 izvodi permutaciju. Ono što algoritam namerno ne radi jeste odlučiti koji font koristiti, oblikovati ligature, ili preurediti glifove unutar klastera; to su brige oblikovanja i pripadaju etapi posle ove

FPdfBidi pipeline za PDF tekst bez Uniscribe-a: PdfResolveBidiLevels dodeljuje nivo ugrađivanja UAX #9 po UTF-16 kodnoj jedinici i PdfBidiVisualOrder primenjuje pravilo L2 da proizvede vizuelni redosled
Nivoi koduju ugnežđavanje nizova, i pravilo L2 pretvara ih u permutaciju koja se čita sleva-udesno
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto primenjuje P2-P3: prvi jak znak odlučuje
  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 sada čita sleva-udesno; Levels[] i dalje kaže koji
    // su nizovi RTL pa oblikovaču mogu biti predati ispravni smerovi
  end;
end;

Tabela klasa znakova generisana je, ne pisana

Svaka kodna tačka ima svojstvo Bidi_Class, i algoritam ga stalno konsultuje, pa je tabela temelj na kojem sve ostalo stoji. Generisana je iz Unicode Character Database umesto održavana rukom: peto polje UnicodeData.txt daje dodeljene klase, i deklaracije @missing u DerivedBidiClass.txt daju podrazumevane za kodne tačke koje baza ne dodeljuje, što je način na koji nedodeljeni blokovi ispravno podrazumevaju R, AL, ET ili BN umesto L

Trik kompresije je emitovati samo opsege čija klasa nije L. Sve što pada van svakog opsega jeste L, što je i Unicode podrazumevano i klasa pretežne većine kodnih tačaka. To uzima tabelu koja bi inače išla na hiljade unosa na 745 opsega i oko 6.7 KB. Operativna posledica vredi izjaviti: kada pređete na novu Unicode verziju, ponovo pokrenite generator. Ručno uređivanje include datoteke radiće, i takođe će se tiho udaljiti od baze pri sledećoj nadogradnji

L2 mora preurediti kodne tačke, ne UTF-16 kodne jedinice

Ovo je greška koja proizvodi istinski oštećen izlaz, i prva implementacija ju je učinila. L2 kaže obrnuti kontinuirane nizove na svakom nivou od najvišeg nadole do najnižeg neparnog nivoa. Napisano protiv UTF-16 niza znakova, "obrni niz" prirodno znači obrnuti kodne jedinice u njemu. Za znakove u Osnovnoj višejezičnoj ravni to je u redu. Za RTL znak u astralnoj ravnini, poput onih u Ciparskim ili Starojuznoarapskim blokovima blizu U+10800, nije: znak je surrogatni par, obrtanje niza stavlja niski surrogat pre visokog, i niz znakova sada sadrži dva nesparenja surrogata umesto jednog znaka. Ništa nizvodno ne može to oporaviti

Popravka je raditi L2 na jedinicama kodnih tačaka. Implementacija spaja kodne jedinice u jedinice kodnih tačaka, izvršava obrtanja na tim jedinicama, i širi rezultat nazad u indekse kodnih jedinica na kraju. Zato PdfBidiVisualOrder prima tekst, a ne samo niz nivoa: ne može reći gde su granice surrogata samo iz nivoa. Ista disciplina surrogatnih parova prolazi kroz tekstualne API-je opšte, kao što je opisano u članku o emotikonima, CJK i surrogatnim parovima

Oštećenje surrogatnog para u dvosmjernom preuređivanju: obrtanje UTF-16 kodnih jedinica cepa astralni znak blizu U+10800 u nesparenje surrogate, dok obrtanje spojenih jedinica kodnih tačaka čuva netaknutim
Pravilo L2 mora spojiti kodne jedinice u kodne tačke pre obrtanja, pa ih naknadno raširiti nazad

Silazak kroz nivoe mora uključiti nivoe koji se ne javljaju

Druga greška suptilnija je i ne proizvodi pad, samo tekst koji nije preuređen. L2 kaže početi od najvišeg prisutnog nivoa i raditi nadole do najnižeg neparnog nivoa. Prirodna optimizacija je sakupiti skup nivoa koji stvarno postoje i iterirati preko tog skupa. Pogrešna je

Uzmite liniju latiničnog teksta unutar ugrađivanja sdesna-ulevo. Nivo pasusa je 0, ugrađivanje gura latinične znakove na nivo 2, i nijedan znak ne sedi na nivou 1. Iteriranje preko postojećih nivoa nalazi samo 0 i 2, i nema neparnog nivoa uopšte, pa petlja ne izvršava obrtanje. Taj odgovor ispravan je, ali iz razloga koji optimizacija ne zna: obrtanje na nivou 2 praćeno obrtanjem na nivou 1 poništilo bi se tačno, pa izvršenje nijednog ispravan je ishod. Promenite ulaz malo, tako da i nivo 1 i nivo 3 znakovi postoje ali nivo 2 ne, i petlja zasnovana na skupu preskače obrtanje nivoa 2 koje algoritam traži

// Ispravno: hodajte svaki nivo od maksimuma nadole do najnižeg
// neparnog nivoa, uključujući nivoe koje nijedan znak zapravo nema
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // bez dejstva kada nijedan niz ne ispunjava uslov
  Dec(Level);
end;

Napisano kao obična petlja umanjivanja ponašanje ispada besplatno, i iteracije bez dejstva ne koštaju ništa merljivo. Ovo je slučaj gde očigledna optimizacija nije malo pogrešna, pogrešna je na način zavisnom od ulaza koji mali test korpus nikad neće otkriti

Zamka silaska nivoa BiDi u UAX #9: iteriranje samo nivoa koji postoje preskače potrebno obrtanje nivoa 2, dok obična petlja umanjivanja od MaxLevel do najnižeg neparnog nivoa uvek preuređuje ispravno
Hodanje svakog nivoa nadole do najnižeg neparnog ne košta ništa i nikad ne preskače potrebno obrtanje

Zagrade: BD16 sa pragmatičnom tabelom

Pravilo N0 i BD16 algoritam parova zagrada postoje da zagrada u tekstu mešanog smera razreši se u smer onoga što obuhvata umesto u šta god se slučajno nalazi pored. To traži tabelu parova zagrada. Implementacija nosi parove u opštoj upotrebi umesto punog sadržaja Unicode datoteke zagrada: ASCII, CJK, punu-širinu, matematičke i ukrasne zagrade

Nenavedena zagrada nije greška. Razrešava se kao običan neutral kroz N1 i N2, što je tačno ponašanje koje je svaka implementacija imala pre nego što je Unicode 6.3 uneo N0. Pa je granica "manje profinjeno za retke zagrade", ne "neispravno". Jedan detalj traži eksplicitno rukovanje: kanonička ekvivalencija između ugaonih zagrada na U+2329 i U+232A i onih na U+3008 i U+3009 mora se saviti pri poklapanju parova, ili otvorena zagrada napisana jednim načinom otkazaće uparivanje sa zatvorenom napisanom drugim

Kako testirati trideset međusobno delujućih pravila

Ne velikim korpusom, barem ne prvo. Plodan pristup bilo je šesnaest ručno verifikovanih slučajeva, svaki izabran da vežba specifično pravilo i svaki proveren protiv nivoa koje UAX #9 kaže da bi trebalo proizvesti: detekcija smera pasusa pod P2 i P3, pravila slabog tipa W2, W3 i W7, pravila implicitnog nivoa I1 i I2, eksplicitno ugrađivanje putem X2 i X7, izolate putem X5a i X6a, L1 resetovanje pratećih razmaka i razdelnika, N0 slučaj zagrade, i jedan slučaj sa astralnim znakom da se prikova rukovanje surrogatima

Šesnaest slučajeva sa poznato-ispravnim očekivanim nivoima hvata više od šesnaest stotina slučajeva sa izlazom koji deluje uverljivo, jer je režim otkazivanja dvosmerne implementacije tekst koji se čita skoro ispravno. Kada ti prođu, korpus koristan je za nalaženje jazova tabele i problema performansi, koji su različite klase defekta

Unutar PDFium komponente nivoi hrane dva potrošača. Na strani pisanja govore backend-u oblikovanja smer svakog niza, što je ulaz koji HarfBuzz traži. Na strani čitanja obaveštavaju geometriju izbora i redosled čitanja, pošto klik u RTL tekstu mora mapirati u logički položaj umesto vizuelni; to mapiranje pokriveno je u članku o izboru vizuelne linije a model redosleda čitanja u strukturiranim blokovima teksta i redosledu čitanja. Detalji podrške platformi za komponentu nalaze se na stranici proizvoda PDFium Delphi component