يفعل Uniscribe عملا أكثر مما يدرك معظم المستدعين. ينفذ ScriptItemize التحليل ثنائي الاتجاه والتقسيم بحسب النظام الكتابي في مرور واحد، وينتج ScriptLayout الترتيب البصري للمقاطع الناتجة. أما HarfBuzz، البديل المحمول الذي يلجأ إليه الناس، فلا يفعل أيا منهما: يشكّل مقطعا واحدا قرر غيره اتجاهه ونظامه الكتابي سلفا. فالجزء الصعب من نقل مسار نص PDF في Windows إلى Linux أو macOS ليس ربط محرك تشكيل. إنه توفير خوارزمية الثنائية الاتجاه التي كان Uniscribe يوفرها بهدوء، وفي مكون PDFium لهذا وُجد FPdfBidi
تنفذ الوحدة UAX #9 مباشرة: قاعدتا P2 وP3 لاتجاه الفقرة، وX1 إلى X10 للتضمينات الصريحة والعوازل، وW1 إلى W7 للأنواع الضعيفة، وN0 إلى N2 للمحايدات والأقواس، وI1 وI2 للمستويات الضمنية، وL1 وL2 لإعادة الترتيب النهائية. ووظيفتان تحملان ذلك: تعيد PdfResolveBidiLevels مستوى تضمين واحدا لكل وحدة كود UTF-16، ويحول PdfBidiVisualOrder تلك المستويات إلى التقليب الذي يضع وحدات الكود من اليسار إلى اليمين
ما تعطيك إياه الخوارزمية، وما لا تعطيك إياه
تعطيك أرقاما. المستويات الزوجية من اليسار إلى اليمين، والفردية من اليمين إلى اليسار، ويرمز مستوى كل محرف إلى تعشيش المقاطع الاتجاهية التي يجلس داخلها ذلك المحرف. ومن تلك الأرقام يشتق L2 تقليبا. وما تمتنع الخوارزمية عمدا عن فعله هو اختيار أي خط يستخدم، أو تشكيل ترابطات، أو إعادة ترتيب المحارف الرسومية داخل مجموعة؛ تلك هي شؤون التشكيل وتنتمي إلى المرحلة التالية لهذه
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[] تقول أي
// المقاطع RTL كي يسلم إلى المكوِّن الاتجاهات الصحيحة
end;
end;
جدول أصناف المحارف مولد لا مكتوب
لكل نقطة كود خاصية Bidi_Class، وتفحصها الخوارزمية باستمرار، فالجدول هو الأساس الذي يقف عليه كل شيء آخر. وهو مولد من قاعدة بيانات محارف يونيكود لا مصان يدويا: يعطي الحقل الخامس من UnicodeData.txt الأصناف المخصصة، وتعطي تعريفات @missing في DerivedBidiClass.txt الافتراضات لنقاط الكود التي لا تخصصها القاعدة، وبذلك تصح افتراضات الكتل غير المخصصة إلى R أو AL أو ET أو BN بدلا من L
حيلة الضغط هي إصدار المدات التي صنفها ليس L فقط. وكل ما يقع خارج كل مدة هو L، وهو الافتراضي في يونيكود وصنف الأغلبية الساحقة من نقاط الكود. ذلك يأخذ جدولا كان سيصل لولا ذلك إلى آلاف المدخلات ويخفضه إلى 745 مدة ونحو 6.7 كيلوبايت. والأثر التشغيلي يستحق التصريح: عندما تنتقل إلى نسخة يونيكود جديدة، أعد تشغيل المولد. تحرير ملف التضمين يدويا سيعمل، وسيتباعد أيضا بصمت عن القاعدة عند الترقية التالية
يجب أن يعيد L2 ترتيب نقاط الكود لا وحدات كود UTF-16
هذا هو الخطأ الذي ينتج مخرجات فاسدة حقا، وقد ارتكبته أول معالجة. تقول L2 اعكس المقاطع المتجاورة عند كل مستوى من الأعلى نزولا إلى أدنى مستوى فردي. ومكتوبة مقابل سلسلة UTF-16 يعني «اعكس مقطعا» طبيعيا عكس وحدات الكود فيه. وللمحارف في المستوى متعدد اللغات الأساسي ذلك جيد. وللمحرف RTL في مستوى فلكي، كتلك في كتلتي القبرصية أو جنوب العربية القديمة قرب U+10800، ليس كذلك: فالمحرف زوج بديل، وعكس المقطع يضع البديل المنخفض قبل العالي، وتصبح السلسلة تحوي بديلين غير مقترنين بدل محرف واحد. ولا شيء في اتجاه المصب يستطيع استرداده
والإصلاح تنفيذ L2 على وحدات نقاط الكود. تدمج المعالجة وحدات الكود إلى وحدات نقاط كود، وتنفذ الانعكاسات على تلك الوحدات، وتوسع النتيجة عائدة إلى فهارس وحدات الكود في النهاية. ولهذا يأخذ PdfBidiVisualOrder النص لا مصفوفة المستويات فقط: فهو لا يستطيع أن يعرف أين حدود البدائل من المستويات وحدها. ويمر انضباط الزوج البديل نفسه عبر واجهات النص عموما، كما هو موصوف في مقال الرموز التعبيرية وCJK وأزواج البدائل
يجب أن يشمل النزول عبر المستويات مستويات لا تحدث
الخطأ الثاني أرق وأنتجه لا انهيار، بل نص لم يعاد ترتيبه فقط. تقول L2 ابدأ عند أعلى مستوى حاضر وانزل إلى أدنى مستوى فردي. والتحسين الطبيعي جمع مجموعة المستويات التي تحدث فعلا والتكرار على تلك المجموعة. وهو خاطئ
خذ سطر نص لاتيني داخل تضمين من اليمين إلى اليسار. مستوى الفقرة 0، والتضمين يدفع المحارف اللاتينية إلى المستوى 2، ولا محرف يجلس عند المستوى 1. والتكرار على المستويات الحادثة يجد 0 و2 فقط، ولا مستوى فردي إطلاقا، فلا تنفذ الحلقة أي انعكاس. ذلك الجواب صحيح، لكن لسبب لا يعرفه التحسين: انعكاس عند المستوى 2 يليه انعكاس عند المستوى 1 سيلغيان بعضهما تماما، فعدم تنفيذ أي منهما هو النتيجة الصائبة. وغيّر المدخل قليلا، بحيث توجد محارف المستوى 1 والمستوى 3 معا ولا يوجد مستوى 2، فيتخطى الحلقة المبنية على المجموعة انعكاس المستوى 2 الذي تشترطه الخوارزمية
// صحيح: تجول على كل مستوى من الأعظم نزولا إلى أدنى مستوى فردي،
// بما في ذلك مستويات لا يملكها أي محرف فعلا
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // بلا عمل عندما لا يؤهل أي مقطع
Dec(Level);
end;
مكتوبة كحلقة تنقيص عادية يسقط السلوك مجانا، وتكرارات لا العمل لا تكلف شيئا قابلا للقياس. هذه حالة يكون فيها التحسين البديهي ليس خاطئا قليلا، بل خاطئا بطريقة معتمدة على المدخل لن تكشفها أبدا مدونة اختبار صغيرة
الأقواس: BD16 بجدول عملي
توجد القاعدة N0 وخوارزمية أزواج الأقواس BD16 كي يُحل قوس في نص مختلط الاتجاه إلى اتجاه ما يحيط به لا إلى ما يصادف أن يكون مجاورا. وذلك يحتاج جدول أزواج أقواس. تحمل المعالجة الأزواج الشائعة الاستخدام لا المحتويات الكاملة لملف أقواس يونيكود: أقواس ASCII وCJK وعريضة ورياضية وزخرفية
القوس غير المدرج ليس خطأ. يُحل كمحايد عادي عبر N1 وN2، وهو بالضبط سلوك كل معالجة قبل أن تقدم يونيكود 6.3 القاعدة N0. فالحد هو «أقل دقة للأقواس النادرة» لا «غير صحيح». وتفصيل واحد يحتاج معالجة صريحة: التكافؤ القانوني بين قوسَي الزاوية عند U+2329 وU+232A ونظيريهما عند U+3008 وU+3009 يجب أن يطوى عند مطابقة الأزواج، وإلا فشل قوس فتح كتب بطريقة في الاقتران بقوس إغلاق كتب بالأخرى
كيف تختبر ثلاثين قاعدة متفاعلة
لا بمدونة كبيرة، على الأقل ليس أولا. والمقاربة المثمرة كانت ست عشرة حالة دققها اليد، اختير كل منها لتجريب قاعدة بعينها وجرى فحص كل منها مقابل المستويات التي تقول UAX #9 إنه ينبغي أن ينتجها: كشف اتجاه الفقرة بموجب P2 وP3، وقواعد الأنواع الضعيفة W2 وW3 وW7، وقاعدتا المستوى الضمني I1 وI2، والتضمين الصريح عبر X2 وX7، والعوازل عبر X5a وX6a، وإعادة الضبط L1 للمسافة البيضاء اللاحقة والفواصل، وحالة قوس N0، وحالة بمحرف فلكي لتثبيت معالجة البدائل
ست عشرة حالة بمستويات متوقعة معروف صحتها تلتقط أكثر من ست عشرة مئة حالة بمخرج معقول المظهر، لأن نمط إخفاق معالجة ثنائية الاتجاه نص يقرأ صحيحا تقريبا. وبمجرد اجتياز تلك، تفيد المدونة في إيجاد فجوات الجدول ومشكلات الأداء، وهما صنفا عيب مختلفان
داخل مكون PDFium تغذي المستويات مستهلكين اثنين. ومن جهة الكتابة تخبران المسار الخلفي للتشكيل باتجاه كل مقطع، وهو المدخل الذي يشترطه HarfBuzz. ومن جهة القراءة تفيدان هندسة التحديد وترتيب القراءة، لأن نقرة في نص RTL يجب أن تقابل إلى موضع منطقي لا بصري؛ وذلك التقابل مشمول في مقال تحديد السطر البصري ونموذج ترتيب القراءة في كتل النص المهيكلة وترتيب القراءة. وتفاصيل دعم المنصات للمكون في صفحة منتج PDFium Delphi component