Teknisk artikkel

BiDi-innbyggingsnivåer for PDF-tekst uten Uniscribe

Uniscribe gjør mer arbeid enn de fleste kallere innser. ScriptItemize utfører bidireksjonell analyse og skriptsegmentering i én gjennomkjøring, og ScriptLayout produserer den visuelle rekkefølgen av de resulterende runene. HarfBuzz, den portable erstatningen folk strekker seg etter, gjør ingen av delene: den shaper en enkelt run hvis retning og skript allerede er avgjort av noen andre. Så den vanskelige delen av å ta en Windows PDF-tekstpipeline til Linux eller macOS, er ikke å binde en shaping-motor. Det er å levere den bidireksjonelle algoritmen Uniscribe stille leverte, og i PDFium-komponenten er det det FPdfBidi er til for

Enheten implementerer UAX #9 direkte: regler P2 og P3 for avsnittsretning, X1 til X10 for eksplisitte innbygginger og isolater, W1 til W7 for svake typer, N0 til N2 for nøytrale og klammer, I1 og I2 for implisitte nivåer, og L1 og L2 for den endelige reordregeringen. To funksjoner bærer den: PdfResolveBidiLevels returnerer ett innbyggingsnivå per UTF-16 kodeenhet, og PdfBidiVisualOrder gjør de nivåene til permutasjonen som plasserer kodeenheter venstre til høyre

Hva algoritmen gir deg, og hva den ikke gjør

Den gir deg tall. Like nivåer er venstre-til-høyre, odde nivåer er høyre-til-venstre, og nivået til hvert tegn koder nestingen av de retningsmessige runene det tegnet sitter inne i. Fra de tallene utleder L2 en permutasjon. Det algoritmen bevisst ikke gjør, er å avgjøre hvilken font som skal brukes, danne ligaturer, eller reordrener glyfer innenfor en klynge; det er shapingbekymringer og hører til stadiet etter denne

FPdfBidi-pipeline for PDF-tekst uten Uniscribe: PdfResolveBidiLevels tildeler ett UAX #9-innbyggingsnivå per UTF-16 kodeenhet, og PdfBidiVisualOrder anvender regel L2 for å produsere visuell rekkefølge
Nivåer koder runnestinging, og regel L2 gjør dem til permutasjonen som leses venstre til høyre
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto anvender P2-P3: det første sterke tegnet avgjø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 leser nå venstre til høyre; Levels[] sier fortsatt hvilke
    // runer som er RTL slik at en shaper kan få korrekte retninger
  end;
end;

Tegnklassetabellen er generert, ikke skrevet

Hvert kodepunkt har en Bidi_Class-egenskap, og algoritmen konsulterer den stadig, så tabellen er fundamentet alt annet står på. Den genereres fra Unicode Character Database snarere enn vedlikeholdt for hånd: felt fem av UnicodeData.txt gir de tildelte klassene, og @missing-deklarasjonene i DerivedBidiClass.txt gir standardene for kodepunktene databasen ikke tildeler, noe som er hvordan utildelte blokker korrekt standarder til R, AL, ET eller BN snarere enn til L

Komprimeringstrikset er å sende ut bare områdene hvis klasse ikke er L. Alt som faller utenfor hvert område, er L, som både er Unicode-standarden og klassen til det overveldende flertallet av kodepunkter. Det tar en tabell som ellers ville løpe til tusenvis av oppføringer ned til 745 områder og omtrent 6,7 KB. Den operasjonelle konsekvensen er verdt å slå fast: når du flytter til en ny Unicode-versjon, kjør generatoren på nytt. Å håndredigere include-filen vil fungere, og den vil også lydløst drive fra databasen ved neste oppgradering

L2 må reordrener kodepunkter, ikke UTF-16 kodeenheter

Dette er feilen som produserer genuint korruptert output, og den første implementeringen gjorde den. L2 sier å reversere sammenhengende runer på hvert nivå fra det høyeste ned til det laveste odde nivået. Skrevet mot en UTF-16-streng betyr «reverser en run» naturlig å reversere kodeenhetene i den. For tegn i Basic Multilingual Plane er det greit. For et RTL-tegn i et astralt plan, slik som de i de kypriotiske eller gammel-sør-arabiske blokkene nær U+10800, er det ikke det: tegnet er et surrogatpar, å reversere runen legger det lave surrogatet før det høye, og strengen inneholder nå to uparede surrogater i stedet for ett tegn. Ingenting nedstrøms kan gjenopprette det

Fiksen er å gjøre L2 på kodepunktenheter. Implementeringen fletter kodeenheter inn i kodepunktenheter, utfører reverseringene på de enhetene, og ekspanderer resultatet tilbake til kodeenhetsindekser på slutten. Det er hvorfor PdfBidiVisualOrder tar teksten og ikke bare nivåarrayen: den kan ikke si hvor surrogatgrensene er fra nivåer alene. Den samme surrogatpardisiplinen går gjennom tekst-API-ene generelt, som beskrevet i artikkelen om emoji, CJK og surrogatpar

