يصيّر HotPDF خطوط PDF المضمَّنة في Delphi دون تثبيت أي شيء على الجهاز: فخط أنابيب تصيير الأحرف الرسومية المضمَّنة في HPDFGlyphRender.pas يحلّل برامج الخطوط المخزّنة داخل ملف PDF نفسه — مخططات TrueType من جدول glyf في FontFile2، وسلاسل أحرف CFF Type 2 من FontFile3، وتدفقات محتوى أحرف Type 3 الرسومية — ويعيد تشغيلها كمسارات GDI متجهية معبّأة. وهذه المقالة هي الغوص المتعمّق في دقّة الخطوط الذي يقف خلف تصيير صفحات PDF إلى TBitmap باستخدام HotPDF: فتلك المقالة تغطي المُصيِّر ككل، أما هذه فتغطي كيف يحصل النص في تلك الصفحات على أشكاله الدقيقة
لماذا يُصيَّر ملف PDF بمربعات بدلًا من النص؟
المربعات أو الفراغات أو الأحرف الخاطئة بشكل خفي في مخرجات PDF المصيَّرة تعني دائمًا تقريبًا أن المُصيِّر طلب خطًا من نظام التشغيل بدلًا من استخدام الخط المضمَّن في الملف. وتصل الشكوى بالطريقة نفسها كل مرة: يبدو المستند مثاليًا على الجهاز الذي أنتجه، ثم يفتحه عميل على خادم نظيف أو سطح مكتب مقيَّد فتظهر الفاتورة اليابانية مربعات «توفو»، أو يزيح وجه بديل مشابه كل فاصل سطر. فالخطوط لم تكن يومًا على ذلك الجهاز — بل داخل ملف PDF فقط — والمُصيِّر الذي يتوقف عند استبدال خطوط النظام لا يستطيع رؤيتها. وتزيد الخطوط الفرعية الأمر سوءًا: فقد يحمل الخط الفرعي أربعين حرفًا رسوميًا تحت رموز أحرف مخصّصة لذلك الملف وحده، وهو تخصيص لا يشاركه أي خط مثبّت
يعرّف المعيار ISO 32000-1 §9.9 ثلاثة حوامل لبرنامج الخط المضمَّن في واصف الخط: يحمل FontFile برنامج Type 1 أصليًا، وFontFile2 برنامج TrueType، وFontFile3 ملف CFF مجرّدًا (Type1C أو CIDFontType0C) أو غلاف OpenType. وثمة نكهة رابعة، هي خط Type 3 في ISO 32000-1 §9.6.5، لا تضمّن أي شيء ثنائي على الإطلاق — فكل حرف رسومي فيها تدفق محتوى PDF صغير يُنفَّذ في مكانه. وتختلف الحوامل الثلاثة في رياضيات المخططات (منحنيات B-spline تربيعية مقابل سلاسل أحرف تكعيبية مقابل عوامل صفحة اعتباطية)، لذا يحتاج المُصيِّر الأمين إلى مفسّر مستقل لكل منها، إضافةً إلى طبقة ترميز تحوّل رموز الأحرف إلى فهرس الحرف الرسومي الصحيح قبل لمس أي مخطط
كيف يحوّل HotPDF مخططات TrueType في glyf إلى مسارات GDI؟
تقرأ THPDFEmbeddedTTF في HPDFGlyphRender.pas جدول loca لتحديد موضع سجل كل حرف رسومي، وتجتاز محيطات glyf نقطةً نقطة، وتُصدر مسار GDI. وثمة اصطلاحان في TrueType يحتاجان إلى معالجة صريحة. أولًا، النقاط المتتالية الواقعة خارج المنحنى تعني ضمنيًا نقطة على المنحنى عند منتصف المسافة بينها، والمحيط الذي تقع جميع نقاطه خارج المنحنى يبدأ عند منتصف المسافة بين آخر نقطة وأول نقطة فيه — أهمل أيًا من القاعدتين فتنمو على الأحرف الرسومية المستديرة أوجه مسطّحة أو تنهار. ثانيًا، منحنيات TrueType هي منحنيات بيزيه تربيعية بينما تأخذ PolyBezierTo في GDI منحنيات تكعيبية، لذا يُرفع كل مقطع تربيعي في الدرجة رفعًا دقيقًا بدلًا من تسطيحه إلى مقاطع مستقيمة
// رفع الدرجة الدقيق: تربيعي (P0, Q, P2) -> تكعيبي (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// ثم PolyBezierTo مع C1 و C2 و P2 — منحنى مطابق هندسيًا
رفع الدرجة عملية بلا فقد: فالمنحنى التكعيبي يرسم المنحنى نفسه تمامًا، ولذلك يطابق المخطط المصيَّر ما يرسمه عارض متوافق من الجدول نفسه، عند أي مستوى تكبير. وما يتبقى من العمل هو الوضع. فكل حرف رسومي مصمَّم بوحدات الخط (عادةً شبكة من 1000 أو 2048 وحدة لكل em)، ويركّب المُصيِّر مصفوفة القياس ومصفوفة النص ومصفوفة التحويل الحالية في تحويل واحد من الحرف الرسومي إلى الجهاز قبل تعبئة المسار. والترتيب هنا أهم مما يبدو: ركّب المصفوفات الثلاث نفسها بالعكس فينهار كل حرف رسومي نحو نقطة الأصل — صفحة تبدو خاطئة بينما علّتها الحقيقية سطر واحد من جبر المصفوفات
كيف يتعامل مفسّر سلاسل أحرف Type 2 مع خطوط CFF
تمنح THPDFEmbeddedCFF برامج FontFile3 مفسّرًا حقيقيًا لسلاسل أحرف Type 2: فهي تحلّل بنى INDEX في CFF، وقاموس Top DICT وقاموس Private DICT، ثم تنفّذ كل سلسلة أحرف وتُصدر مقاطع المسار مباشرةً إلى GDI. ويُنزع غلاف OpenType (حاوية OTTO) أولًا للوصول إلى جدول CFF المجرّد؛ أما تدفقات CIDFontType0C وType1C العارية فتُستهلك مباشرةً. وسلاسل الأحرف لغة مكدّس مضغوطة، وثلاثة من اصطلاحاتها تحدّد ما إذا كان المفسّر سيبقى متزامنًا مع تدفق البايتات. فبادئة العرض الاختيارية تعني أن أول عامل يفرّغ المكدّس قد يحمل معاملًا إضافيًا واحدًا في المقدمة. والعامل hintmask يعني ضمنيًا vstemhm حين تكون هناك معاملات لا تزال على المكدّس، وعدد بايتات القناع التي يجب تخطّيها يعتمد على عدد الأعمدة المتراكم — أخطئ في العدد مرة واحدة فتُقرأ كل شفرة عملية لاحقة قراءة خاطئة. واستدعاءات البرامج الفرعية تضيف انحيازًا إلى فهرسها (107 أو 1131 أو 32768 حسب عدد البرامج الفرعية) قبل البحث، لذا يقع الاستدعاء غير المنحاز على برنامج فرعي خاطئ تمامًا
ويضيف CFF المفتاح بـ CID مستوى تحويل إضافيًا يوقع التطبيقات الساذجة: فرمز الحرف يختار CID، لكن فهرس سلسلة الأحرف هو GID، ومجموعة أحرف الخط تربط GID بـ CID — لذا يبني المُصيِّر الربط العكسي من CID إلى GID قبل الرسم، ويختار قاموس Private DICT لكل حرف رسومي عبر FDSelect في الخطوط التي تحمل عدة قواميس. أما برامج Type1C المفتاحة بالأسماء، وهي الحامل المعتاد لخطوط Type 1 البسيطة، فتحلّ بدلًا من ذلك الرموز أحادية البايت عبر الترميز المدمج في برنامج CFF أو عبر آلية الترميز على مستوى PDF الموصوفة تاليًا. وثمة تحفّظ صريح واحد: يقرأ المفسّر عوامل التلميح ليُبقي التدفق متزامنًا لكنه لا ينفّذ التلميح، وهو حد سنناقشه في النهاية
ما هو خط Type 3 وكيف يُرسم؟
الحرف الرسومي في Type 3 ليس مخططًا على الإطلاق — إذ يعرّفه ISO 32000-1 §9.6.5 على أنه تدفق محتوى، لذا يصيّره HotPDF بدفع حالة الرسوميات، وتركيب مصفوفة الخط وحجم الخط ومصفوفة النص على CTM، وتنفيذ إجراء الحرف الرسومي عبر مفسّر العوامل نفسه الذي يرسم الصفحات، مع وضع /Resources الخاصة بالخط في النطاق. وثمة تفصيلان في المواصفة يهمّان للصحة. فقيم /Widths في Type 3 مُعبَّر عنها في فضاء الحرف الرسومي لا في فضاء النص 1/1000 الذي يستخدمه كل نوع خط آخر، لذا يجب أن تمر التقدّمات عبر /FontMatrix — وإلا تخطو خطوط الباركود ذات المصفوفة 0.01 خطوات خاطئة بمرتبة كاملة من الحجم. والإجراء الذي يبدأ بالعامل d1 يقطع وعدين يفرضهما المُصيِّر: يُقصّ الرسم على المربع المحيط المعلَن، وبحسب ISO 32000-1 §9.6.5.2 يتجاهل الحرف الرسومي عوامل اللون الخاصة به ويرسم بلون التعبئة الحالي للمستدعي، لذا تُكبت rg وg وk وتوائمها الخاصة بالتخطيط داخل الإجراء طوال مدة ذلك الحرف الرسومي. أهمل قاعدة اللون فيخرج خط باركود d1 ختمته الصفحة بالأزرق أسودَ؛ وأهمل القصّ فيرسم حرف رسومي مشوّه خارج خليته
كيف تتحوّل رموز الأحرف إلى معرّفات أحرف رسومية
مفسّرات المخططات نصف المهمة فحسب، لأن البايتات في سلسلة نص PDF هي رموز أحرف لا فهارس أحرف رسومية، ويخصّص ISO 32000-1 بندين فرعيين لهذا الربط. فللخطوط البسيطة، يفرض §9.6.6 أولوية صارمة: مصفوفة /Differences تتجاوز الترميز الأساسي (WinAnsiEncoding أو MacRomanEncoding أو StandardEncoding)، الذي يتجاوز بدوره خريطة برنامج الخط نفسه. ويحلّ HotPDF تلك السلسلة إلى جدول من 256 مدخلًا من الرمز إلى GID، مترجمًا أسماء الأحرف الرسومية إلى فهارسها بثلاثة طرق: مطابقة دقيقة لمجموعة الأحرف داخل برنامج CFF، والأسماء الرقمية gNN/glyphNN المأخوذة كفهارس حرفية، وترجمة الاسم إلى Unicode عبر Adobe Glyph List يتبعها بحث في cmap لبرامج TrueType. أما للخطوط المركّبة، فيضع §9.7 CIDToGIDMap في موضع القيادة: الحالة الشائعة هي /Identity، لكن المدخل قد يكون تدفقًا من أزواج big-endian مفهرسة بـ CID — ومخرجات Unicode الخاصة بـ HotPDF نفسها تستخدم صيغة التدفق هذه بالضبط للخطوط الفرعية المضغوطة، لذا فمسار التدفق ليس زاوية غريبة
// /CIDToGIDMap كتدفق: أزواج Word بترتيب big-endian مفهرسة بـ CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // ما هو خارج النطاق يُربط بـ .notdef
وحين يلزم بحث في cmap الخاص بـ TrueType، يجتاز HotPDF سلسلة احتياطية بدلًا من الوثوق بجدول فرعي واحد: تأتي جداول Windows Unicode الفرعية أولًا (الصيغة 4، ثم الصيغة 12 للمستويات التكميلية)، ثم جدول الرموز الفرعي (3,0) باصطلاح منطقة الاستخدام الخاص F000 المعكوس نزولًا إلى البايت الأدنى — وهو سبب استجابة خط رموز مثل Wingdings لرموز ASCII العادية — ثم الصيغتان القديمتان 6 و0. ولا تُفسَّر جداول الصيغة 2 الفرعية عمدًا: فهي تربط صفحات رموز قديمة متعددة البايتات مثل Shift-JIS و Big5 لا Unicode، والخطوط الصينية واليابانية والكورية الحديثة تحمل على أي حال جدولًا فرعيًا من الصيغة 4 أو 12 دون استثناء. وأي رمز لا ينجو من أي من هذه الطرق يعود إلى رسم GDI لذلك الحرف الرسومي وحده، فيُضعف الحرف الواحد الذي يتعذّر ربطه حرفًا رسوميًا واحدًا لا سلسلة النص بأكملها
ما الذي لا يفعله المسار المضمَّن
تستحق الحدود أن تُذكر بوضوح. فالتلميح لا يُنفَّذ — تُعبّأ المخططات كما صُمّمت، وهو ما لا يمكن تمييزه عن المخرجات المُلمَّحة عند 150 DPI فما فوق لكنه قد يختلف ببكسل واحد عن مُنقِّط مُلمَّح في الأحجام الصغيرة جدًا. ولا تُفسَّر برامج Type 1 الأصلية في FontFile (سلاسل أحرف مشفّرة بـ eexec)، ولا تُطبَّق محاور الخطوط المتغيّرة في OpenType؛ وكلتا الحالتين، شأنهما شأن برنامج خط تالف أو جدول glyf بلا أي cmap صالح للاستخدام، تعود إلى رسم خطوط النظام بدلًا من إفشال الصفحة. ويمتد النهج نفسه الذي يقدّم الدقة أولًا إلى أماكن أخرى في المُصيِّر — فـأنماط التظليل المحوري والشعاعي تنال المعاملة نفسها التي تستحقها التدرّجات — ولجانب التوليد قصته الخاصة في دقائق الخطوط في كيف ترتّب EndDoc الخطوط الفرعية
ولا يتطلب استخدام خط الأنابيب أي كود خاص بالخطوط على الإطلاق — فكل آلية مما سبق تعمل تلقائيًا داخل استدعاء تصيير الصفحة
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// تُصيَّر خطوط TrueType و CFF و Type 3 المضمَّنة من
// الملف نفسه — لا شيء يحتاج إلى التثبيت على هذا الجهاز
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
والنتيجة العملية هي ما يهم صندوق بريد الدعم لديك: ملف PDF يحمل خطوطه يُصيَّر بتلك الخطوط، على خادم بناء، أو في حاوية Windows، أو على سطح مكتب عميل لم ير ذلك الخط يومًا. ويُشحن خط أنابيب تصيير الأحرف الرسومية المضمَّنة كجزء من مكوّن HotPDF لـ Delphi لـ Delphi و C++Builder — مكتبة VCL أصيلة تغطي إنشاء PDF وتحريره واستخراج النص وتصيير الصفحات دون أي اعتماد على DLL خارجي