Uniscribe gör mer arbete än de flesta anropare inser. ScriptItemize utför tvåriktad analys och skriptsegmentering i ett pass, och ScriptLayout producerar den visuella ordningen av de resulterande körningarna. HarfBuzz, den portabla ersättare folk sträcker sig efter, gör inget av dem: den formar en enda körning vars riktning och skript redan har beslutats av någon annan. Så den svåra delen av att ta en Windows-PDF-textpipeline till Linux eller macOS är inte att binda en formningsmotor. Det är att tillhandahålla den tvåriktade algoritm som Uniscribe tyst tillhandahöll, och i PDFium-komponenten är det vad FPdfBidi är till för
Enheten implementerar UAX #9 direkt: reglerna P2 och P3 för styckriktning, X1 till X10 för uttryckliga inbäddningar och isolat, W1 till W7 för svaga typer, N0 till N2 för neutraler och parenteser, I1 och I2 för implicita nivåer, och L1 och L2 för den slutliga omordningen. Två funktioner bär den: PdfResolveBidiLevels returnerar en inbäddningsnivå per UTF-16-kodenhet och PdfBidiVisualOrder gör de nivåerna till permutationen som placerar kodenheter vänster till höger
Vad algoritmen ger dig, och vad den inte gör
Den ger dig tal. Jämna nivåer är vänster-till-höger, udda nivåer är höger-till-vänster, och nivån för vart och ett tecken kodar nästlingen av de riktade körningar tecknet sitter inuti. Ur de talen härleder L2 en permutation. Vad algoritmen med flit inte gör är att avgöra vilket teckensnitt som ska användas, bilda ligaturer eller omordna glyfer inuti ett kluster; det är formningsangelägenheter och hör till steget efter denna
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto tillämpar P2-P3: första starka tecknet avgö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 vänster till höger; Levels[] säger fortfarande
// vilka körningar som är RTL så att en formare kan få korrekta
// riktningar
end;
end;
Teckenklasstabellen genereras, skrivs inte
Varenda kodpunkt har en egenskap Bidi_Class, och algoritmen rådfrågar den ständigt, så tabellen är grunden allt annat står på. Den genereras ur Unicode Character Database i stället för att underhållas för hand: fält fem i UnicodeData.txt ger de tilldelade klasserna, och @missing-deklarationerna i DerivedBidiClass.txt ger standardvärdena för kodpunkter databasen inte tilldelar, vilket är hur oallokerade block korrekt blir standard R, AL, ET eller BN i stället för L
Komprimeringsknepet är att avge bara de intervall vars klass inte är L. Allt som faller utanför varenda intervall är L, vilket är både Unicode-standarden och klassen för den överväldigande majoriteten av kodpunkter. Det tar en tabell som annars skulle löpa till tusentals poster ner till 745 intervall och cirka 6,7 KB. Den operativa konsekvensen är värd att ange: när du flyttar till en ny Unicode-version, kör generatorn igen. Att handredigera includefilen fungerar, och den kommer också tyst divergera från databasen vid nästa uppgradering
L2 måste omordna kodpunkter, inte UTF-16-kodenheter
Detta är misstaget som producerar genuint korrumperad utmatning, och den första implementeringen gjorde det. L2 säger att vända sammanhängande körningar på varje nivå från den högsta ner till den lägsta udda nivån. Skriven mot en UTF-16-sträng betyder ”vänd en körning” naturligt att vända kodenheterna i den. För tecken i Basic Multilingual Plane är det fint. För ett RTL-tecken i ett astralplan, som dem i de kypriotiska eller gammalsydarabiska blocken nära U+10800, är det inte det: tecknet är ett surrogatpar, att vända körningen lägger det låga surrogatet före det höga, och strängen innehåller nu två oparade surrogat i stället för ett tecken. Ingenting nedströms kan återvinna den
Fixen är att göra L2 på kodpunktsenheter. Implementeringen slår ihop kodenheter till kodpunktsenheter, utför vändningarna på de enheterna och expanderar resultatet tillbaka till kodenhetsindex i slutet. Det är därför PdfBidiVisualOrder tar texten och inte bara nivåarrayen: den kan inte säga var surrogatgränserna är ur nivåerna ensamma. Samma surrogatpar-disciplin löper genom text-API:erna i allmänhet, som beskrivs i artikeln om emoji, CJK och surrogatpar
Nedstigningen genom nivåer måste omfatta nivåer som inte förekommer
Det andra misstaget är subtilare och ger ingen krasch, bara text som inte omordnas. L2 säger att börja på den högsta nivån som finns och arbeta ner till den lägsta udda nivån. En naturlig optimering är att samla mängden nivåer som faktiskt förekommer och iterera över den mängden. Det är fel
Betrakta en rad latinsk text inuti en höger-till-vänster-inbäddning. Stycknivån är 0, inbäddningen knuffar de latinska tecknen till nivå 2, och inget tecken sitter på nivå 1. Att iterera över förekommande nivåer hittar bara 0 och 2, och det finns ingen udda nivå alls, så loopen utför ingen vändning. Det svaret är korrekt, men av en orsak optimeringen inte känner till: en vändning på nivå 2 följt av en vändning på nivå 1 skulle annuleras exakt, så att utföra ingendera är det rätta utfallet. Ändra indatan lätt, så att både nivå 1- och nivå 3-tecken finns men nivå 2 inte gör det, och den mängdsbaserade loopen hoppar över vändningen på nivå 2 som algoritmen kräver
// Rätt: gå igenom varenda nivå från maximum ner till den lägsta
// udda nivån, inklusive nivåer inget tecken faktiskt har
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op när ingen körning kvalificerar
Dec(Level);
end;
Skriven som en enkel dekrementerande loop faller beteendet ut gratis, och no-op-iterationerna kostar inget mätbart. Detta är ett fall där den uppenbara optimeringen inte är lätt fel, den är fel på ett indataberoende sätt som en liten testkorpus aldrig kommer avslöja
Parenteser: BD16 med en pragmatisk tabell
Regel N0 och BD16-parentesparalgoritmen finns så att en parentes i text med blandad riktning löser upp sig till riktningen av det den omsluter i stället för till vadhelst som råkar ligga bredvid. Det behövs en tabell över parentespar. Implementeringen bär paren i allmänt bruk i stället för hela innehållet i Unicode-parentesfilen: ASCII, CJK, fullbredd, matematiska och ornamentala parenteser
En olistad parentes är inte ett fel. Den löser upp sig som en vanlig neutral genom N1 och N2, vilket är exakt beteendet varje implementering hade innan Unicode 6.3 introducerade N0. Så gränsen är ”mindre förfinad för sällsynta parenteser”, inte ”inkorrekt”. En detalj behöver uttrycklig hantering: den kanoniska ekvivalensen mellan vinkelparenteserna på U+2329 och U+232A och dem på U+3008 och U+3009 måste vikas ihop vid parmatchning, eller kommer en öppnande parentes skriven på ett sätt inte att para sig med en stängande parentes skriven på det andra
Hur du testar trettio samverkande regler
Inte med en stor korpus, åtminstone inte först. Den produktiva metoden var sexton handverifierade fall, vart och ett valt att trära en specifik regel och vart och ett kontrollerat mot de nivåer UAX #9 säger att det ska producera: styckriktningsdetektering under P2 och P3, reglerna för svaga typer W2, W3 och W7, reglerna för implicita nivåer I1 och I2, uttrycklig inbäddning via X2 och X7, isolat via X5a och X6a, L1-återställningen av avslutande blanksteg och separatorer, ett N0-parentesfall och ett fall med ett astraltecken för att låsa surrogathanteringen
Sexton fall med kändkorrekta förväntade nivåer fångar mer än sextonhundra fall med plausibelt seende utmatning, eftersom felläget hos en tvåriktad implementering är text som läses nästan rätt. När de väl passerar är en korpus användbar för att hitta tabellgap och prestandaproblem, vilket är olika klasser av defekt
Inuti PDFium-komponenten matar nivåerna två konsumenter. På skrivarsidan talar de för formningsbackenden riktningen för varje körning, vilket är indatan HarfBuzz kräver. På läsarsidan informerar de urvalsgeometri och läsordning, eftersom ett klick i RTL-text måste mappas till en logisk position snarare än en visuell; den mappningen tas upp i artikeln om urval av visuella rader och läsordningsmodellen i strukturerade textblock och läsordning. Detaljer om plattformsstöd för komponenten finns på produktsidan för PDFium Delphi component