Surrogatparkorruptering i bidi-reordregering: å reversere UTF-16 kodeenheter deler et astralt tegn nær U+10800 opp i uparede surrogater, mens å reversere flettede kodepunktenheter holder det intakt
Regel L2 må flette kodeenheter inn i kodepunkter før reversering, og deretter ekspandere dem tilbake

Nedstigningen gjennom nivåer må inkludere nivåer som ikke opptrer

Den andre feilen er subtilere og produserer intet krasj, bare tekst som ikke er reordnet. L2 sier å starte på det høyeste til stede værende nivået og arbeide ned til det laveste odde nivået. En naturlig optimalisering er å samle mengden av nivåer som faktisk opptrer og iterere over den mengden. Det er feil

Betragt en linje med latinsk tekst inne i en høyre-til-venstre-innbygging. Avsnittsnivået er 0, innbyggingen skyver de latinske tegnene til nivå 2, og intet tegn sitter på nivå 1. Å iterere over opptrødende nivåer finner bare 0 og 2, og det finnes ikke noe odde nivå i det hele tatt, så løkken utfører ingen reversering. Det svaret er korrekt, men av en grunn optimaliseringen ikke kjenner: en reversering på nivå 2 etterfulgt av en reversering på nivå 1 ville kansellere nøyaktig, så å utføre ingen av dem er det riktige utfallet. Endre inputen litt, slik at både nivå 1- og nivå 3-tegn finnes men nivå 2 ikke gjør det, og den mengdebaserte løkken hopper over nivå 2-reverseringen algoritmen krever

// Korrekt: gå hvert nivå fra maksimum ned til det laveste odde
// nivået, inkludert nivåer intet tegn faktisk har
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // ingen virkning når ingen run kvalifiserer
  Dec(Level);
end;

Skrevet som en ren dekrementerende løkke faller atferden ut gratis, og ingenvirkning-iterasjonene koster ingenting målbart. Dette er et tilfelle der den opplagte optimaliseringen ikke er litt feil, den er feil på en inputavhengig måte et lite testkorpus aldri vil avdekke

Bidi-nivånedstigningsfelle i UAX #9: å iterere bare nivåene som opptrer hopper over den påkrevde nivå 2-reverseringen, mens en ren dekrementerende løkke fra MaxLevel til det laveste odde nivået alltid reordrener korrekt
Å gå hvert nivå ned til det laveste odde koster ingenting og hopper aldri over en påkrevd reversering

Klammer: BD16 med en pragmatisk tabell

Regel N0 og BD16 klammeparalgoritmen finnes slik at en parentes i blandetretningstekst løses til retningen av det den omslutter, snarere enn til hva som tilfeldigvis er tilstøtende. Det trenger en tabell av klammepar. Implementeringen bærer parene i vanlig bruk snarere enn det fulle innholdet av Unicode-klammefilen: ASCII, CJK, fullbredde, matematiske og dekorative klammer

En uoppført klamme er ikke en feil. Den løses som en ordinær nøytral gjennom N1 og N2, noe som er nøyaktig atferden enhver implementering hadde før Unicode 6.3 innførte N0. Så grensen er «mindre finjustert for sjeldne klammer», ikke «inkorrekt». Én detalj trenger eksplisitt håndtering: den kanoniske ekvivalensen mellom vinkelklammene ved U+2329 og U+232A og de ved U+3008 og U+3009 må foldes ved matching av par, ellers vil en åpneklamme skrevet på én måte feile å pare med en lukkeklamme skrevet på den andre

Hvordan du tester tretti samvirkende regler

Ikke med et stort korpus, i det minste ikke først. Den produktive tilnærmingen var seksten håndverifiserte tilfeller, hvert valgt for å trene en spesifikk regel og hvert sjekket mot nivåene UAX #9 sier det skal produsere: avsnittsretningdeteksjon under P2 og P3, de svaketyperreglene W2, W3 og W7, de implisitte nivåreglene I1 og I2, eksplisitt innbygging via X2 og X7, isolater via X5a og X6a, L1-nullstillingen av etterfølgende whitespace og skilletegn, et N0-klammertilfelle, og ett tilfelle med et astralt tegn for å låse surrogathåndteringen

Seksten tilfeller med kjentriktige forventede nivåer fanger mer enn seksten hundre tilfeller med plausibel seende output, fordi feilmodusen til en bidireksjonell implementering er tekst som leses nesten riktig. Når de passerer, er et korpus nyttig for å finne tabellhull og ytelsesproblemer, noe som er forskjellige klasser av defekter

Inne i PDFium-komponenten mater nivåene to konsumenter. På skrivesiden forteller de shaping-backenden retningen til hver run, noe som er input HarfBuzz krever. På lesesiden informerer de utvalgsgeometri og leserrekkefølge, siden et klikk i RTL-tekst må mappe til en logisk posisjon snarere enn en visuell; den mappingen er dekket i artikkelen om visuell linjeutvalg og leserrekkefølgemodellen i strukturerte tekstblokker og leserrekkefølge. Plattformstøttedetaljer for komponenten står på produktsiden for PDFium Delphi component