يختار المصمم خطاً يحتوي على حرف a أحادي الطابق للعناوين، أو صفراً مائلاً للجداول، أو مجموعة من الأحرف الاستهلالية المزخرفة للغلاف. هذه المحارف (glyphs) موجودة في الخط بالفعل. إنها ببساطة ليست الافتراضية. الحرف a الافتراضي يعين من الحرف عبر جدول cmap إلى محرف واحد، والبديل يقع على بعد معرفات محارف قليلة، ولا يمكن الوصول إليه إلا من خلال قاعدة استبدال. إنتاج هذا البديل في ملف PDF يعني قراءة القاعدة وإرسال المحرف البديل في تدفق المحتوى. يدور هذا المقال حول قراءة تلك القواعد، من نوع الاستبدال المفرد، في Object Pascal بدون مكتبة تشكيل أصلية كامنة تحته.
النطاق ضيق عمداً. المجموعات والبدائل الأسلوبية هي استبدالات محرف بمحرف (يدخل محرف واحد ويخرج محرف واحد). إنها جزء من تخطيط OpenType الذي يمكنك حله بمسار جدول صغير ومحدد، مما يجعلها مناسبة تماماً لمحرك Pascal الذي يريد البقاء خالياً من تبعيات لغة C.
لماذا Delphi الخالص بدلاً من HarfBuzz
مكتبة HarfBuzz هي الإجابة الواضحة لـ "تشكيل هذا النص"، ولالتشكيل الكامل ثنائي الاتجاه، أو الهندي، أو العربي، هي الإجابة الصحيحة. وهي أيضاً مكتبة مكتوبة بلغة C. وربطها بمنتج Delphi أو C++Builder يعني شحن كائن أصلي لكل منصة وبنية مستهدفة، ومطابقة اتفاقية الاستدعاء الخاصة بها، وتتبع وتيرة إصدارها، وقراءة شروط ترخيصها مقابل شروط ترخيصك. لا شيء من هذا صعب بمفرده. ولكن كل ذلك يمثل احتكاكاً لا يزول أبداً، ولا يشتري شيئاً عندما يكون المطلوب الفعلي هو "أعطني شكل ss01 لهذا الحرف".
الاستبدال المفرد لا يحتاج إلى محرك تشكيل. إنه يحتاج إلى محلل لعدد قليل من تنسيقات جداول GSUB الفرعية وبحث ثنائي أو اثنين. كتابة ذلك بلغة Pascal تبقي سلسلة الأدوات بأكملها داخل مترجم واحد. والحد الحقيقي هو أن هذا النهج يتعامل مع عمليات البحث عن استبدال المحارف ولا شيء غير ذلك. إنه ليس حلاً للكتابة ثنائية الاتجاه (bidi)، وليس إعادة ترتيب هندية، وليس تشكيلاً سياقياً تلقائياً. عندما تكون هذه الأشياء مطلوبة، فإنها تظل مطلوبة، ولن يحل محلها استعلام الاستبدال المفرد.
هيكل GSUB، من الأعلى إلى الأسفل
يتم تنظيم جدول استبدال المحارف (Glyph Substitution) كسلسلة من التوجيهات غير المباشرة، ويمشي استعلام الاستبدال في السلسلة من الأعلى. في الأعلى يوجد ScriptList. يحدد وسم النص (script tag) مثل latn agenda مدخلاً ما، والوسم الخاص DFLT هو النص الافتراضي الذي يتم تطبيقه عندما لا يتطابق أي نص أكثر تحديداً. يشير مدخل النص إلى LangSys، وهو نظام اللغة، مع وجود نظام لغة افتراضي للحالة العامة وأنظمة مسماة اختيارية للغات التي تحتاج إلى سلوك مختلف. التركية هي المثال المعتاد، حيث يتطلب الحرف i بنقطة وبدون نقطة معالجة خاصة بكل منهما.
يسمي LangSys مجموعة من فهارس الميزات. يشير كل فهرس إلى FeatureList، حيث يحمل سجل الميزة وسماً من أربعة بايتات، من بينها ss01، وقائمة بفهارس البحث. تشير هذه الفهارس أخيراً إلى LookupList، حيث تعيش جداول الاستبدال الفرعية الفعلية. لذا فإن حل ss01 يعني: العثور على النص، والعثور على LangSys الخاص به، والعثور على الميزة التي يحمل وسمها القيمة ss01، وجمع عمليات البحث التي تسميها، وتطبيقها. يقفز HotPDF افتراضياً إلى النص DFLT ونظام اللغة الافتراضي، وهو ما تشحن به الغالبية العظمى من تصميمات النصوص اللاتينية، ويكشف عن طريقة لتجاوز وسم النص عندما يربط الخط ميزاته تحت نص معين بدلاً من ذلك.
جداول التغطية تقرر من يشارك
يبدأ كل جدول استبدال فرعي بنفس السؤال: هل يشارك محرف الإدخال هذا في هذه القاعدة، وإذا كان الأمر كذلك، فأين يقع في الفهرسة الخاصة بالقاعدة نفسها. تجيب على هذا السؤال شجرة التغطية (Coverage table)، والإجابة هي فهرس التغطية، وهو ترتيب صغير يستخدمه باقي الجدول الفرعي للبحث عما يصبح عليه المحرف.
تأتي التغطية في تنسيقين. التنسيق 1 عبارة عن قائمة بمعرفات المحارف المرتبة بترتيب تصاعدي. تجد المحرف باستخدام بحث ثنائي، وموضعه في القائمة هو فهرس التغطية الخاص به. والتنسيق 2 عبارة عن قائمة بسجلات النطاق، كل منها عبارة عن محرف بداية، ومحرف نهاية، وفهرس التغطية الذي يعينه محرف البداية. يحصل المحرف داخل النطاق على فهرس التغطية الخاص به عن طريق الإزاحة من بداية النطاق. التنسيق 1 مدمج عندما تكون المحارف المشاركة متناثرة، والتنسيق 2 عندما تقع في مجموعات متجاورة. كلاهما مرتب، لذا يتم البحث في كليهما في وقت لوغاريتمي، ويعيد كلاهما إما فهرس تغطية أو رسالة واضحة "غير مغطى" تتيح للمحرك ترك المحرف لحاله.
الاستبدال المفرد، التنسيقان
الاستبدال المفرد هو من نوع البحث 1 (LookupType 1)، ويعين محرفاً واحداً إلى بديل واحد بالضبط. وله أيضاً تنسيقان، والانقسام بينهما هو تحسين للمساحة. يخزن التنسيق 1 فرقاً واحداً موقعاً (delta). معرف محرف المخرجات هو معرف محرف المدخلات بالإضافة إلى هذا الفرق، باقي قسمة 65536. هذه هي الطريقة التي يرمز بها الخط استبدالاً حيث يقع كل محرف مشارك عند نفس الإزاحة الثابتة من بديله، على سبيل المثال كتلة من الأرقام الخطية الموضوعة على مسافة ثابتة من الأرقام القديمة المطابقة. يوضح جدول التغطية المحارف المؤهلة، ويخدم الفرق الواحد جميعها.
يخزن التنسيق 2 مصفوفة صريحة من معرفات المحارف البديلة. فهرس التغطية من جدول التغطية هو الفهرس في تلك المصفوفة، وبالتالي يصبح المحرف عند فهرس التغطية 0 هو المدخل الأول للمصفوفة، وفهرس التغطية 1 هو الثاني، وهكذا. يتم استخدام التنسيق 2 عندما لا تكون البدائل عند إزاحة موحدة، وهي الحالة الشائعة للمجموعات الأسلوبية المبنية يدوياً. الاستعلام هو نفسه من جانب المستدعي في كلتا الحالتين. خذ محرف الإدخال، ومرره عبر التغطية، وإذا كان مغطى، فطبق الفرق أو اقرأ فتحة المصفوفة.
var
Pdf: THotPDF;
BaseGID, AltGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
Pdf.SetFont('My Stylistic Face', 12, []);
// Default glyph for 'a' through the font's cmap.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID means the feature did not touch this glyph.
if AltGID <> BaseGID then
{ emit AltGID in the content stream };
finally
Pdf.Free;
end;
end;
العقد الذي يستحق الملاحظة هو التمرير. يعيد GetSingleSubstituteGlyph معرف محرف الإدخال دون تغيير في كل حالة عدم مطابقة: لا يوجد خط، ولا جدول GSUB، ولا ميزة مطابقة، ولا تغطية. هذا يعني أن الاستدعاء آمن لإجرائه دون شروط. تطلب البديل، وإذا لم يكن هناك بديل، تحصل على ما وضعته بالضبط، لذا لا يحتاج كود الاستدعاء أبداً لمعالجة حالة خاصة لخط يفتقر إلى الميزة.
ما تعنيه وسوم الميزات الأسلوبية
وسم الميزة هو المفردة الكاملة للبديل الذي تطلبه، والوسوم ذات الصلة بالعمل الأسلوبي هي قائمة قصيرة. الزوج الرئيسي هو salt، البدائل الأسلوبية، وهو الوصول الشامل للأشكال البديلة للمحرف، والوسوم من ss01 إلى ss20، وهي المجموعات الأسلوبية العشرين المرقمة التي يمكن للخط تعريفها، كل منها حزمة مسماة من الاستبدالات التي يجمعها المصمم معاً. قد يضع الخط حرف a أحادي الطابق وحرف R مستقيم الساق تحت ss03، على سبيل المثال، لذا فإن تمكين تلك المجموعة يعيد تصميم كليهما.
وحولها تقع عدة وسوم استبدال مفردة إضافية. aalt هو الوصول إلى جميع البدائل، وهو اتحاد كل البدائل التي يملكها المحرف، ويُعرض عادةً كميزة لوحة المحارف. يختار titl الأحرف الاستهلالية للعنونة المصممة للأحجام الكبيرة. يقوم subs وsups بتبديل الأرقام السفلية والعلوية الحقيقية بدلاً من الأرقام الافتراضية المصغرة. ينتج ordn الأشكال الترتيبية، وهي الأحرف المرفوعة في الأول والثاني. يبني frac الكسور، على الرغم من أن الكسور القطرية الكاملة تعتمد أيضاً على منطق الترابط والسياق الذي يتجاوز الاستبدال المفرد البسيط. بالنسبة لحالات المحرف المفرد، فإن الآلية مطابقة لـ ss01: مرر الوسم إلى استعلام الاستبدال واقرأ المحرف البديل.
// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
const PreferredTag: AnsiString): Word;
begin
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
if Result = BaseGID then
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
// Still BaseGID if neither feature covers this glyph.
end;
تنسيق cmap 12 والمستويات التكميلية
قبل تشغيل أي استبدال، يجب أن يصبح الحرف محرفاً، وهذه وظيفة جدول cmap. يبدأ استعلام الاستبدال من معرف محرف، لذا فإن المسار دائماً هو من الحرف إلى المحرف عبر cmap، ثم من المحرف إلى البديل عبر GSUB. الجزء المثير للاهتمام في cmap هو نطاق وصوله. يغطي الجدول الفرعي من التنسيق 4 المستوى متعدد اللغات الأساسي (BMP)، وهو أول 65536 نقطة كود، وهذا كافٍ لمعظم النصوص اللاتينية. وهو غير كافٍ لنقاط الكود من U+10000 فصاعداً، وهي المستويات التكميلية، حيث تعيش الآن الأحرف والأرقام الرياضية، والعديد من الرموز، والعديد من النصوص البرمجية الحية.
التنسيق 12 هو الجدول الفرعي الذي يغطي النطاق الكامل من U+0000 إلى U+10FFFF. وهو عبارة عن قائمة مرتبة من المجموعات، كل مجموعة بها نقطة كود بداية، ونقطة كود نهاية، ومعرف محرف بداية، بحيث يعين تشغيل متجاور لنقاط الكود إلى تشغيل متجاور للمحارف. يحل HotPDF نقاط الكود باستراتيجية هجينة تطابق كيفية تشكيل البيانات. يتم خدمة نقاط الكود في BMP من مصفوفة مباشرة مفهرسة بنقطة الكود، وهو بحث واحد بدون بحث ثنائي. بينما يتم خدمة نقاط الكود في المستويات التكميلية من جدول متناثر مرتب حسب نقطة الكود ويتم البحث فيه باستخدام بحث ثنائي. والنتيجة هي أن GetUnicodeGlyphForCodepoint يأخذ Cardinal كاملاً ويجيب بشكل صحيح عبر النطاق بأكمله، ويعيد معرف المحرف 0، وهو المحرف .notdef، لأي نقطة كود لا يعينها الخط.
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // format 12 lookup
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // font has no glyph for this code point
end;
أين تتوقف هذه الاستعلامات
تجيب واجهات برمجة تطبيقات الاستبدال المفرد على شكل واحد من الأسئلة، ويجدر بنا أن نكون واضحين بشأن ما لا تجيب عليه. نوع البحث 1 هو واحد من ثمانية أنواع استبدال. لا يتعامل الاستعلام مع استبدال متعدد من النوع 2، حيث يصبح المحرف الواحد عدة محارف، ولا استبدال الترابط من النوع 4، حيث تصبح عدة محارف محرفاً واحداً. ولا يتعامل مع الأنواع السياقية والارتباطية السياقية، أنواع البحث 5 و6، التي تعمل فقط عندما يظهر محرف في جوار معين، ولا أنواع التمديد والارتباط العكسي. الكسر القطري، أو حرف ديواناجاري منضم، أو تتابع الأحرف العربية في البداية والوسط والنهاية هي مشكلة تسلسل، ولا يمكن لاستعلام استبدال مفرد لكل محرف التعبير عنها.
كما أنها لا تقوم بالتشكيل التلقائي. لا شيء هنا يفحص تشغيل النص، ويقرر الميزات التي يجب تشغيلها، ويطبقها بالترتيب الذي يتطلبه النص. يختار المستدعي وسم الميزة ويطبقه محرفاً بمحرف. هذه هي الأداة الصحيحة تماماً للمجموعات والبدائل الأسلوبية، وهي اختيارية ومحلية، وهي الأداة الخاطئة تماماً لنص يحتاج إلى إعادة ترتيب. الحفاظ على وضوح الحدود هو ما يسمح لمسار الاستبدال بالبقاء صغيراً ومتوقعاً.
بالنسبة للحالات التي تحتاج إلى عمل على مستوى التسلسل، تم تناول قصة النصوص المعقدة في مقالنا حول تشكيل نصوص البرمجة المعقدة في Delphi. إذا كانت استبدالاتك جزءاً من وظيفة تقرير أكبر تضع أيضاً صوراً وخطوطاً أخرى على الصفحة، فإن دليل تقرير المخرجات مع الخطوط والصور يغطي كيفية ملاءمة تلك الأجزاء معاً. تعمل كل هذه الميزات على نفس المحرك، وهو مكون HotPDF لـ Delphi وC++Builder، والذي يحمل استعلامات استبدال GSUB إلى جانب تضمين الخطوط، والتجزئة الفرعية، وواجهات برمجة تطبيقات النصوص المغطاة في مكان آخر من هذه المدونة.