Tehnični članak

Ravni vgradnje BiDi za besedilo PDF brez Uniscribe

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

Cevovod FPdfBidi za besedilo PDF brez Uniscribe: PdfResolveBidiLevels dodeli raven vgradnje UAX #9 na kodno enoto UTF-16 in PdfBidiVisualOrder aplicira pravilo L2, da proizvede vizualni vrstni red
Ravni kodirajo gnezdenje tekov in pravilo L2 jih spremeni v permutacijo, ki bere od leve proti desni
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

Pokvarjenost surrogatnega para v dvosmernem preurejanju: obračanje kodnih enot UTF-16 razcepi astralni znak blizu U+10800 v neparna surrogata, medtem ko obračanje združenih enot kodnih točk ohranja njega nedotaknjenega
Pravilo L2 mora združiti kodne enote v kodne točke pred obračanjem in jih nato razširiti nazaj

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

Past spusta dvosmernih ravni v UAX #9: iteriranje samo ravni, ki nastopijo, preskoči zahtevani obrat ravni 2, medtem ko premo odštevajoča zanka od MaxLevel do najnižje lihe ravni vedno pravilno preuredi
Hoja vsake ravni navzdol do najnižje lihe ne stale ničesar in nikoli ne preskoči zahtevanega obrata

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