Uniscribe ทำงานมากกว่าที่ผู้เรียกส่วนใหญ่เข้าใจ ScriptItemize ทำการวิเคราะห์ bidirectional กับการแบ่งช่วง script จบในรอบเดียว และ ScriptLayout ผลิตลำดับภาพของ run ที่ได้ HarfBuzz ซึ่งเป็นสิ่งทดแทนแบบพกพาที่คนมักหยิบมาใช้ ทำทั้งสองอย่างนั้นไม่ได้: มัน shape run เดียวที่ทิศทางและ script ถูกคนอื่นตัดสินไปแล้ว ส่วนที่ยากของการพา pipeline ข้อความ PDF ของ Windows ไป Linux หรือ macOS จึงไม่ใช่การผูกเอนจิน shaping แต่คือการจัดเตรียมอัลกอริทึม bidirectional ที่ Uniscribe จัดให้อย่างเงียบ ๆ และใน component PDFium นั่นคือหน้าที่ของ FPdfBidi
ยูนิตนี้ implement UAX #9 โดยตรง: กฎ P2 และ P3 สำหรับทิศทางย่อหน้า, X1 ถึง X10 สำหรับ embedding และ isolate แบบชัดเจน, W1 ถึง W7 สำหรับชนิดอ่อน, N0 ถึง N2 สำหรับตัวกลางและวงเล็บ, I1 และ I2 สำหรับระดับโดยปริยาย และ L1 กับ L2 สำหรับการจัดลำดับใหม่ขั้นสุดท้าย มีสองฟังก์ชันแบกมัน: PdfResolveBidiLevels คืนหนึ่งระดับ embedding ต่อหนึ่ง code unit ของ UTF-16 และ PdfBidiVisualOrder เปลี่ยนระดับเหล่านั้นเป็น permutation ที่วาง code unit จากซ้ายไปขวา
สิ่งที่อัลกอริทึมให้ และสิ่งที่มันไม่ให้
มันให้ตัวเลขกับคุณ ระดับคู่คือซ้ายไปขวา ระดับคี่คือขวาไปซ้าย และระดับของตัวอักษรแต่ละตัวเข้ารหัสการซ้อนของ run เชิงทิศทางที่ตัวอักษรนั้นอยู่ข้างใน จากตัวเลขเหล่านั้น L2 derive เป็น permutation สิ่งที่อัลกอริทึมตั้งใจไม่ทำคือการเลือก font, การหลอม ligature หรือการจัดลำดับ glyph ข้างใน cluster ใหม่ พวกนั้นเป็นเรื่องของ shaping และเป็นของขั้นถัดไป
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto ใช้ P2-P3: ตัวอักษรแข็งแรงตัวแรกเป็นผู้ตัดสิน
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 อ่านจากซ้ายไปขวา Levels[] ยังบอกว่า run ใด
// เป็น RTL จึงส่งทิศทางที่ถูกต้องให้ shaper ได้
end;
end;
ตารางชนิดตัวอักษรถูก generate ไม่ใช่ถูกเขียนด้วยมือ
code point ทุกตัวมี property Bidi_Class และอัลกอริทึมปรึกษามันตลอดเวลา ตารางจึงเป็นฐานรากที่ทุกอย่างยืนอยู่ มันถูก generate จาก Unicode Character Database แทนการดูแลด้วยมือ: field ที่ห้าของ UnicodeData.txt ให้ชั้นที่ถูกจัดสรร และการประกาศ @missing ใน DerivedBidiClass.txt ให้ค่า default ของ code point ที่ฐานข้อมูลไม่จัดสรร ซึ่งเป็นวิธีที่บล็อกที่ยังไม่ถูกจัดสรรมีค่า default ที่ถูกต้องเป็น R, AL, ET หรือ BN แทน L
เทคนิคบีบอัดคือปล่อยเฉพาะช่วงที่ชั้นไม่ใช่ L อะไรก็ตามที่หลุดอยู่นอกทุกช่วงคือ L ซึ่งทั้งเป็นค่า default ของ Unicode และเป็นชั้นของ code point ส่วนใหญ่เหลือเฟือ สิ่งนี้หิ้วตารางที่จะวิ่งยาวถึงหลายพัน entry ให้เหลือ 745 ช่วงประมาณ 6.7 KB ผลเชิงปฏิบัติที่สมควรพูดถึง: เมื่อคุณขยับไปเวอร์ชัน Unicode ใหม่ ให้รัน generator ซ้ำ การแก้ไฟล์ include ด้วยมือใช้ได้ แต่มันจะหลงออกจากฐานข้อมูลอย่างเงียบ ๆ ในการอัปเกรดครั้งถัดไป
L2 ต้องจัดลำดับ code point ใหม่ ไม่ใช่ code unit ของ UTF-16
นี่คือความผิดพลาดที่ผลิตผลลัพธ์เสียหายอย่างแท้จริง และ implementation แรกก็ทำมัน L2 สั่งให้กลับด้าน run ต่อเนื่องที่แต่ละระดับ จากระดับสูงสุดลงไปถึงระดับคี่ต่ำสุด เขียนกับสตริง UTF-16 "กลับด้าน run" จึงหมายถึงการกลับด้าน code unit ข้างในมันตามธรรมชาติ กับตัวอักษรใน Basic Multilingual Plane ก็เรียบร้อยดี แต่กับตัวอักษร RTL ใน astral plane เช่นพวกในบล็อก Cypriot หรือ Old South Arabian แถว ๆ U+10800 ไม่ใช่: ตัวอักษรคือ surrogate pair คู่หนึ่ง การกลับด้าน run จะวาง low surrogate ไว้หน้า high surrogate และสตริงกลายเป็น surrogate ไร้คู่สองตัวแทนตัวอักษรหนึ่งตัว ปลายน้ำไม่มีอะไรกู้มันคืนได้
ทางแก้คือทำ L2 บนหน่วยระดับ code point implementation หลอม code unit เข้าเป็นหน่วยระดับ code point ทำการกลับด้านบนหน่วยเหล่านั้น แล้วขยายผลลัพธ์กลับเป็นดัชนีระดับ code unit ตอนท้าย นั่นคือเหตุผลที่ PdfBidiVisualOrder รับข้อความไปด้วย ไม่ใช่แค่อาร์เรย์ระดับ: มันบอกขอบเขต surrogate ได้จากระดับอย่างเดียวไม่ได้ วินัยเรื่อง surrogate pair แบบเดียวกันวิ่งทั่ว text API โดยรวม ตามที่อธิบายไว้ในบทความ emoji, CJK และ surrogate pair
การไล่ระดับลงต้องครอบคลุมระดับที่ไม่มีตัวอักษรใดใช้ด้วย
ความผิดพลาดที่สองละเอียดกว่าและไม่ให้ crash เลย เหลือเพียงข้อความที่ไม่ถูกจัดลำดับใหม่ L2 สั่งให้เริ่มที่ระดับสูงสุดที่มีอยู่แล้วไล่ลงไปถึงระดับคี่ต่ำสุด การปรับแต่งตามธรรมชาติคือเก็บชุดระดับที่เกิดขึ้นจริงแล้ววนซ้ำเฉพาะชุดนั้น มันผิด
พิจารณาบรรทัดข้อความละตินข้างใน embedding แบบขวาไปซ้าย ระดับย่อหน้าเป็น 0 การ embed ดันตัวอักษรละตินไปอยู่ระดับ 2 และไม่มีตัวอักษรใดนั่งที่ระดับ 1 การวนซ้ำเหนือระดับที่เกิดขึ้นจะเจอแค่ 0 กับ 2 และไม่มีระดับคี่เลย ลูปจึงกลับด้านอะไรเลย คำตอบนั้นถูก แต่ถูกด้วยเหตุผลที่การปรับแต่งไม่รู้: การกลับด้านที่ระดับ 2 ตามด้วยการกลับด้านที่ระดับ 1 จะหักล้างกันพอดี การไม่ทำทั้งคู่จึงเป็นผลลัพธ์ที่ถูก เปลี่ยนอินพุตเล็กน้อยให้มีตัวอักษรทั้งระดับ 1 และระดับ 3 แต่ไม่มีระดับ 2 แล้วลูปที่อิงชุดจะข้ามการกลับด้านระดับ 2 ที่อัลกอริทึมต้องการ
// ถูกต้อง: เดินทุกระดับจากค่าสูงสุดลงไปถึงระดับคี่ต่ำสุด
// รวมระดับที่ไม่มีตัวอักษรใดถือจริงด้วย
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // เป็น no-op เมื่อไม่มี run ผ่านเกณฑ์
Dec(Level);
end;
เขียนเป็นลูปนับถอยธรรมดา พฤติกรรมก็ได้มาฟรี และรอบ no-op แทบไม่มีต้นทุนที่วัดได้ นี่คือกรณีที่การปรับแต่งที่ชัดที่สุดไม่ได้ผิดเพียงเล็กน้อย แต่ผิดแบบขึ้นกับอินพุต ซึ่ง corpus ทดสอบชุดเล็กไม่มีวันเผยให้เห็น
วงเล็บ: BD16 กับตารางแบบใช้งานจริง
กฎ N0 และอัลกอริทึมจับคู่วงเล็บ BD16 มีอยู่เพื่อให้วงเล็บในข้อความผสมทิศทง resolve ไปทิศทางของสิ่งที่มันล้อม ไม่ใช่ของสิ่งที่บังเอิญนั่งอยู่ข้าง ๆ เรื่องนี้ต้องใช้ตารางคู่วงเล็บ implementation แบกคู่ที่ใช้กันทั่วไป แทนเนื้อหาเต็มของไฟล์วงเล็บของ Unicode: ASCII, CJK, fullwidth, วงเล็บคณิตศาสตร์และวงเล็บตกแต่ง
วงเล็บที่ไม่อยู่ในรายการไม่ใช่ error มัน resolve เป็นตัวกลางธรรมดาผ่าน N1 กับ N2 ซึ่งเป็นพฤติกรรมพอดีที่ทุก implementation เคยมีก่อน Unicode 6.3 จะเพิ่ม N0 ขอบเขตจึงเป็น "แม่นยำน้อยกว่ากับวงเล็บหายาก" ไม่ใช่ "ไม่ถูกต้อง" มีรายละเอียดหนึ่งที่ต้องจัดการอย่างชัดเจน: ความเทียบเท่าเชิง canonical ระหว่างวงเล็บเหลี่ยมมุมที่ U+2329 กับ U+232A และที่ U+3008 กับ U+3009 ต้องถูกหลอมรวมตอนจับคู่ ไม่อย่างนั้นวงเล็บเปิดที่เขียนแบบหนึ่งจะจับคู่กับวงเล็บปิดที่เขียนอีกแบบไม่ได้
ทดสอบสามสิบกฎที่ปฏิสัมพันธ์กันอย่างไร
ไม่ใช่ด้วย corpus ขนาดใหญ่ อย่างน้อยก็ไม่ใช่อย่างแรก แนวทางที่ให้ผลคือสิบหกกรณีที่ตรวจยืนยันด้วยมือ แต่ละกรณีถูกเลือกให้ออกกำลังกฎเจาะจงหนึ่งกฎ และแต่ละกรณีถูกเทียบกับระดับที่ UAX #9 ระบุว่าควรให้: การตรวจจับทิศทางย่อหน้าภายใต้ P2 และ P3, กฎชนิดอ่อน W2, W3 และ W7, กฎระดับโดยปริยาย I1 และ I2, embedding แบบชัดเจนผ่าน X2 กับ X7, isolate ผ่าน X5a กับ X6a, การรีเซ็ตช่องว่างท้ายบรรทัดและตัวคั่นของ L1, กรณีวงเล็บแบบ N0 หนึ่งกรณี และหนึ่งกรณีที่มีตัวอักษร astral เพื่อตรึงการจัดการ surrogate ไว้
สิบหกกรณีที่ระดับที่คาดหวังถูกต้องเป็นที่รู้จักจับข้อบกพร่องได้มากกว่าสิบหกร้อยกรณีที่ผลลัพธ์ดูน่าเชื่อถือ เพราะ failure mode ของ implementation bidirectional คือข้อความที่อ่านได้เกือบถูก พอกรณีเหล่านี้ผ่านแล้ว corpus จึงมีประโยชน์กับการหาช่องว่างของตารางและปัญหาประสิทธิภาพ ซึ่งเป็นข้อบกพร่องคนละชั้น
ข้างใน component PDFium ระดับเหล่านี้ป้อนผู้บริโภคสองราย ฝั่งการเขียน พวกมันบอก backend การ shape ว่าทิศทางของ run แต่ละตัว ซึ่งเป็นอินพุตที่ HarfBuzz ต้องการ ฝั่งการอ่าน พวกมันหล่อเลี้ยงเรขาคณิตการเลือกและลำดับการอ่าน เพราะการคลิกในข้อความ RTL ต้องแมปไปตำแหน่งเชิงตรรกะ ไม่ใช่เชิงภาพ การแมปนั้นอยู่ในบทความการเลือกบรรทัดเชิงภาพ และโมเดลลำดับการอ่านอยู่ในบล็อก structured text และลำดับการอ่าน รายละเอียดการรองรับ platform ของ component อยู่บนหน้าผลิตภัณฑ์PDFium Delphi component