مقال تقني

النص الرأسي CJK في PDF باستخدام Delphi وHotPDF

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

يمنحك HotPDF مفتاحًا يتولى عنك الحساب على مستوى كل حرف. فالخط الذي تضبطه يحمل العلامة IsVertical، وبمجرد تفعيلها، يجعل استدعاء TextOut واحد سلسلة كاملة تتراص داخل عمود رأسي بدلًا من أن تمتد على baseline. موضع العمود، والترتيب من اليمين إلى اليسار، واستبدال glyph مهم بصمت، كلها موضوعات تشرحها بقية الصفحة

صفحة PDF بحجم A4 من إنتاج HotPDF تُظهر نصًا صينيًا ويابانيًا وكوريًا مكدسًا في أعمدة رأسية تُقرأ من اليمين إلى اليسار
A single A4 page with Chinese, Japanese, and Korean text in vertical columns, generated from one Delphi procedure

المفتاح موجود في SetFont

التخطيط الرأسي ليس خاصية للصفحة أو للمستند. إنه خاصية للخط الذي ترسم به، وتفعّله عبر الوسيط الخامس في SetFont:

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// الوسيط الخامس يحوّل الخط الحالي إلى الوضع الرأسي.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // أفقي
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // رأسي

وبما أن العلامة تابعة للخط، فإنك تنتقل بين الكتابة الأفقية والرأسية ببساطة عبر استدعاء SetFont مرة أخرى مع وسيط أخير مختلف. يمكن للصفحة أن تحمل عنوانًا أفقيًا في الأعلى ونصًا رأسيًا في الأسفل، ويُبقي HotPDF النمطين منفصلين بحسب كائن الخط لا بحسب الصفحة. هذا ما يجعل التخطيطات المختلطة ممكنة من دون أي معالجة خاصة على جانبك: كل TextOut بعد SetFont رأسي يكدّس النص، وكل TextOut بعد أفقي يسير على baseline، وآخر SetFont هو الذي يحسم الأمر

الوسيط الرابع هو مجموعة أحرف Windows، وهي نفسها التي يأخذها الاستدعاء الأفقي. يتيح تمرير DEFAULT_CHARSET للنظام أن يحل glyphs بحسب السلسلة، وهذا مهم هنا لأن المستند الرأسي يخلط بين عدة نصوص كثيرًا. وينطبق كل شيء آخر يخص الخط أيضًا: يجب أن يكون مثبتًا على جهاز البناء، وغالبًا سترغب في FontEmbedding := True حتى يَرسم الملف glyphs CJK نفسها على قارئ لم يكن يملك Arial Unicode MS أبدًا

استدعاء TextOut واحد هو عمود واحد

مع تفعيل خط رأسي، لا يعود استدعاء TextOut يوزع السلسلة أفقيًا من النقطة المعطاة. بل يضع أول حرف في الأعلى ثم ينزل بالباقي مباشرة إلى الأسفل، متقدمًا بمقدار line height الخاص بالخط لكل glyph. إحداثي X الذي تمرره يثبت العمود؛ وإحداثي Y يحدد أين يبدأ أعلى العمود. لذا فإن تخطيط فقرة فعلية يعني استدعاء TextOut واحد لكل عمود، ثم تحريك X إلى اليسار بين الاستدعاءات، لأن أعمدة CJK تُقرأ من اليمين إلى اليسار

var
  Pdf: THotPDF;
const
  ColTop = 80;    // Y التي يبدأ عندها كل عمود، من الأعلى (بالنقاط)
  ColGap = 28;    // المسافة الأفقية بين الأعمدة
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // ضمّن وجه CJK لعرض قابل للنقل
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // عنوان أفقي أولًا، بوضع الكتابة العادي.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 40, 0, 'Tang poem, vertical layout');

    // حوّل الخط إلى الوضع الرأسي؛ كل TextOut أدناه يكدّس الآن.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // الأعمدة تتقدم من اليمين إلى اليسار، لذا X تنقص مع كل استدعاء.
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

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

الإحداثيات: إلى أسفل الصفحة، ومن اليمين إلى اليسار

هناك محوران يعملان هنا ويدفع كل منهما في اتجاه معاكس، لذا يجب أن نكون دقيقين. يقيس HotPDF من الزاوية العلوية اليسرى للصفحة مع ازدياد Y نحو الأسفل، بالنقاط، وهو الإطار نفسه الذي تستخدمه لوحة VCL. لذلك فإن العمود الرأسي يبدأ عند Y صغير، قريب من أعلى الورقة، ثم تنزل الأحرف من هناك نحو قيم Y أكبر، بتقدم glyph واحد في كل مرة في وضع الكتابة الرأسي. أنت تضبط قيمة Y الابتدائية مرة واحدة لكل عمود، ويتولى المكوّن النزول بعد ذلك

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

