مقال تقني

تشكيل النص العربي ومن اليمين إلى اليسار (RTL) في ملفات PDF لـ Delphi باستخدام HotPDF

مرر العبارة العربية يوضح ملف PDF إلى TextOut وافتح النتيجة. تعمل الحروف في الاتجاه الخاطئ، وكل حرف يجلس في شكله المعزول مع وجود فجوة مرئية قبل الحرف التالي، كما لو أن شخصًا ما كتب الإنجليزية بالمقلوب وضغط على مسافة بين كل حرف. لم يظهر أي استثناء. لم يُطبع أي تحذير. المخرجات خاطئة ببساطة، وهي خاطئة لأن عمليتين منفصلتين تعتمد عليهما اللغة العربية لم تحدثا أبدًا. معرفة ماهية هاتين العمليتين، وأي استدعاء ينفذهما، هو معظم ما يعنيه إخراج PDF للنصوص المعقدة

HotPDF هو مكون PDF أصلي لـ VCL لـ Delphi و C++Builder، ويقوم بالعمل من اليمين إلى اليسار نيابة عنك من خلال استدعاء متميز. يتوقف أيضًا بشكل مفاجئ في أماكن محددة قليلة تريد التعرف عليها قبل الالتزام بلغة (locale)، لذلك تقوم هذه القطعة بتعيين المفاهيم والحدود الصادقة؛ إعداد التدريب العملي للاستدعاء نفسه يعيش في مقال مرجعي RtLTextOut

لماذا لا تزال السلسلة الصحيحة تُطبع بشكل خاطئ

