Tehnički članak

Dvosmjerne razine ugnježđivanja za PDF tekst bez Uniscribea

Uniscribe radi više posla nego što većina pozivatelja shvaća. ScriptItemize izvodi dvosmjernu analizu i segmentaciju po pismu u jednom prolazu, a ScriptLayout proizvodi vizualni red nastalih nizova. HarfBuzz, prijenosna zamjena za koju se ljudi odlučuju, ne čini ni jedno ni drugo: oblikuje jedan niz čije su smjer i pismo već odlučeni od nekoga drugoga. Dakle teški dio premještanja Windows cjevovoda PDF teksta na Linux ili macOS nije vezivanje motora oblikovanja. To je opskrba dvosmjernim algoritmom koji je Uniscribe tiho pružao, i u PDFium komponenti upravo za to služi FPdfBidi

Jedinica implementira UAX #9 izravno: pravila P2 i P3 za smjer odlomka, X1 do X10 za izričita ugnježđivanja i izolate, W1 do W7 za slabe tipove, N0 do N2 za neutralne znakove i zagrade, I1 i I2 za implicitne razine, te L1 i L2 za konačno preuređenje. Dvije funkcije nose to: PdfResolveBidiLevels vraća jednu razinu ugnježđivanja po UTF-16 jedinici koda, a PdfBidiVisualOrder pretvara te razine u permutaciju koja smješta jedinice koda slijeva nadesno

Što algoritam daje, a što ne daje

Daje vam brojeve. Parne razine su slijeva nadesno, neparne s desna nalijevo, i razina svakog znaka kodira ugnježđivanje dvosmjernih nizova unutar kojih znak sjedi. Iz tih brojeva L2 izvodi permutaciju. Ono što algoritam namjerno ne čini jest odabir kojeg fonta koristiti, stvaranje ligatura ili preuređenje glifova unutar skupine; to su brige oblikovanja i pripadaju stupnju poslije ovoga

Cjevovod FPdfBidi za PDF tekst bez Uniscribea: PdfResolveBidiLevels dodjeljuje razinu ugnježđivanja UAX #9 po UTF-16 jedinici koda, a PdfBidiVisualOrder primjenjuje pravilo L2 da proizvede vizualni red
Razine kodiraju ugnježđivanje nizova, a pravilo L2 pretvara ih u permutaciju koja se čita slijeva nadesno
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto primjenjuje P2-P3: prvi snažni 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 se sada čita slijeva nadesno; Levels[] još govori koji
    // su nizovi RTL pa oblikovaču mogu se predati ispravni smjerovi
  end;
end;

Tablica znakovnih klasa generira se, ne piše

Svaka točka koda ima svojstvo Bidi_Class, i algoritam ga stalno pita, pa je tablica temelj na kojem sve ostalo stoji. Generira se iz baze Unicode Character Database umjesto da se održava ručno: peto polje UnicodeData.txt daje dodijeljene klase, a deklaracije @missing u DerivedBidiClass.txt daju zadane vrijednosti za točke koda koje baza ne dodjeljuje, pa tako nedodijeljeni blokovi ispravno padaju na R, AL, ET ili BN umjesto na L

Trik kompresije jest emitirati samo raspone čija klasa nije L. Sve što pada izvan svakog raspona jest L, što je i Unicode zadano i klasa ogromne većine točaka koda. To svodi tablicu koja bi inače išla do tisuća unosa na 745 raspona i oko 6,7 KB. Operativna posljedica vrijedi izreći: kada prijeđete na novu verziju Unicodea, ponovno pokrenite generator. Ručno uređivanje uključive datoteke radit će, i također će se tiho razilaziti s bazom pri sljedećoj nadogradnji

L2 mora preurediti točke koda, ne UTF-16 jedinice koda

To je greška koja proizvodi istinski oštećen izlaz, i prva implementacija ju je napravila. L2 kaže da se obrnu uzastopni nizovi na svakoj razini od najviše do najniže neparne. Napisano nad UTF-16 nizom, "obrni niz" prirodno znači obrnuti jedinice koda u njemu. Za znakove u Osnovnom višejezičnom prostoru to je u redu. Za RTL znak u astralnoj ravnini, poput onih u blokovima Ciparskog ili Starog južnoarapskog blizu U+10800, nije: znak je par zamjenskih jedinica, okretanje niza stavlja nisku zamjensku jedinicu ispred visoke, i niz sada sadrži dvije neuparene zamjenske jedinice umjesto jednog znaka. Ništa nizvodno to ne može oporaviti

Ispravak jest raditi L2 na jedinicama točaka koda. Implementacija spaja jedinice koda u jedinice točaka koda, izvodi okretanja nad tim jedinicama, i na kraju proširuje rezultat natrag u indekse jedinica koda. Zato PdfBidiVisualOrder uzima tekst, a ne samo polje razina: ne može reći gdje su granice zamjenskih jedinica iz samih razina. Ista disciplina parova zamjenskih jedinica prolazi kroz tekstualne API-je općenito, kao što je opisano u članku o emojijima, CJK-u i parovima zamjenskih jedinica

