Uniscribe gør mere arbejde, end de fleste kaldere er klar over. ScriptItemize udfører bidirektionel analyse og scriptsegmentering i ét gennemløb, og ScriptLayout producerer den visuelle orden af de resulterende runs. HarfBuzz, den portable erstatning, folk rækker efter, gør ingen af delene: den shaper et enkelt run, hvis retning og script allerede er besluttet af en anden. Så den svære del af at tage en Windows PDF-tekstpipeline til Linux eller macOS er ikke at binde en shaping-motor. Det er at levere den bidirektionelle algoritme, som Uniscribe stille leverede, og i PDFium-komponenten er det det, FPdfBidi er til for
Uniten implementerer UAX #9 direkte: reglerne P2 og P3 for afsnitsretning, X1 til X10 for eksplicitte embeddings og isolater, W1 til W7 for svage typer, N0 til N2 for neutrale og klammer, I1 og I2 for implicitte niveauer samt L1 og L2 til den afsluttende reordering. To funktioner bærer det: PdfResolveBidiLevels returnerer ét embedding-niveau pr. UTF-16 kodeenhed, og PdfBidiVisualOrder omdanner de niveauer til den permutation, der placerer kodeenheder fra venstre mod højre
Hvad algoritmen giver dig, og hvad den ikke gør
Den giver dig tal. Lige niveauer er venstre-mod-højre, ulige niveauer er højre-mod-venstre, og hvert tegns niveau koder nesting af de retningsbestemte runs, tegnet sidder inden i. Ud fra de tal afleder L2 en permutation. Hvad algoritmen bevidst ikke gør, er at afgøre, hvilken font der skal bruges, danne ligaturer eller omordne glyphs inden for et cluster; det er shaping-anliggender og hører til trinnet efter dette
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto anvender P2-P3: det første stærke tegn afgør
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 læses nu fra venstre mod højre; Levels[] siger stadig,
// hvilke runs der er RTL, så en shaper kan få korrekte retninger
end;
end;
Tegnklassetabellen er genereret, ikke skrevet
Hvert kodepunkt har en Bidi_Class-egenskab, og algoritmen slår den op konstant, så tabellen er fundamentet, alt andet står på. Den genereres fra Unicode Character Database frem for at vedligeholdes i hånden: felt fem i UnicodeData.txt giver de tildelte klasser, og @missing-erklæringerne i DerivedBidiClass.txt giver standarderne for de kodepunkter, databasen ikke tildeler, hvilket er sådan, ikke-allokerede blokke korrekt standardiseres til R, AL, ET eller BN frem for til L
Komprimeringstricket er kun at udsende de intervaller, hvis klasse ikke er L. Alt, der falder uden for ethvert interval, er L, hvilket både er Unicode-standarden og klassen af det overvældende flertal af kodepunkter. Det tager en tabel, der ellers ville løbe op i tusindvis af poster, ned til 745 intervaller og omkring 6,7 KB. Den operationelle konsekvens er værd at slå fast: når du flytter til en ny Unicode-version, kør generatoren igen. At redigere include-filen i hånden vil virke, og den vil også i stilhed afvige fra databasen ved næste opgradering
L2 skal omordne kodepunkter, ikke UTF-16 kodeenheder
Dette er den fejl, der producerer genuint korrupt output, og den første implementation lavede den. L2 siger at vende sammenhængende runs ved hvert niveau fra det højeste ned til det laveste ulige niveau. Skrevet mod en UTF-16-streng betyder "at vende et run" naturligt at vende kodeenhederne i det. For tegn i Basic Multilingual Plane er det fint. For et RTL-tegn i et astralt plan, som dem i de cypriotiske eller oldsydarabiske blokke nær U+10800, er det ikke: tegnet er et surrogate-par, og at vende run'et sætter den lave surrogate før den høje, og strengen indeholder nu to uparrede surrogater i stedet for ét tegn. Intet længere nede kan redde det
Rettelsen er at gøre L2 på kodepunkt-enheder. Implementationen fletter kodeenheder til kodepunkt-enheder, udfører vendingerne på de enheder og ekspanderer resultatet tilbage til kodeenhedsindekser til sidst. Det er grunden til, at PdfBidiVisualOrder tager teksten og ikke kun niveauerne-arrayet: den kan ikke se, hvor surrogate-grænserne er ud fra niveauer alene. Samme surrogate-par-disciplin løber gennem tekst-API'erne generelt, som beskrevet i artiklen om emoji, CJK og surrogate-par
Nedstigningen gennem niveauer skal inkludere niveauer, der ikke optræder
Den anden fejl er mere subtil og producerer intet crash, bare tekst, der ikke er omordnet. L2 siger at starte ved det højeste tilstedeværende niveau og arbejde ned til det laveste ulige niveau. En naturlig optimering er at indsamle mængden af niveauer, der reelt optræder, og iterere over den mængde. Det er forkert
Overvej en linje latinsk tekst inde i en højre-mod-venstre-embedding. Afsnitsniveauet er 0, embeddingen skubber de latinske tegn til niveau 2, og intet tegn sidder på niveau 1. At iterere over optrædende niveauer finder kun 0 og 2, og der er slet intet ulige niveau, så løkken udfører ingen vending. Det svar er korrekt, men af en grund, optimeringen ikke kender: en vending på niveau 2 efterfulgt af en vending på niveau 1 ville annullere eksakt, så at udføre ingen af dem er det rigtige udfald. Ændr inputtet en anelse, så både niveau 1- og niveau 3-tegn findes, men niveau 2 ikke gør, og den mængdebaserede løkke springer niveau 2-vendingen over, som algoritmen kræver
// Korrekt: gå hvert niveau fra maksimum ned til det laveste ulige
// niveau, inklusive niveauer, intet tegn reelt har
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op, når intet run kvalificerer
Dec(Level);
end;
Skrevet som en almindelig nedtællende løkke falder adfærden gratis ud af det, og no-op-iterationerne koster intet måleligt. Dette er et tilfælde, hvor den oplagte optimering ikke er en anelse forkert, den er forkert på en inputafhængig måde, som et lille testkorpus aldrig vil afsløre
Klammer: BD16 med en pragmatisk tabel
Regel N0 og BD16 klammerpar-algoritmen findes, så en parentes i blandet-retning tekst opløses til retningen af det, den omslutter, snarere end til hvad end der tilfældigvis er tilstødende. Det behøver en tabel over klammerpar. Implementationen bærer parrene i almindelig brug frem for hele indholdet af Unicode-klammefilen: ASCII, CJK, fullwidth, matematiske og ornamentale klammer
En ulistet klamme er ikke en fejl. Den opløses som en almindelig neutral gennem N1 og N2, hvilket præcis er den adfærd, enhver implementation havde, før Unicode 6.3 indførte N0. Så grænsen er "mindre raffineret for sjældne klammer", ikke "forkert". Én detalje behøver eksplicit håndtering: den kanoniske ækvivalens mellem vinkelklammerne ved U+2329 og U+232A og dem ved U+3008 og U+3009 skal foldes ved parringsmatch, ellers vil en åbningsklamme skrevet på den ene måde ikke parre med en lukkeklamme skrevet på den anden
Sådan tester du tredive interagerende regler
Ikke med et stort korpus, i det mindste ikke først. Den produktive tilgang var seksten håndverificerede tilfælde, hvert valgt til at afprøve en specifik regel og hvert tjekket mod de niveauer, UAX #9 siger, det skal give: afsnitsretningsdetektion under P2 og P3, de svage type-regler W2, W3 og W7, de implicitte niveauregler I1 og I2, eksplicit embedding via X2 og X7, isolater via X5a og X6a, L1-nulstillingen af afsluttende whitespace og separatorer, et N0-klammertilfælde og ét tilfælde med et astralt tegn for at låse surrogate-håndteringen
Seksten tilfælde med kendt-korrekte forventede niveauer fanger mere end seksten hundrede tilfælde med plausibelt udseende output, for fejltilstanden hos en bidirektionel implementation er tekst, der læses næsten rigtigt. Når først de består, er et korpus nyttigt til at finde tabelhuller og ydelsesproblemer, som er forskellige klasser af defekter
Inde i PDFium-komponenten føder niveauerne to forbrugere. På skrivesiden fortæller de shaping-backenden retningen af hvert run, hvilket er det input, HarfBuzz kræver. På læsesiden informerer de markeringsgeometri og læserækkefølge, da et klik i RTL-tekst skal mappes til en logisk position snarere end en visuel; den mapping er dækket i artiklen om visuel linjemarkering og læserækkefølge-modellen i strukturerede tekstblokke og læserækkefølge. Platformunderstøttelsesdetaljer for komponenten findes på produktsiden PDFium Delphi component