Műszaki cikk

BiDi beágyazási szintek PDF szöveghez Uniscribe nélkül

A Uniscribe több munkát végez, mint amennyit a legtöbb hívó észrevesz. A ScriptItemize egyetlen menetben végez kétirányú elemzést és szkriptszegmentálást, a ScriptLayout pedig előállítja a keletkező futamok vizuális sorrendjét. A HarfBuzz, az a hordozható helyettesítő, amelyhez az emberek nyúlnak, egyiket sem teszi: egyetlen futamot alakít, amelynek irányát és szkriptjét valaki más már eldöntötte. Tehát az a nehéz rész, hogy egy Windows PDF szövegvezetéket Linuxra vagy macOS-re vigyenek, nem az alakító motor bekötése. Hanem annak a kétirányú algoritmusnak a szolgáltatása, amelyet a Uniscribe csendben szolgáltatott, és a PDFium komponensben erre való a FPdfBidi

Az egység közvetlenül implementálja a UAX #9-et: P2 és P3 szabályok a bekezdésirányhoz, X1-től X10-ig explicit beágyazásokhoz és izolátumokhoz, W1-től W7-ig gyenge típusokhoz, N0-tól N2-ig semlegesekhez és zárójelekhez, I1 és I2 implicit szintekhez, valamint L1 és L2 a végső átrendezéshez. Két függvény hordozza: a PdfResolveBidiLevels egy beágyazási szintet ad vissza UTF-16 kódegységenként, a PdfBidiVisualOrder pedig azokat a szinteket arra a permutációvá alakítja, amely a kódegységeket balról jobbra helyezi

Mit ad Önnek az algoritmus, és mit nem

Számokat ad Önnek. A páros szintek balról jobbra, a páratlanok jobbról balra mennek, és minden karakter szintje kódolja azt a kétirányú futamok beágyazását, amelyben az a karakter ül. Azokból a számokból az L2 permutációt vezet le. Amit az algoritmus szándékosan nem tesz, az annak eldöntése, melyik betűt használja, ligatúrák képzése vagy glífák átrendezése egy klaszteren belül; ezek alakítási kérdések, és az ezt követő szakaszhoz tartoznak

FPdfBidi vezeték PDF szöveghez Uniscribe nélkül: a PdfResolveBidiLevels UAX #9 beágyazási szintet rendel UTF-16 kódegységenként, a PdfBidiVisualOrder pedig L2 szabályt alkalmaz a vizuális sorrend előállítására
A szintek a futamok beágyazását kódolják, és az L2 szabály azokból azt a permutációt formálja, amely balról jobbra olvas
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // A pbdAuto alkalmazza a P2-P3-t: az első erős karakter dönt
  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];
    // A Visual most balról jobbra olvas; a Levels[] továbbra is megmondja,
    // mely futamok RTL-ek, hogy egy alakító helyes irányokat kaphasson
  end;
end;

A karakterosztály-tábla generált, nem írt

Minden kódpontnak van Bidi_Class tulajdonsága, és az algoritmus folyamatosan felkeresi, tehát a tábla az az alap, amelyen minden más áll. Az a Unicode Character Database-ből generált, nem kézzel karbantartott: a UnicodeData.txt ötödik mezője adja a hozzárendelt osztályokat, és a DerivedBidiClass.txt @missing deklarációi adják az alapértelmezéseket azokhoz a kódpontokhoz, amelyeket az adatbázis nem rendel hozzá, ami az oka annak, hogy a kiosztatlan blokkok helyesen alapértelmeződnek R-re, AL-re, ET-re vagy BN-re, nem L-re

A tömörítési csíny az, hogy csak azokat a tartományokat bocsátja ki, amelyek osztálya nem L. Bármi, amely minden tartományon kívül esik, L, ami egyben a Unicode alapértelmezése és a kódpontok elsöprő többségének osztálya. Az egy táblát, amely egyébként több ezer bejegyzésre futna, 745 tartományra és nagyjából 6,7 KB-ra visz le. Az operatív következmény kimondásra érdemes: amikor új Unicode verzióra költözik, futtassa újra a generátort. Az include fájl kézi szerkesztése működni fog, és ugyanúgy csendben eltér majd az adatbázistól a következő frissítéskor

