Uniscribe počne več dela, kot se večini klicateljev zdi. ScriptItemize izvede dvosmjerno analizo in razdelitev po skripti v enem prehodu, ScriptLayout pa proizvede vizualni vrstni red nastalih tekov. HarfBuzz, prenosljiva zamenjava, po kateri ljudje posežejo, ne počne niti enega: oblikuje en sam tek, katerega smer in skripto je nekdo drug že odločil. Torej težki del prenosa besedilnega cevovoda PDF Windows na Linux ali macOS ni vezanje oblikovalnega motorja. Je preskrba dvosmjernega algoritma, ki ga je Uniscribe tiho preskrbel, in v komponenti PDFium je FPdfBidi zanj
Enota implementira UAX #9 neposredno: pravila P2 in P3 za smer odstavka, X1 do X10 za izrecne vgradnje in izolate, W1 do W7 za šibke tipe, N0 do N2 za nevtralne in oklepaje, I1 in I2 za implicirane ravni ter L1 in L2 za končno preurejanje. Dve funkciji jo nosita: PdfResolveBidiLevels vrne eno raven vgradnje na kodno enoto UTF-16 in PdfBidiVisualOrder spremeni te ravni v permutacijo, ki umešča kodne enote od leve proti desni
Kaj vam algoritem da in kaj ne
Da vam števila. Soda ravni so od leve proti desni, lihe so od desne proti levi, raven vsakega znaka pa kodira gnezdenje dvosmernih tekov, znotraj katerih znak sedi. Iz teh števil L2 izpelje permutacijo. Kar algoritem namensko ne počne, je odločiti, katero pisavo uporabiti, tvoriti ligature ali preurediti glife znotraj klasterja; to so skrbi oblikovanja in pripadajo stopnji za to
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto aplicira P2-P3: prvi močni znak odloči
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 zdaj bere od leve proti desni; Levels[] še vedno pove,
// kateri teki so RTL, tako da oblikovalcu lahko izročite pravilne smeri
end;
end;
Tabela razredov znakov je generirana, ne zapisana
Vsaka kodna točka ima lastnost Bidi_Class in algoritem se nanjo nenehno sklicuje, zato je tabela temelj, na katerem stoji vse ostalo. Generirana je iz podatkovne baze znakov Unicode namesto ročno vzdrževana: peto polje UnicodeData.txt da dodeljene razrede in deklaracije @missing v DerivedBidiClass.txt dajo privzete vrednosti za kodne točke, ki jih podatkovna baza ne dodeli, kar je način, kako nedodeljeni bloki pravilno privzamejo R, AL, ET ali BN namesto L
Trik stiskanja je izdati samo razpone, katerih razred ni L. Karkoli pade zunaj vsakega razpona je L, kar je tako privzeto Unicode kot razred izjemne večine kodnih točk. To vzame tabelo, ki bi sicer teko do tisočih vnosov, navzdol na 745 razponov in približno 6,7 KB. Operativna posledica je vredna izjave: ko se premaknete na novo različico Unicode, ponovno poženite generator. Ročno urejanje datoteke include bo delovalo in bo prav tako tiho divergiralo od podatkovne baze ob naslednji nadgradnji
L2 mora preurediti kodne točke in ne kodne enote UTF-16
To je napaka, ki proizvede resnično pokvarjen izhod, in prva implementacija jo je naredila. L2 pravi, da obrnete sosednje teke na vsaki ravni od najvišje navzdol do najnižje lihe ravni. Zapisano proti nizu UTF-16, »obrni tek« naravno pomeni obračanje kodnih enot v njem. Za znake v Osnovni večjezični ravnini je to v redu. Za znak RTL v astralni ravnini, kot so tisti v blokih Ciprskega ali Starega južnoarabskega blizu U+10800, ni: znak je surrogatni par, obračanje teka da nizki surrogat pred visokega in niz zdaj vsebuje dva neparna surrogata namesto enega znaka. Nič nizvodno ga ne more obnoviti
Popravek je početi L2 na enotah kodnih točk. Implementacija združi kodne enote v enote kodnih točk, izvede obrate na teh enotah in razširi rezultat nazaj v indekse kodnih enot na koncu. Zato PdfBidiVisualOrder vzame besedilo in ne samo matriko ravni: ne more povedati, kje so meje surrogatov iz samih ravni. Isti nadzor surrogatnih parov teče skozi API-je besedila na splošno, kot je opisano v članku o emodžijih, CJK in surrogatnih parih
Spust skozi ravni mora vključevati ravni, ki ne nastopijo
Druga napaka je subtilnejša in ne proizvede sesutja, samo besedilo, ki ni preurejeno. L2 pravi, da začnete pri najvišji prisotni ravni in delate navzdol do najnižje lihe ravni. Naravna optimizacija je zbrati množico ravni, ki dejansko nastopijo, in iterirati čez to množico. Narobe je
Razmislite o vrstici latinskega besedila znotraj vgradnje od desne proti levi. Raven odstavka je 0, vgradnja potisne latinske znake na raven 2 in noben znak ne sedi na ravni 1. Iteriranje čez nastopajoče ravni najde samo 0 in 2 in ni lihe ravni sploh, tako da zanka ne izvede nobenega obrata. Ta odgovor je pravilen, ampak iz razloga, ki ga optimizacija ne ve: obrat na ravni 2, ki mu sledi obrat na ravni 1, bi se točno izničil, tako da je izvedba nobenega pravi izid. Spremenite vhod malenkostno, tako da obstajata tako znaka ravni 1 kot ravni 3, raven 2 pa ne, in zanka na osnovi množice preskoči obrat ravni 2, ki ga algoritem zahteva
// Pravilno: prehodite vsako raven od maksimuma navzdol do najnižje
// lihe ravni, vključno z ravnmi, ki jih noben znak dejansko nima
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // brez delovanja, ko noben tek ne kvalificira
Dec(Level);
end;
Zapisan kot premo odštevajoča zanka se vedenje izliže zastonj in iteracije brez delovanja ne stale ničesar merljivega. To je primer, kjer očitna optimizacija ni malenkostno napačna, je napačna na način, odvisen od vhoda, ki ga majhen testni korpus nikoli ne bo razkril
Oklepaji: BD16 s pragmatično tabelo
Pravilo N0 in algoritem parov oklepajev BD16 obstajata, tako da se oklepaj v besedilu mešanih smeri razreši v smer tistega, kar zapira, in ne v karkoli se slučajno nahaja sosednje. To potrebuje tabelo parov oklepajev. Implementacija nosi pare v splošni rabi namesto polne vsebine datoteke oklepajev Unicode: ASCII, CJK, polne širine, matematični in okrasni oklepaji
Neuvrščen oklepaj ni napaka. Razreši se kot običajen nevtralen skozi N1 in N2, kar je natanko vedenje, ki ga je imela vsaka implementacija, preden je Unicode 6.3 uvedel N0. Torej meja je »manj rafinirano za redke oklepaje« in ne »nepravilno«. Ena podrobnost potrebuje izrecno ravnanje: kanonična ekvivalenca med oglatimi oklepaji pri U+2329 in U+232A in tistimi pri U+3008 in U+3009 mora biti zložena pri ujemanju parov, ali odpirajoči oklepaj, zapisan na en način, bo spodletel pariti z zapirajočim oklepajem, zapisanim drugače
Kako testirate trideset interagirajočih pravil
Ne z velikim korpusom, vsaj ne najprej. Produktiven pristop je bil šestnajst ročno preverjenih primerov, vsak izbran, da vadi specifično pravilo, in vsak preverjen proti ravnem, ki pravi UAX #9, naj jih proizvede: zaznavanje smeri odstavka pod P2 in P3, pravila šibkih tipov W2, W3 in W7, pravila impliciranih ravni I1 in I2, izrecna vgradnja skozi X2 in X7, izolati skozi X5a in X6a, ponastavitev L1 sledečih presledkov in ločevalnikov, primer oklepaja N0 in en primer z astralnim znakom, da se zaklene ravnanje s surrogati
Šestnajst primerov z znano-pravilnimi pričakovanimi ravnmi ujame več kot tisoč šeststo primerov z izhodom, ki izgleda verodostojno, ker je način odpovedi dvosmerne implementacije besedilo, ki se bere skoraj prav. Ko ti gredo, je korpus koristen za iskanje vrzel tabel in težav zmogljivosti, ki so različni razredi napak
Znotraj komponente PDFium ravni hranita dva potrošnika. Na strani pisanja povesta zaledju oblikovanja smer vsakega teka, kar je vhod, ki ga zahteva HarfBuzz. Na strani branja obvestita geometrijo izbire in bralni vrstni red, saj se klik v besedilu RTL mora preslikati v logični položaj in ne vizualnega; ta preslikava je pokrita v članku o izbiri vizualne vrstice in model bralnega vrstnega reda v blokih strukturiranega besedila in bralnem vrstnem redu. Podrobnosti podpore platformam za komponento so na strani produkta PDFium Delphi component