يحتفظ Unicode بالنص بترتيب منطقي، الترتيب الذي تكتبه به وتقرأه بصوت عالٍ. يتعين على العارض (renderer) وضع الصور الرمزية بترتيب مرئي. بالنسبة للنصوص التي تُكتب من اليسار إلى اليمين، تتطابق هذه الترتيبات ولا يفكر أحد في الأمر. بالنسبة للعربية والعبرية، لا تتطابق، وعندما يمزج سطر واحد بين الاتجاهات، على سبيل المثال جملة عربية تحمل الرمز اللاتيني "PDF" أو سعر مكتوب بالأرقام، تقرر خوارزمية Unicode ثنائية الاتجاه (UAX #9) بالضبط كيف تتداخل الأجزاء التي تُقرأ من اليسار إلى اليمين داخل السطر الذي يُقرأ من اليمين إلى اليسار. هذا هو التحول الأول، إعادة الترتيب، وتخطيه هو ما يقلب السطر

التحول الثاني هو التشكيل السياقي (contextual shaping). يُرسم الحرف العربي بشكل مختلف بناءً على مكان وقوعه في الكلمة: في البداية، أو في المنتصف، أو في النهاية، أو منفردًا. تظل نقطة الكود (codepoint) كما هي طوال الوقت؛ يتغير فقط الشكل (glyph). خط الأنابيب الذي يسلم كل نقطة كود مباشرة إلى شكلها الافتراضي ينتج بالضبط المخرجات المنفصلة المعزولة من الفقرة الافتتاحية. تتخطى العبرية هذه الخطوة، لأن حروفها لا تتصل، لكنها لا تزال بحاجة إلى إعادة الترتيب. تحتاج العربية إلى كليهما، ولهذا السبب فإن العربية، وليس العبرية، هي السلسلة التي تختبر بها

على سطح المكتب، لا شيء من هذا يمثل مشكلتك. عندما يقوم نموذج VCL برسم اللغة العربية في TEdit، يقوم مكدس النصوص الخاص بنظام التشغيل بهدوء بإعادة الترتيب والتشكيل، وهذا هو بالضبط السبب في أن السلسلة التي تبدو مثالية على الشاشة تظهر مكسورة في ملف PDF ساذج. دفق المحتوى لا يخزن نصًا قابلاً للتحرير. بل يخزن صورًا رمزية متمركزة، لذا فإن من يصدر الدفق يرث مهمة التشكيل التي كان يتعامل معها نظام التشغيل. RtLTextOut هو الاستدعاء الذي يستعيد هذه المهمة

ما يشكله RtLTextOut لك

يحتفظ HotPDF بالمسار اللاتيني ومسار النص المعقد كطريقتين مختلفتين. يطبع TextOut ما تعطيه إياه بالترتيب الذي تعطيه إياه. يقوم RtLTextOut بكلا التحولين أولاً - إعادة الترتيب ثنائي الاتجاه عبر السطر بأكمله، والتحليل السياقي للنصوص المتصلة - ثم يطبع. قواعد أي نص تُطبق تنتقل عبر مجموعة أحرف الخط (charset) بدلاً من الاستدعاء نفسه، لذا فإن الاتجاه هو خيار صريح في كل موقع استدعاء بدلاً من تخمين يُبنى من الحروف. الإعداد معاملًا تلو الآخر، وقيم مجموعة الأحرف، وخطوات تسجيل الخط، ومثال كامل قابل للترجمة موجودة في مقال مرجع RtLTextOut؛ ستبقى هذه القطعة مع ما تعنيه التحولات، وأين تتوقف، وكيف تثبت أنها نجحت

قاعدة استخدام واحدة مهمة حتى في هذا المستوى: يجب أن يكون الإدخال بترتيب منطقي، لأن RtLTextOut يقوم بالانعكاس بنفسه، والسلسلة التي قلبتها بالفعل يدويًا تخرج معكوسة بشكل مزدوج - يمر المقال المرجعي عبر هذا الفخ وتنظيفه. ما يجعل الفخ يستحق الذكر هنا هو سبب نجاته من الاختبار. يمكن أن تبدو السلسلة العربية النقية المعكوسة بشكل مزدوج صحيحة تمامًا، ولا تنهار إلا عندما يحمل السطر كلمة لاتينية أو رقمًا، لأن هذه التشغيلات المضمنة لم تعد تتداخل بالطريقة التي يمليها UAX #9. الخلل ليس في العرض؛ بل في إطعام الخوارزمية بنص تمت معالجته نصفًا بالفعل

سلوك الاتجاه المختلط نفسه يعرقل المراجعين أكثر مما يعرقل الكود. داخل سطر يُقرأ من اليمين إلى اليسار، لا تزال الأرقام والكلمات اللاتينية المضمنة تُقرأ من اليسار إلى اليمين. قد ينظر شخص لم يعمل مع التخطيط ثنائي الاتجاه إلى فاتورة معروضة، ويرى رقم الحساب يُقرأ بالطريقة "الخاطئة" بالنسبة للعربية المحيطة به، ويسجله كخطأ. إنها النتيجة الصحيحة وفقًا للمواصفات. ملاحظة قصيرة في معايير القبول الخاصة بك، مكتوبة قبل تمرير المتحدث الأصلي الأول، توفر رحلة الذهاب والإياب هذه

متى تكون إعادة الترتيب والتوصيل كافية، ومتى لا تكون كذلك

بالنسبة للنصوص العربية والعبرية الجارية - التقارير والفواتير والعقود والرسائل - فإن إعادة الترتيب بالإضافة إلى التوصيل السياقي هما المهمة بأكملها، وينفذها RtLTextOut بمفرده. تظهر الحدود عندما تطلب الطباعة أكثر من التوصيل. إجابة HotPDF على الجانب العربي عبارة عن مُشكّل من جانب المُنتِج (producer-side shaper) يُطلب اختياريًا: قم بتعيين AutoShapeArabic := True وسيقوم المكون بإعادة كتابة تشغيل الترتيب المنطقي إلى أشكال عرض Unicode قبل التمرير ثنائي الاتجاه، بحيث يتم حساب أشكال الاتصال مقابل الجيران المنطقيين ويتم دمج طيات الربط (ligature folds) في نقاط الكود التي يحملها ملف PDF فعليًا، بدلاً من تركها ليحلها العارض. المفتاح يكون معطلاً افتراضيًا والمخرجات مستقرة بايتيًا عند بقائه معطلاً، لذا فإن تشغيله هو قرار متعمد لكل مسار عمل مستند، وليس ترقية عالمية. يمتد نفس نموذج الاشتراك (opt-in) إلى النصوص الأخرى المتصلة من اليمين إلى اليسار التي يشكلها HotPDF: كل من السريانية (Syriac)، و N'Ko، و Adlam، و Hanifi Rohingya لديها علامة شكل تلقائي (auto-shape) خاصة بها تعكس العلامة العربية

ميزات OpenType الاختيارية هي آلية مختلفة مرة أخرى. تمر أربطة الحروف الاختيارية (Discretionary ligatures) وميزات الاستبدال الفردي المماثلة عبر GetSingleSubstituteGlyph(GID, 'liga')، والذي يحل استبدالًا واحدًا في كل مرة - معرّف شكل الإدخال (input glyph ID) أولاً، وعلامة الميزة ثانيًا - ويعيد شكل الإدخال دون تغيير عندما لا تنطبق الميزة. هذا يكفي لتشغيل قائمة أربطة معروفة ومحدودة تحتفظ بها بنفسك. إنه ليس محرك GSUB كاملاً، والفرق هو بالضبط المكان الذي تسوء فيه خطط اللغة (locale) الطموحة: خط أنابيب التشكيل الذي يتعامل مع اللغة العربية بشكل لا تشوبه شائبة أثبت إعادة الترتيب والتوصيل، ولا شيء أكثر

التغطية عبر النصوص

تطبق اللغة العربية كلا التحولين، وهذا هو السبب في أنها السلسلة التي يتم الاختبار بها، ولماذا يعد المرور العربي أقوى دليل منفرد على أن مسار العمل يعمل. تحتاج العبرية إلى إعادة الترتيب ولكن ليس التوصيل، لأن أحرفها تقف بمفردها؛ إذا تم عرض العبرية بشكل صحيح ولكن العربية خرجت منفصلة، فإن النصف ثنائي الاتجاه بخير ولم يتم تشغيل النصف السياقي أبدًا. الفارسية والأردية تركبان النص العربي وترثان سلوكه، على الرغم من أن تفضيل الأردية لأسلوب النستعليق هو قرار خطي له عواقب على وضوح القراءة يجب أن يحكم عليها القارئ الأصلي

تقع التايلاندية على الجانب الآخر من الخط تمامًا. تُكتب من اليسار إلى اليمين، لذا فهي لا تحتاج إلى عمل ثنائي الاتجاه، ولا تتصل أحرفها، لذا فهي لا تحتاج إلى تحليل سياقي؛ تمر السلاسل التايلاندية عبر مسار TextOut العادي مثل اللاتينية. ما تمتلكه التايلاندية هو العلامات المتراكمة - حروف العلة وعلامات النغمة أعلى وأسفل الساكن الأساسي - وما إذا كانت تجلس بشكل صحيح يعتمد على الخط الذي يبني علامات الجمع الخاصة به لتتراكم دون مساعدة محرك التشكيل. معظم الخطوط التايلاندية المخصصة تفعل ذلك. اختبر باستخدام الخط الدقيق الذي ستقوم بتضمينه، وليس خطًا مشابهًا

ديفاناغاري (Devanagari) وبقية العائلة الهندية هي نقطة التوقف الصعبة بصدق. يُعاد ترتيب علامات حروف العلة الخاصة بها حول مجموعات الحروف الساكنة وتتشكل مركباتها من خلال سلاسل من الاستبدالات المعتمدة على السياق، وهي منطقة GSUB كاملة، تتجاوز إعادة الترتيب والتوصيل. إذا كانت اللغة الهندية على خريطة الطريق، فقم بإجراء مشروع تجريبي حقيقي على سلاسل عملاء أصلية قبل أن تعد به - نجاح العربية ليس دليلاً على نجاح الديفاناغاري. السلاسل الصينية واليابانية والكورية (CJK)، والفيتنامية مع تشكيلاتها المتراكمة، والنصوص الأوروبية المختلطة كلها تسلك المسار العادي بدون تحليل ثنائي الاتجاه، ويفضل إبقاء المسارين منفصلين ماديًا في كود التقرير، روتين واحد لتشغيلات RTL وآخر لكل شيء آخر، بحيث يكون منطق اللغة مرئيًا في موقع الاستدعاء بدلاً من إخفائه خلف علامة ينسى أحدهم تعيينها

يتم تحديد تغطية الصور الرمزية (Glyph coverage) حتى قبل تشغيل التشكيل

يلتقط التشكيل الصور الرمزية من الخط. إذا كان الخط لا يحملها، فلا يوجد شيء لالتقاطه، وهذا هو السبب في أن فشل النشر الكلاسيكي - لا تشوبه شائبة على جهاز المطور، مربعات فارغة على خادم العميل بعد استبدال صامت للخط - هو مشكلة تغطية، وليس مشكلة تشكيل. العلاج العملي، تسجيل خط تشحنه بدلاً من الوثوق بكل ما قام الجهاز بتثبيته، تم شرحه خطوة بخطوة في المقال المرجعي. النقطة المفاهيمية هي أنه يجب إثبات التغطية قبل أن يكون لأي سؤال تشكيل معنى، وأنه يمكن إثباتها برمجيًا بدلاً من التحديق في المخرجات

// After RegisterUnicodeTTF, audit coverage for the
// codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628);  // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);

التسجيل نفسه يحمل قيدين - حد PDF 1.5 الأدنى لمعالجة Unicode المضمنة وبتات إذن تضمين الخط - كلاهما مغطى جنبًا إلى جنب مع خطوات الإعداد في مقال مرجعي RtLTextOut. ما يخص هنا هو عادة التدقيق (audit habit): يعد GetUnicodeGlyphForCodepoint نظام الإنذار المبكر الخاص بك. قم بالسير عبر نطاقات نقاط الكود التي تستخدمها بياناتك فعليًا عند بدء تشغيل الخدمة وسجل معرفات الصور الرمزية (glyph IDs) التي تعود. ثم تظهر فجوة التغطية كسطر في سجل بدء التشغيل أثناء النشر (rollout)، بدلاً من إظهارها كأحرف مفقودة في فاتورة وصلت بالفعل إلى عميل

ترتيب القراءة ينتمي إلى المستند، وليس إلى الصور الرمزية

الحصول على كل شكل بشكل صحيح لا يزال يترك شيئًا واحدًا غير مكتمل. يحدد ISO 32000-1 §12.2 تفضيل عارض (viewer preference) يسمى /Direction ينص على ترتيب القراءة الإجمالي للمستند. إنه لا يلمس أي صور رمزية. ما يفعله هو إخبار العارض بكيفية ترتيب الانتشار المزدوج (two-up spreads)، وأي جانب يجب أن يبدأ منه تخطيط الصفحة المقابلة، وأي اتجاه يجب أن تميل إليه واجهة القراءة (reading UI). لا يظهر أي من ذلك في صفحة واحدة، وهذا هو بالضبط سبب نسيانه

// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft;  // adds vpDirection to ViewerPreferences

إعداد Direction هو المهمة بأكملها: يضيف محدد الخاصية vpDirection إلى ViewerPreferences في المستند، لذلك يحمل سطر واحد التفضيل في الملف. إذا خرج النص من خلال RtLTextOut، فستحصل على هذا مجانًا، لأن الاستدعاء يقلب اتجاه المستند كأثر جانبي - يغطي المقال المرجعي متى يحتاج المستند المختلط إلى التراجع عن ذلك. الحالة التي يجب عليك فيها تعيينه بنفسك هي مستند من اليمين إلى اليسار تم إنتاجه بأي طريقة أخرى، على سبيل المثال من إدخال قمت بتشكيله مسبقًا في المرحلة السابقة (upstream) ورسمته عبر المسار العادي. اتركه في الخارج والمسودة ذات الصفحة الواحدة التي تنظر إليها تبدو متطابقة في كلتا الحالتين؛ ثم يقوم شخص ما بطباعة كتيب على الوجهين (duplex)، فتخرج الصفحات المزدوجة معكوسة، والسبب هو سطر واحد مفقود من أسابيع سابقة

التحقق من المخرجات المشكلة

تحقق من البداية إلى النهاية، لأن الصفحة يمكن أن تبدو صحيحة ولا تزال عديمة الفائدة لكل شيء في المراحل اللاحقة. تكتشف ثلاث عمليات تحقق معظم المشكلات. انسخ النص مرة أخرى من Acrobat وقارن نقاط الكود بسلسلة المصدر الخاصة بك. قم بتشغيل بحث العارض داخل المستند عن كلمة يمكنك رؤيتها على الصفحة. وافتح الإخراج على جهاز لا يحتوي على خطوط التطوير الخاصة بك، وهو الجهاز الأكثر احتمالاً لكشف عملية استبدال. لا شيء من هذا يحل محل القارئ الأصلي الذي ينظر إلى مستند حقيقي واحد، والذي يكتشف أشياء لا تكتشفها أي مجموعة نصوص اصطناعية (synthetic corpus). ضع هذه المراجعة في التقويم قبل شحن التنسيق

اختر سلاسل الاختبار عن قصد بدلاً من إعادة تدوير أي شيء أرسله مترجم العام الماضي. حد أدنى قابل للعمل لكل لغة (locale): جملة نصية خالصة، وجملة بأسماء علامات تجارية لاتينية مضمنة، وسطر يحمل أرقامًا وعملة، وأسماء مع تشكيل أو علامات تجميع. تكسر أسماء العملاء الحقيقيين الافتراضات التي يتركها النص البديل (filler text) دون مساس، لذا دع مجموعة الانحدار (regression set) تنمو بمقدار سلسلة واحدة في كل مرة تكشف فيها حالة دعم عن نمط لم تره من قبل

تتم تغطية تسجيل الخطوط، والتقسيم الفرعي (subsetting)، وواجهة برمجة تطبيقات رسم النصوص اليومية في المقال حول مخرجات التقارير والخطوط والصور باستخدام HotPDF. عندما يتعين على نفس المستندات أيضًا تلبية ملفات تعريف إمكانية الوصول، فإن قواعد وضع علامات اللغة والهيكل في مقال التحقق من صحة PDF/A و PDF/UA تجلس فوق عمل التشكيل هنا

تأتي واجهات برمجة تطبيقات الخطوط من اليمين إلى اليسار و Unicode الموضحة أعلاه مع مكون HotPDF لـ Delphi و C++Builder؛ تربط صفحة المنتج بالمرجع الكامل لإخراج النص