Az L2-nek kódpontokat kell átrendeznie, nem UTF-16 kódegységeket

Ez az a hiba, amely valóban rongált kimenetet produkál, és az első implementáció elkövette. Az L2 azt mondja, hogy fordítson meg összefüggő futamokat minden szinten a legmagasabbtól a legalacsonyabb páratlan szintig. UTF-16 karakterláncon megírva a „futam megfordítása” természetesen azt jelenti, hogy a benne lévő kódegységeket fordítja meg. A Többnyelvű Alap Sík karaktereire az rendben van. Egy asztrális síkbeli RTL karakterre, mint amilyenek a Ciprusi vagy Ódél-Arábiai blokkokban vannak az U+10800 közelében, az nem: a karakter szürrogátpár, a futam megfordítása az alacsony szürrogátot a magas elé teszi, és a karakterlánc most két párosítatlan szürrogátot tartalmaz egy karakter helyett. Semmi lefelé irányuló nem tudja helyreállítani

A javítás az, hogy az L2-t kódpontegységeken végezzék. Az implementáció a kódegységeket kódpontegységekké olvasztja össze, azokon az egységeken végzi a megfordításokat, és az eredményt a végén vissza bontja kódegység-indexekké. Ez az ok, amiért a PdfBidiVisualOrder a szöveget veszi át, nem csak a szinttömböt: a szürrogáthatárokat a szintekből egyedül nem tudja megmondani. Ugyanaz a szürrogátpár-megfontoltság áthatja a szöveg API-kat általában, ahogyan azt a emoji, CJK és szürrogátpár cikk írja

Szürrogátpár-rongálás a kétirányú átrendezésben: az UTF-16 kódegységek megfordítása egy U+10800 közeli asztrális karaktert párosítatlan szürrogátokra hasít, míg az összeolvasztott kódpontegységek megfordítása érintetlenül tartja
Az L2 szabálynak a megfordítás előtt kell a kódegységeket kódpontokká olvasztania, majd azután vissza bontania

A szinteken való leereszkedésnek azokat a szinteket is tartalmaznia kell, amelyek nem fordulnak elő

A második hiba finomabb, és nem produkál összeomlást, csak nem átrendezett szöveget. Az L2 azt mondja, hogy a jelen lévő legmagasabb szinttől induljon, és a legalacsonyabb páratlan szintig menjen le. Kézenfekvő optimalizáció annak a szintkészletnek a gyűjtése, amely ténylegesen előfordul, és azon a készleten iterálni. Az téves

Vegyünk egy latin szövegsort egy jobbról balra beágyazáson belül. A bekezdésszint 0, a beágyazás a latin karaktereket 2-es szintre tolja, és egyetlen karakter sem ül 1-es szinten. Az előforduló szinteken való iteráció csak 0-t és 2-t talál, és egyáltalán nincs páratlan szint, tehát a ciklus nem végez megfordítást. Az a válasz helyes, de olyan okból, amelyet az optimalizáció nem tud: egy 2-es szintű megfordítást egy 1-es szintű követne, és pontosan kioltanák egymást, tehát egyiket sem végezni a helyes kimenet. Változtassa meg kicsit a bemenetet úgy, hogy 1-es és 3-as szintű karakterek is léteznek, de 2-es nem, és a készletalapú ciklus kihagyja azt a 2-es szintű megfordítást, amelyet az algoritmus megkövetel

// Helyes: járjon minden szintet a maximumtól a legalacsonyabb páratlan
// szintig, azokat a szinteket is beleértve, amelyekkel egy karakter sem bír
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // nincs művelet, amikor egyetlen futam sem minősül
  Dec(Level);