Oštećenje para zamjenskih jedinica u dvosmjernom preuređenju: okretanje UTF-16 jedinica koda rastavlja astralni znak blizu U+10800 u neuparene zamjenske jedinice, dok okretanje spojenih jedinica točaka koda čuva netaknutim
Pravilo L2 mora spajati jedinice koda u točke koda prije okretanja, a zatim ih proširiti natrag

Spust kroz razine mora uključiti razine koje se ne pojavljuju

Druga greška suptilnija je i ne proizvodi rušenje, samo tekst koji nije preuređen. L2 kaže da se počne od najviše prisutne razine i radi prema dolje do najniže neparne. Prirodna optimizacija jest skupiti skup razina koje stvarno postoje i iterirati nad tim skupom. Pogrešna je

Razmotrite red latiničkog teksta unutar ugnježđivanja s desna nalijevo. Razina odlomka je 0, ugnježđivanje gura latiničke znakove na razinu 2, i nijedan znak ne sjedi na razini 1. Iteriranje nad postojećim razinama nalazi samo 0 i 2, i nema neparne razine uopće, pa petlja ne izvodi nikakvo okretanje. Taj je odgovor ispravan, ali iz razloga koji optimizacija ne zna: okretanje na razini 2 pa okretanje na razini 1 potrošilo bi se točno, pa je izvođenje nijednog ispravan ishod. Promijenite unos malo, tako da postoje znakovi na razini 1 i razini 3 ali ne na razini 2, i petlja temeljena na skupu preskače okretanje razine 2 koje algoritam traži

// Ispravno: hodajte svakom razinom od najviše do najniže neparne
// razine, uključujući razine koje nijedan znak stvarno nema
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // ništa kada nijedan niz ne kvalificira
  Dec(Level);
end;

Napisano kao obična petlja s umanjivanjem ponašanje ispada besplatno, i iteracije bez učinka ne koštaju ništa mjerljivo. Ovo je slučaj u kojem očita optimizacija nije malo pogrešna, pogrešna je na način koji ovisi o unosu i koji mali testni korpus nikad neće otkriti

Zamka spusta dvosmjernih razina u UAX #9: iteriranje samo razina koje postoje preskače potrebno okretanje razine 2, dok obična petlja s umanjivanjem od MaxLevel do najniže neparne razine uvijek ispravno preuređuje
Hodanje svakom razinom do najniže neparne ne košta ništa i nikad ne preskače potrebno okretanje

Zagrade: BD16 s pragmatičnom tablicom

Pravilo N0 i algoritam parova zagrada BD16 postoje da bi se zagrada u tekstu miješanih smjerova razriješila u smjer onoga što obuhvaća umjesto u ono što slučajno priliježe. To treba tablicu parova zagrada. Implementacija nosi parove u općoj uporabi umjesto punog sadržaja Unicode datoteke zagrada: ASCII, CJK, pune širine, matematičke i ukrasne zagrade

Neizlistana zagrada nije pogreška. Razrješuje se kao običan neutralan znak kroz N1 i N2, što je točno ponašanje koje je svaka implementacija imala prije nego je Unicode 6.3 uveo N0. Dakle granica je "manje profinjeno za rijetke zagrade", a ne "neispravno". Jedna pojedinost treba izričito rukovanje: kanonska ekvivalencija između kutnih zagrada na U+2329 i U+232A i onih na U+3008 i U+3009 mora se savinuti pri podudaranju parova, ili će otvorena zagrada napisana jednim načinom ne uspjeti upariti se sa zatvarajućom napisanom drugim

Kako testirate trideset međudjelujućih pravila

Ne velikim korpusom, barem ne prvim. Plodni pristup bilo je šesnaest ručno provjerenih slučajeva, svaki odabran da vježba određeno pravilo i svaki provjeren u odnosu na razine koje UAX #9 kaže da treba proizvesti: otkrivanje smjera odlomka pod P2 i P3, pravila slabih tipova W2, W3 i W7, pravila implicitnih razina I1 i I2, izričito ugnježđivanje preko X2 i X7, izolate preko X5a i X6a, L1 ponovno postavljanje pratećeg praznog prostora i odjeljnika, slučaj zaglavlja N0, i jedan slučaj s astralnim znakom da se učvrsti rukovanje zamjenskim jedinicama

Šesnaest slučajeva s poznato ispravnim očekivanim razinama uhvati više od šesnaest stotina slučajeva s uvjerljivo izgledajućim izlazom, jer je način kvara dvosmjerne implementacije tekst koji se čita gotovo ispravno. Kad ti prođu, korpus je koristan za pronalaženje jazova tablice i problema performansi, što su različiti razredi defekata

Unutar PDFium komponente razine hrane dva potrošača. Na strani pisanja govore pozadini oblikovanja smjer svakog niza, što je unos koji HarfBuzz traži. Na strani čitanja informiraju geometriju odabira i redoslijed čitanja, jer se klik u RTL tekstu mora preslikati u logički položaj umjesto vizualnog; to se preslikavanje pokriva u članku o odabiru vizualnog retka, a model redoslijeda čitanja u strukturiranim blokovima teksta i redoslijedu čitanja. Pojedinosti podrške platformi za komponentu su na stranici proizvoda PDFium Delphi component