Uniscribe dělá víc práce, než si většina volajících uvědomuje. ScriptItemize provede obousměrnou analýzu a segmentaci psacích soustav v jednom průchodu a ScriptLayout produkuje vizuální pořadí vzniklých runs. HarfBuzz, přenositelná náhrada, po níž lidé sahají, nedělá ani jedno: tvaruje jediný run, jehož směr a psací soustavu už rozhodl někdo jiný. Takže tvrdou částí přenést Windows potrubí textu PDF na Linux nebo macOS není vázat tvarovací engine. Je to dodat obousměrný algoritmus, který Uniscribe potichu poskytoval, a v komponentě PDFium je k tomu FPdfBidi
Unita implementuje UAX #9 přímo: pravidla P2 a P3 pro směr odstavce, X1 až X10 pro explicitní zapouzdření a izoláty, W1 až W7 pro slabé typy, N0 až N2 pro neutrály a závorky, I1 a I2 pro implicitní úrovně a L1 a L2 pro finální přeřazení. Nesou je dvě funkce: PdfResolveBidiLevels vrací jednu úroveň zapouzdření na kódovou jednotku UTF-16 a PdfBidiVisualOrder mění ty úrovně v permutaci, která klade kódové jednotky zleva doprava
Co algoritmus dává a co nedává
Dává vám čísla. Sudé úrovně jsou zleva doprava, liché zprava doleva a úroveň každého znaku kóduje zanoření směrových runs, uvnitř nichž znak sedí. Z těch čísel L2 odvozuje permutaci. Co algoritmus záměrně nedělá, je rozhodnout, které písmo použít, tvořit ligatury nebo přeřazovat glyfy uvnitř clusteru; to jsou starosti tvarování a patří fázi po této
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto aplikuje P2-P3: rozhodne první silný znak
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 teď čte zleva doprava; Levels[] pořád říká, které
// runs jsou RTL, takže shaperovi lze podat správné směry
end;
end;
Tabulka znakových tříd se generuje, nepíše se
Každý kódový bod má vlastnost Bidi_Class a algoritmus se jí neustále dotazuje, takže tabulka je základem, na němž stojí všechno ostatní. Generuje se z Unicode Character Database, nikoli se neudržuje ručně: pole pět v UnicodeData.txt dává přiřazené třídy a deklarace @missing v DerivedBidiClass.txt dávají výchozí hodnoty pro kódové body, které databáze nepřiřazuje, čímž nealokované bloky správně defaultují na R, AL, ET nebo BN, nikoli na L
Kompresním trikem je vypustit jen rozsahy, jejichž třída není L. Cokoli padajícího mimo každý rozsah je L, což je jak výchozí hodnota Unicode, tak třída naprosté většiny kódových bodů. Tím se tabulka, která by jinak běžela do tisíců položek, dostane na 745 rozsahů a kolem 6,7 KB. Provozní důsledek stojí za vyslovení: přejdete-li na novou verzi Unicode, spusťte generátor znovu. Ruční úprava include souboru bude fungovat a zároveň se potichu rozchází s databází při příštím upgradu
L2 musí přeřazovat kódové body, nikoli kódové jednotky UTF-16
Toto je chyba, která produkuje skutečně poškozený výstup, a první implementace ji udělala. L2 říká obrátit souvislé runs na každé úrovni od nejvyšší dolů po nejnižší lichou úroveň. Zapsáno proti řetězci UTF-16 znamená „obrátit run“ přirozeně obrátit kódové jednotky v něm. U znaků v Basic Multilingual Plane je to v pořádku. U znaku RTL v astrální rovině, jako jsou ty v blocích kyperském nebo starojihožnoarabském poblíž U+10800, není: znak je surrogate pár, obrácení runu dá nízký surrogát před vysoký a řetězec teď obsahuje dva nespárované surrogáty namísto jednoho znaku. Nic downstream to nedokáže obnovit
Opravou je dělat L2 na jednotkách kódových bodů. Implementace sloučí kódové jednotky na jednotky kódových bodů, provede obracení na těch jednotkách a na konci rozvine výsledek zpět na indexy kódových jednotek. Proto PdfBidiVisualOrder bere text, nikoli jen pole úrovní: z úrovní samotných nedokáže říct, kde jsou hranice surrogátů. Tatáž disciplína surrogate párů protéká textovými API obecně, jak popisuje článek o emoji, CJK a surrogátových párech
Sestup úrovněmi musí zahrnovat úrovně, které se nevyskytují
Druhá chyba je jemnější a neprodukuje pád, jen text, který není přeřazen. L2 říká začít na nejvyšší přítomné úrovni a pracovat dolů po nejnižší lichou úroveň. Přirozenou optimalizací je sesbírat množinu úrovní, které skutečně nastávají, a iterovat po té množině. Je chybná
Uvažujte řádek latinského textu uvnitř zapouzdření zprava doleva. Úroveň odstavce je 0, zapouzdření zatlačí latinské znaky na úroveň 2 a žádný znak nesedí na úrovni 1. Iterace po nastávajících úrovních najde jen 0 a 2 a žádná lichá úroveň není, takže smyčka neprovede žádné obrácení. Ta odpověď je správná, ale z důvodu, který optimalizace nezná: obrácení na úrovni 2 následované obrácením na úrovni 1 by se přesně vyrušilo, takže neprovedení ani jednoho je správný výsledek. Změňte vstup nepatrně tak, aby existovaly znaky úrovně 1 i úrovně 3, ale úroveň 2 ne, a smyčka založená na množině přeskočí obrácení úrovně 2, které algoritmus vyžaduje
// Správně: procházejte každou úroveň od maxima dolů po nejnižší
// lichou úroveň, včetně úrovní, které žádný znak skutečně nemá
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op, když nevyhovuje žádný run
Dec(Level);
end;
Zapsáno jako prostá klesající smyčka to chování vypadne zdarma a no-op iterace nestojí nic měřitelného. To je případ, kdy zjevná optimalizace není mírně chybná, je chybná způsobem závislým na vstupu, který malý testovací korpus nikdy neodhalí
Závorky: BD16 s pragmatickou tabulkou
Pravidlo N0 a algoritmus párování závorek BD16 existují proto, aby závorka v textu se smíšenými směry se rozřešla na směr toho, co obklopuje, nikoli na cokoli, co se náhodou vyskytuje vedle. To potřebuje tabulku párů závorek. Implementace nese páry v obecném užívání namísto plného obsahu souboru závorek Unicode: ASCII, CJK, plné šířky, matematické a ozdobné závorky
Neuvedená závorka není chyba. Rozřeší se jako obyčejný neutrální skrze N1 a N2, což je přesně chování, které měly všechny implementace předtím, než Unicode 6.3 přineslo N0. Hranice tedy je „méně propracovaná pro vzácné závorky“, nikoli „nesprávná“. Jeden detail vyžaduje explicitní obsluhu: kanonická ekvivalence mezi úhlovými závorkami na U+2329 a U+232A a těmi na U+3008 a U+3009 musí být při párování srovnána, nebo otevírací závorka napsaná jedním způsobem nedokáže spárovat se zavírací napsanou druhým
Jak testujete třicet interagujících pravidel
Ne velkým korpusem, aspoň ne nejdřív. Produktivním přístupem bylo šestnáct ručně ověřených případů, každý zvolený k procvičení konkrétního pravidla a každý zkontrolovaný proti úrovním, které UAX #9 říká, že má produkovat: detekce směru odstavce pod P2 a P3, slabotypová pravidla W2, W3 a W7, pravidla implicitních úrovní I1 a I2, explicitní zapouzdření skrze X2 a X7, izoláty skrze X5a a X6a, reset L1 koncových bílých míst a oddělovačů, případ závorky N0 a jeden případ s astrálním znakem k uzamčení obsluhy surrogátů
Šestnáct případů se známě správnými očekávanými úrovněmi chytí víc než šestnáct set případů s výstupem vypadajícím věrohodně, protože režim selhání obousměrné implementace je text, který se čte téměř správně. Jakmile ty projdou, korpus je užitečný k hledání mezer v tabulce a problémů s výkonem, což jsou jiné třídy defektů
Uvnitř komponenty PDFium živí úrovně dva konzumenty. Na straně zápisu říkají tvarovacímu backendu směr každého runu, což je vstup, který HarfBuzz vyžaduje. Na straně čtení informují geometrii výběru a pořadí čtení, protože kliknutí v textu RTL se musí mapovat na logickou pozici, nikoli vizuální; to mapování pokrývá článek o výběru vizuálních řádků a model pořadí čtení strukturované textové bloky a pořadí čtení. Detaily podpory platforem komponenty jsou na stránce produktu PDFium Delphi component