glyphs يجب أن تغيّر شكلها

بعض الأحرف ليست مجرد نسخ مدوّرة من نظيراتها الأفقية؛ يجب استبدالها عندما يتحول النص إلى الوضع الرأسي. والحرف الذي يوقع معظم التطبيقات في الفخ هو علامة المد اليابانية ー ‏(U+30FC)، وهي شريط المد الطويل الذي يظهر في كلمات الكاتاكانا مثل コーヒー. عندما ترسم أفقيًا تكون شرطة قصيرة على baseline. ولو رصصت glyph نفسه داخل عمود فسيمتد أفقيًا عبر العمود، وهذا خطأ: ففي اليابانية الرأسية تتحول العلامة إلى خط رأسي يصل بين الحرفين اللذين تقع بينهما. أما هل يصلحه HotPDF عنك فيتوقف على الاستدعاء. فـ RtLTextOut مع خط رأسي ترصّ السلسلة glyph بعد glyph وتستبدل U+30FC بشريط رأسي (U+007C). أما TextOut، وهو الاستدعاء المستخدم في هذه الصفحة، فلا يجري أي استبدال من هذا النوع: فهو يكتب السلسلة عبر ترميز Identity-V الخاص بالخط، فالـ glyph الذي تحصل عليه هو ما يربطه الخط بـ U+30FC، وعادةً تكون العلامة الأفقية. وللحصول على الشكل الرأسي من TextOut، أضف sfVerticalAlternates إلى THotPDF.ShapingFeatures حتى يتولى الاستبدال vert / vrt2 المدمج في الخط توفيرها، أو استبدل الحرف بنفسك قبل الاستدعاء

علامة المد الطويلة هي الحالة التي تكسر معظم التطبيقات الساذجة، لكن يجدر أن تعرف أين تتعمق المشكلة العامة أكثر. فالتنضيد الرأسي الكامل يدير أيضًا الحروف اللاتينية وعلامات الترقيم الغربية بتدوير 90 درجة، ويحرك kana الصغيرة، ويعيد تموضع الأقواس والفواصل إلى أشكالها الرأسية، ويعيش التنفيذ الكامل لذلك في ميزات OpenType الرأسية الخاصة بالخط لا في قواعد ثابتة. يستطيع HotPDF الاستفادة من تلك الميزات عندما يحملها الخط: البدائل الرأسية، أي ميزات GSUB vert وvrt2، والتباعد الرأسي، أي عمليات GPOS vkrn وvpal، كلها اختيارية تُطبّق على المسار الرأسي فقط عندما يعرّفها الخط النشط بالفعل. وبالنسبة إلى النص الأيديوغرافي داخل وجه واحد واسع التغطية مثل Arial Unicode MS، تكفي خطوة line height متساوية لإنتاج أعمدة صحيحة وقابلة للقراءة؛ أما ميزات OpenType فتهمك بمجرد أن تحتاج علامات المد الطويل أو kana الصغيرة أو علامات الترقيم إلى أشكالها الرأسية، وعندما تنتقل إلى خط مصمم للتنضيد الرأسي الدقيق وتريد منه تموضع kana الطبيعي وتباعد الحروف

خلط النصوص والاتجاهات في صفحة واحدة

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

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

من المثال إلى الإنتاج

الأجزاء صغيرة وتتآلف بشكل متوقع. فعّل العلم الرأسي في SetFont، وأعطِ استدعاء TextOut واحدًا لكل عمود من Y علوي ثابت، وأنقص X عبر الاستدعاءات حتى تُقرأ الأعمدة من اليمين إلى اليسار. ضمّن الخط حتى تصمد glyphs CJK في انتقالها إلى جهاز القارئ، وفعّل sfVerticalAlternates حتى يوفر الخط الذي يحمل ميزتي vert / vrt2 الأشكال الرأسية، من بينها علامة المد الطويل U+30FC، أو استبدل تلك العلامة بنفسك إذا كان الخط يفتقر إلى هذه الميزة. ومن هناك، فإن تحويل التخطيط إلى إنتاج هو في معظمه حساب: اشتقاق مواضع X للأعمدة من عرض كتلة مقاس، وتقسيم المقاطع الطويلة إلى أعمدة تناسب ارتفاع الصفحة، وترك مساحة للعناوين الأفقية وأرقام الصفحات

وللجانب الأوسع من النص والخط الذي يرتكز عليه هذا، بما في ذلك اصطلاحات TextOut الأفقية وتسجيل خطوط Unicode، راجع مثال Hello World متعدد اللغات ومثال TextOut. ومفتاح SetFont الرأسي واستدعاءات TextOut المعروضة هنا جزء من مكوّن HotPDF الخاص بـ Delphi وC++Builder