end;

Egyszerű csökkentő ciklusként megírva a viselkedés magától adódik, és a nem működő iterációk mérhető költsége nulla. Ez az az eset, amikor a nyilvánvaló optimalizáció nem enyhén téves, hanem bemenettől függő módon téves, amelyet egy kis tesztkorpusz soha nem fedne fel

BiDi szintleereszkedési buktató a UAX #9-ben: csak az előforduló szintek iterálása kihagyja a szükséges 2-es szintű megfordítást, míg egy egyszerű csökkentő ciklus a MaxLevel-től a legalacsonyabb páratlan szintig mindig helyesen rendez át
Minden szint végigjárása a legalacsonyabb páratlanig semmibe sem kerül, és soha nem hagy ki szükséges megfordítást

Zárójelek: BD16 pragmatikus táblával

Az N0 szabály és a BD16 zárójelpár-algoritmus azért létezik, hogy egy vegyes irányú szövegben lévő zárójel arra az irányra oldódjon fel, amit bezár, nem pedig arra, ami éppen mellette van. Ehhez zárójelpárok táblája kell. Az implementáció az általánosan használt párokat hordozza, nem a Unicode zárójelfájl teljes tartalmát: ASCII, CJK, teljes szélességű, matematikai és díszítő zárójelek

Egy fel nem sorolt zárójel nem hiba. Az közönséges semlegesként oldódik fel az N1 és N2 útján, ami pontosan az a viselkedés, amelyet minden implementáció mutatott, mielőtt a Unicode 6.3 bevezette az N0-t. Tehát a határvonal az, hogy „kevésbé kifinomult ritka zárójelekre”, nem „téves”. Egy részletnek egyértelmű kezelés kell: a U+2329 és U+232A csúcsos zárójelek, valamint a U+3008 és U+3009 zárójelek közötti kanonikus egyenértékűséget össze kell hajtani párpárosításkor, vagy egy egyik írásmóddal írt nyitózárójel nem párosodik a másik írásmóddal írt zárózárójellel

Hogyan teszteljen harminc egymással kölcsönható szabályt

Nem nagy korpuszzal, legalábbis nem először. A termékeny megközelítés tizenhat kézzel ellenőrzött eset volt, mindegyik úgy kiválasztva, hogy egy konkrét szabályt gyakoroltasson, és mindegyik ellenőrizve a szintekkel, amelyeket a UAX #9 szerint produkálnia kell: bekezdésirány-észlelés P2 és P3 alatt, a gyenge típusú szabályok W2, W3 és W7, az implicit szintszabályok I1 és I2, explicit beágyazás X2-vel és X7-gyel, izolátumok X5a-val és X6a-val, a záró üres helyek és elválasztók L1 visszaállítása, egy N0 zárójel eset, és egy asztrális karakteres eset a szürrogátkezelés rögzítésére

Tizenhat ismert helyességű várt szintekkel bíró eset többet fog el, mint ezerhatszáz hihetőnek látszó kimenetű eset, mert egy kétirányú implementáció hibamódja az a szöveg, amely majdnem jól olvasható. Amint azok átmennek, a korpusz a táblalyukak és a teljesítményproblémák megtalálására hasznos, amelyek más hibaosztályok

A PDFium komponensen belül a szintek két fogyasztót táplálnak. Az írás oldalon megmondják az alakító backendnek minden futam irányát, ami az a bemenet, amelyre a HarfBuzz-nak szüksége van. Az olvasás oldalon a kijelölésgeometriát és az olvasási sorrendet tájékoztatják, mert egy RTL szövegben lévő kattintásnak logikai pozícióra kell képződnie, nem vizuálisra; az a leképezés a vizuális sor kijelölési cikkben van tárgyalva, és az olvasási sorrend modell a strukturált szövegblokkokban és olvasási sorrendben. A komponens platformtámogatási részletei a PDFium Delphi component terméklapon találhatók