الضبط الكامل (Full justification) هو التخطيط الذي يجعل عمود النص يصطف على الحافتين اليسرى واليمنى، وهو المظهر الذي تتوقعه من كتاب مطبوع أو تقرير رسمي. من السهل وصفه ولكن من السهل بشكل مفاجئ أن تخطئ فيه، لأن الإجابة على سؤال "أين تذهب المساحة الإضافية" ليست هي نفسها بالنسبة للغة الإنجليزية كما هي بالنسبة للغة اليابانية، ولأن الطريقة الساذجة لقياس كل سطر تحول الصفحة السريعة إلى صفحة بطيئة. يمنحك HotPDF ضبطاً مدركاً للبرنامج النصي (script-aware justification) من خلال استدعاء تخطيط صندوق واحد، وتحت هذا الاستدعاء يكمن إصلاح أداء نموذجي يستحق الفهم بحد ذاته
يستعرض هذا المقال كلا الأمرين. أولاً، القاعدة المطبعية (typographic rule) التي تقرر كيفية توزيع التراخي (slack) للبرامج النصية ذات الفجوات بين الكلمات مقابل تلك التي لا تحتوي عليها. ثانياً، التغيير في القياس الذي خفض تكلفة الضبط لكل صفحة بحوالي ثمانين مرة دون أي اختلاف مرئي في المخرجات. كلاهما مهم إذا قمت بإنشاء مستندات بكميات كبيرة وتريد أن تُقرأ مثل تنضيد الحروف الحقيقي (real typesetting) بدلاً من مخرجات أحادية المسافة (monospaced) وممتدة لتتناسب
ما يتطلبه الضبط الكامل في الواقع
نادراً ما يصل سطر النص المرسوم بعرضه الطبيعي إلى الحافة اليمنى للعمود الخاص به. يوجد دائماً باقٍ، وهو التراخي (slack)، بين المكان الذي ينتهي فيه آخر رمز (glyph) والمكان الذي يقع فيه حد العمود. تترك المحاذاة لليسار ذلك التراخي على اليمين. المحاذاة لليمين تنقله إلى اليسار. التوسيط يقسمه. يقوم الضبط الكامل بإزالته عن طريق توسيع السطر نفسه حتى تلتقي كلتا الحافتين بالصندوق، والطريقة الصادقة الوحيدة للقيام بذلك هي دفع الرموز بعيداً عن بعضها من الداخل
القاعدة التي تفصل الضبط الجيد عن السيئ هي المكان الذي تضع فيه التراخي. البرنامج النصي الذي يكتب كلمات مع مسافات بينها، مثل اللغة الإنجليزية وبقية العائلة اللاتينية، له فواصل طبيعية في كل مسافة بين الكلمات. إن توسيع هذه المسافات غير مرئي للعين لأن القراء يتقبلون بالفعل أن فجوات الكلمات تختلف. البرنامج النصي الذي يكتب بدون فجوات بين الكلمات، مثل رموز هان الصينية (Han)، أو الكانا اليابانية (kana)، أو الهانغول الكورية (Hangul)، ليس لديه مثل هذه الفواصل. هناك يجب أن ينتشر التراخي بالتساوي بين الرموز المتجاورة، وهو المبدأ الذي يسميه منضدو الحروف اليابانيون kintou-waritsuke، التباعد المتساوي (even spacing). إن وضع امتداد لفجوة الكلمات على النمط اللاتيني على سطر CJK، أو حشر كل التراخي في المكان الواحد الذي يصادف أن يحتوي سطر CJK فيه على مسافة، ينتج عنه الأنهار والفجوات التي تميز مخرجات الهواة
كيف يقرر HotPDF أين تذهب المساحة
يتخذ HotPDF هذا القرار لكل فجوة، وليس لكل سطر. عندما يضبط سطراً، فإنه يمر على كل زوج متجاور من الرموز (glyphs) ويسأل ما إذا كان هناك حد قابل للتمدد (stretchable boundary) يقع بينهما. يكون الحد قابلاً للتمدد عندما يكون أي من الجانبين مسافة أو علامة جدولة (tab)، وهي الحالة اللاتينية، أو عندما يكون كلا الجانبين أحرف CJK قابلة للكسر، وهي حالة التباعد المتساوي. يقوم بحساب تلك الحدود، ويقسم تراخي السطر بالتساوي بينها، ويضيف تلك الحصة إلى كل فجوة مؤهلة
تقع النتيجة بشكل طبيعي. يحتوي السطر الإنجليزي على حدود قابلة للتمدد فقط في مسافات كلماته، لذلك يهبط كل التراخي هناك وتنتشر الكلمات بعيداً بينما تحتفظ الحروف داخل كل كلمة بتباعدها الطبيعي. يحتوي سطر Han أو kana على حد قابل للتمدد بين كل زوج تقريباً من الرموز، وبالتالي يتم توزيع التراخي بالتساوي عبر السطر بأكمله، وهو بالضبط التباعد المتساوي بين الرموز الذي تتطلبه هذه البرامج النصية. السطر الذي عبارة عن كلمة لاتينية واحدة طويلة بدون مسافة داخلية ليس له أي حد قابل للتمدد على الإطلاق، لذلك يتركه HotPDF بعرضه الطبيعي بدلاً من تمزيق الكلمة حرفاً بحرف. يعالج نفس المنطق اللاتيني والمختلط من CJK في سطر واحد بدون حالات خاصة، لأن القرار محلي لكل حد
يتم استبعاد حد واحد عمداً في كل مكان. لا يتم أبداً التعامل مع الموضع الذي يلي الرمز النهائي للسطر على أنه فجوة، لأن التمدد هناك سيعيد ببساطة إدخال باقٍ على اليمين، وهو عكس الضبط (justification)
لماذا يُترك السطر الأخير وشأنه
السطر الأخير من الفقرة مميز، والخطأ فيه هو الخلل الأكثر شيوعاً في الضبط. عادة ما يكون السطر الأخير من الفقرة قصيراً، وغالباً ما يتكون من بضع كلمات فقط، وتمتد إلى عرض العمود بالكامل مما يسحب تلك الكلمات عبر الصفحة إلى صف مكسور ومتناثر. تترك الطباعة الصحيحة السطر الأخير بعرضه الطبيعي، مع محاذاته لليسار
يكتشف HotPDF السطر الختامي حسب الموضع. بينما يقوم بالتفاف النص إلى أسطر، فإنه يعرف متى يصل السطر الذي قام بتقسيمه للتو إلى نهاية السلسلة (string) المتوفرة. يتم إخراج ذلك السطر النهائي بمحاذاة عادية لليسار ويحتفظ بعرضه الطبيعي. يتم ضبط كل سطر قبله على كلتا الحافتين. يتم احترام فواصل الأسطر الثابتة (Hard line breaks) التي تكتبها في النص كما هي مكتوبة، لذلك لا يتم أبداً تمديد السطر القصير المقصود أيضاً. يرى القارئ كتلة مستطيلة نظيفة من النص ينتهي سطرها الأخير بشكل طبيعي، وهو ما تتوقعه العين
تكلفة القياس التي جعلت الضبط بطيئاً
لضبط سطر يجب أن تعرف عرضه الدقيق، ويجب أن تعرف تقدم (advance) كل رمز بحيث يمكنك وضع المساحة الإضافية بدقة. حصل التنفيذ الأول على هذه الأرقام بالطريقة الواضحة. قام بقياس السطر بأكمله من خلال استعلام كامل لعرض Unicode، ثم قام بقياس بادئة (prefix) تلو الأخرى لاستعادة تقدم كل رمز عن طريق إيجاد الفرق. بالنسبة لسطر مكون من N من الرموز، فهذا يعني N+1 من الاستدعاءات إلى محرك القياس، وكل استدعاء عبارة عن رحلة دائرية (round-trip) كاملة لـ GDI، يطلب من نظام التشغيل تشكيل النص وقياسه وإعادة الإجابة
لكل سطر يبدو ذلك رخيصاً. عبر الصفحة ليس كذلك. خذ صفحة A4 كثيفة من نص النص، ما يقرب من خمسة وأربعين سطراً يحتوي كل منها على حوالي ثمانين حرفاً. عند N+1 من الرحلات الدائرية لكل سطر، فإن ذلك يمثل حوالي 81 رحلة دائرية لكل سطر وحوالي 3645 للصفحة، يتم إنفاق معظمها تقريباً في إعادة قياس النص الذي نظر إليه المحرك بالفعل قبل لحظات. في مهمة مجمعة (batch job) تنتج آلاف الصفحات، فإن هذا الحمل الزائد يهيمن على وقت التخطيط، وكل رحلة دائرية تعبر الحدود بين عمليتك والنظام الفرعي للرسومات
استدعاء واحد بدلاً من N زائد واحد
الإصلاح هو نوع التغيير الذي يبدو صغيراً ويؤتي ثماراً كبيرة. يمكن لـ GDI بالفعل الإبلاغ عن العرض الإجمالي لسلسلة (string) وموضع كل رمز في استعلام واحد. يعرض HotPDF ذلك من خلال GetWideCharAdvances، والذي يملأ مصفوفة بالتقدم الطبيعي لكل رمز، بما في ذلك تقنين الأحرف (kerning)، ويعيد العرض الإجمالي، في استدعاء واحد بدلاً من N+1. يطلب روتين الضبط، _HPDFEmitJustifiedWideLine داخلياً، جميع التقدمات مرة واحدة، ويحسب التراخي، ويوزعه عبر الحدود القابلة للتمدد، ويخرج السطر
بالنسبة لصفحة A4 نفسها، ينخفض القياس لكل سطر من حوالي 81 رحلة دائرية إلى واحدة، لذا تنخفض الصفحة من حوالي 3645 رحلة دائرية إلى حوالي 45، وهو تخفيض يقترب من ثمانين ضعفاً. المخرجات متطابقة بايت مقابل بايت، لأنه لم يتغير شيء في القياس باستثناء عدد المرات التي يتم طلبها فيها. نفس محرك GDI، ونفس مقاييس الخط، ونفس تقنين الأحرف يغذي نفس الأرقام. فقط عدد الرحلات الدائرية هو الذي انخفض. عندما يكون القياس صحيحاً بالفعل، فإن التحسين الصحيح هو التوقف عن طلبه بشكل متكرر، وليس تقريبه
كيف يصل السطر إلى الصفحة
بمجرد تقسيم التراخي، يخرج HotPDF السطر باستخدام ExtTextOut ومصفوفة تقدم لكل رمز، مصفوفة Dx. يمثل كل إدخال المسافة من أصل رمز واحد إلى التالي، وهو التقدم الطبيعي لذلك الرمز بالإضافة إلى حصته من التراخي عندما يتبعه حد قابل للتمدد. يتم تعيين هذا مباشرة على نموذج تصوير PDF (PDF imaging model). يُكتب النص الموضوع بواسطة المشغل TJ، وهو مصفوفة تداخل مجموعات الرموز (glyph runs) مع تعديلات أفقية صريحة، وتصبح قيم Dx هي بالضبط تلك التعديلات. هذا هو السبب في أن المساحة الإضافية تهبط بين الرموز في مواضع نقاط فرعية (sub-point) دقيقة بدلاً من تزييفها بأحرف حشو (padding characters)، ولهذا السبب يقيس سطر HotPDF المضبوط بشكل صحيح إذا قرأته أداة تالية (downstream tool)
أنت لا تستدعي ExtTextOut بنفسك للفقرات المضبوطة. نقطة الدخول هي WideTextOutBox، والتي تلتف سلسلة Unicode في صندوق وتطبق المحاذاة التي تطلبها. يقوم بتقسيم النص إلى أسطر تناسب عرض الصندوق، ويضع كل سطر لأسفل ارتفاع الصندوق، ويعيد عدد الأحرف التي تمكن من احتواءها قبل نفاد المساحة الرأسية. يتم اختيار المحاذاة بواسطة نوع الضبط enum (justification enum)
type
THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);
الثلاثة الأولى واضحة بذاتها: محاذاة لليسار، والتوسيط، ولليمين. الرابع، jtJustify، هو الضبط الكامل على كلتا الحافتين الموصوف هنا، وهو القيمة التي يقرؤها WideTextOutBox لتشغيل التباعد المدرك للبرنامج النصي
ضبط فقرة في الممارسة العملية
يقوم مثال كامل بإنشاء مستند، وتعيين خط، وصب فقرة في صندوق مع ضبط كامل. يضبط الكود نفسه النص اللاتيني و CJK دون تغيير في العلامة (flag)، لأن إدراك البرنامج النصي (script-awareness) يعيش أسفل الـ API
uses
HPDFDoc;
procedure JustifyParagraph;
var
Pdf: THotPDF;
Body: WideString;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'Justified.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', 11);
Body :=
'Full justification spreads the slack on each filled line so both ' +
'edges meet the column, while the last line keeps its natural width. ' +
'For scripts with word gaps the space lands between words; for ' +
'scripts without them it spreads evenly between glyphs.';
// X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
لرسم نفس الكتلة بمحاذاة لليسار، أو توسيط، أو محاذاة لليمين، قم بتغيير الوسيطة الأخيرة فقط إلى jtLeft، أو jtCenter، أو jtRight. يظل الالتفاف (wrapping)، ووضع السطر، والقيمة المرتجعة كما هي. العرض المُقاس الذي يحرك المسارات الأربعة جميعها يأتي من GetWideTextWidth، وهو استعلام العرض المدرك لـ Unicode والذي يقيس WideString بشكل صحيح حيث أن القياس القديم بالبايت (byte-wise) كان سيخطئ في تحديد حجم أي شيء يتجاوز Latin-1، وهذا هو ما يجعل الصندوق يلتف حول نص CJK ونص الزوج البديل (surrogate-pair) في المكان المناسب لتبدأ به
الضبط هو إحدى طبقات مكدس تشكيل نص أكبر. عندما يحتوي سطر على برامج نصية تعيد ترتيب رموزها أو تنضم إليها، فإن قرارات التباعد هنا تجلس فوق العمل الموصوف في مقالنا حول تشكيل النص للبرنامج النصي المعقد (complex-script)، وعندما يحمل خط متغيرات مطبعية تريد تحديدها، راجع كيفية دفع البدائل الأسلوبية OpenType GSUB. كل ذلك يُشحن في مكون HotPDF لكل من Delphi و C++Builder، إلى جانب واجهات برمجة تطبيقات النص الأوسع، والتخطيط، والمستند التي تم تناولها عبر هذه المدونة