مقالة تقنية

عرض خطوط PDF المضمنة في دلفي باستخدام HotPDF

يعرض HotPDF خطوط PDF المضمنة في دلفي دون تثبيت أي شيء على الجهاز: تقوم أنبوبية عرض رموز الخطوط المضمنة في HPDFGlyphRender.pas بتحليل برامج الخطوط المخزنة داخل ملف PDF نفسه — المخططات التفصيلية TrueType glyf من FontFile2، وسلاسل أحرف CFF Type 2 من FontFile3، وتدفقات محتوى رموز الخطوط Type 3 — وتعيد تشغيلها كمسارات متجهة مملوءة لـ GDI. هذه المقالة هي تعمق في دقة الخط خلف عرض صفحات PDF إلى TBitmap باستخدام HotPDF: يغطي هذا الجزء المعالج الإجمالي للعرض، ويغطي هذا الجزء كيفية حصول النص في تلك الصفحات على أشكاله الدقيقة

تعني المربعات أو الفراغات أو الأحرف الخاطئة بذكاء في ناتج PDF المعروض دائماً تقريباً أن معالج العرض طلب خطاً من نظام التشغيل بدلاً من استخدام الخط المضمن في الملف. وتأتي الشكوى بنفس الطريقة في كل مرة: يبدو المستند ممتازاً على الجهاز الذي أنتجه، ثم يفتحه عميل على خادم نظيف أو سطح مكتب مغلق وتظهر الفاتورة اليابانية بمربعات فارغة (tofu squares)، أو يقوم خط بديل مشابه بإزاحة كل فاصل سطر. لم تكن الخطوط موجودة أبداً على هذا الجهاز — فقط داخل ملف 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-splines التربيعية مقابل سلاسل أحرف cubic charstrings التكعيبية مقابل معاملات الصفحات العشوائية)، لذا يحتاج معالج العرض الأمين إلى مفسر منفصل لكل منها، بالإضافة إلى طبقة ترميز تحول رموز الأحرف إلى فهرس رمز الخط الصحيح قبل لمس أي مخطط تفصيلي

كيف يحول HotPDF مخططات TrueType glyf التفصيلية إلى مسارات GDI؟

يقرأ الفئة THPDFEmbeddedTTF في HPDFGlyphRender.pas جدول loca لتحديد موقع كل سجل لرمز الخط، ويسير في مسارات glyf نقطة بنقطة، وينتج مسار GDI. هناك اتفاقيتان في TrueType تحتاجان إلى معالجة صريحة. أولاً، تعني النقاط المتتالية الواقعة خارج المنحنى وجود نقطة على المنحنى عند منتصف المسافة بينهما، والمسار الذي تكون جميع نقاطه خارج المنحنى يبدأ عند منتصف المسافة بين نقطتيه الأخيرة والأولى — تخطى أيًا من القاعدتين وستنمو لرموز الخطوط المستديرة وجوه مسطحة أو تنهار. ثانياً، منحنيات TrueType هي منحنيات Bézier تربيعية بينما تأخذ وظيفة GDI PolyBezierTo منحنيات تكعيبية، لذا فإن كل مقطع تربيعي يتم رفع درجته تماماً بدلاً من تسطيحه إلى مقاطع مستقيمة

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (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;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

رفع الدرجة لا غبار عليه ولا خسارة فيه: يتتبع المنحنى التكعيبي نفس المنحنى المتطابق، لذا فإن المخطط التفصيلي المعروض يطابق ما يرسمه العارض الملتزم من نفس الجدول عند أي تكبير. العمل المتبقي هو الموضع. يتم كتابة كل رمز خط بوحدات الخط (عادة ما تكون شبكة 1000 أو 2048 وحدة لكل em)، ويقوم معالج العرض بتركيب مصفوفة المقياس، ومصفوفة النص، ومصفوفة التحويل الحالية في تحويل واحد من رمز الخط إلى الجهاز قبل ملء المسار. الترتيب مهم هنا أكثر مما يظهر: ركب المصفوفات الثلاث نفسها بشكل عكسي وينهار كل رمز خط باتجاه نقطة الأصل — صفحة تبدو خاطئة وخللها الفعلي هو سطر واحد من جبر المصفوفات

كيف يتعامل مفسر سلاسل الأحرف Type 2 مع خطوط CFF

تمنح الفئة THPDFEmbeddedCFF برامج FontFile3 مفسر سلاسل أحرف Type 2 حقيقي: فهي تحلل هياكل CFF INDEX، وTop DICT وPrivate DICT، ثم تنفذ كل سلسلة أحرف وتصدر مقاطع مسار مباشرة إلى GDI. يتم تجريد غلاف OpenType (حاوية OTTO) أولاً للوصول إلى جدول CFF المجرد؛ ويتم استهلاك تدفقات CIDFontType0C وType1C المجردة مباشرة. سلاسل الأحرف هي لغة مكدس مدمجة، وتحدد ثلاث من اتفاقياتها ما إذا كان المفسر يظل متزامناً مع تدفق البايتات. يعني بادئة العرض الاختيارية أن المعامل الأول لمسح المكدس قد يحمل معاملاً رئيسياً إضافياً واحداً. يقبل معامل hintmask وجود vstemhm عندما لا تزال المعاملات على المكدس، ويعتمد عدد بايتات القناع التي سيتم تخطيها على عدد stem المتراكم — أخطئ في العد مرة واحدة وسيتم قراءة كل رمز عملية لاحق بشكل خاطئ. وتضيف استدعاءات الدوال الفرعية انحيازاً (bias) إلى فهرسها (107، أو 1131، أو 32768 اعتماداً على عدد الدوال الفرعية) قبل البحث، لذا فإن الاستدعاء غير المنحاز يهبط على الدالة الفرعية الخاطئة تماماً

يضيف CFF المفتاحي بـ CID توجيهاً غير مباشر واحداً يعطل التطبيقات الساذجة: يحدد رمز الحرف CID، ولكن فهرس سلسلة الأحرف هو GID، وتطابق مجموعة محارف الخط GID مع CID — لذا يبني معالج العرض مطابقة CID إلى GID العكسية قبل الرسم، ويحدد Private DICT لكل رمز خط من خلال FDSelect للخطوط التي تحمل عدة قواميس. وبدلاً من ذلك، تحل برامج Type1C ذات المفاتيح بالاسم، وهي الناقل المعتاد لخطوط Type 1 البسيطة، الرموز ذات البايت الواحد من خلال الترميز المدمج لبرنامج CFF أو من خلال آلية الترميز على مستوى PDF الموضحة لاحقاً. تحذير صادق واحد: يقرأ المفسر معاملات التلميح (hint) للحفاظ على تزامن التدفق ولكنه لا ينفذ التلميح، وهو حد تمت مناقشته في النهاية

ما هو خط 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 المختوم باللون الأزرق بواسطة الصفحة باللون الأسود؛ وتخطي القص ويرسم رمز خط مشوه خارج خليته

كيف تصبح رموز الأحرف معرفات لرموز الخطوط (glyph IDs)

مفسرات المخططات التفصيلية هي نصف العمل فقط، لأن البايتات في سلسلة نصية بملف PDF هي رموز أحرف، وليست فهارس لرموز الخطوط، ويخصص معيار ISO 32000-1 بندين فرعيين لهذه المطابقة. بالنسبة للخطوط البسيطة، يفرض البند §9.6.6 أولوية صارمة: مصفوفة /Differences تتجاوز الترميز الأساسي (WinAnsiEncoding، أو MacRomanEncoding، أو StandardEncoding), والذي يتجاوز بدوره الخريطة الخاصة ببرنامج الخط. يحل HotPDF هذه السلسلة في جدول مكون من 256 إدخالاً لرموز GID، مترجماً أسماء رموز الخطوط إلى فهارس رموز الخطوط بثلاث طرق: تطابق دقيق لمجموعة محارف الخط داخل برنامج CFF، والأسماء الرقمية gNN/glyphNN المأخوذة كفهارس حرفية، وترجمة اسم Adobe Glyph List إلى Unicode متبوعاً ببحث cmap لبرامج TrueType. بالنسبة للخطوط المركبة، يجمع البند §9.7 ملف CIDToGIDMap في المسؤولية: الحالة الشائعة هي /Identity، ولكن الإدخال قد يكون تدفقاً من أزواج big-endian مفهرسة بـ CID — ويستخدم ناتج Unicode الخاص بـ HotPDF نفس شكل التدفق تماماً للمجموعات الفرعية المدمجة، لذا فإن مسار التدفق ليس زاوية غريبة

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

عند الحاجة إلى بحث cmap في TrueType، يسير HotPDF في سلسلة تراجع بدلاً من الوثوق بجدول فرعي واحد: تأتي الجداول الفرعية لنظام التشغيل Windows Unicode (التنسيق 4، ثم التنسيق 12 للمستويات الإضافية) أولاً، ثم جدول الرموز الفرعي (3,0) مع اتفاقية منطقة الاستخدام الخاص F000 التي تنعكس وصولاً إلى البايت المنخفض — وهو السبب في أن خط الرموز مثل Wingdings يستجيب لرموز ASCII العادية — ثم التنسيقات القديمة 6 و 0. لا يتم تفسير الجداول الفرعية من التنسيق 2 عمداً: فهي تطابق صفحات الرموز القديمة متعددة البايتات مثل Shift-JIS و Big5، وليس Unicode، وتحمل خطوط CJK الحديثة دائماً جدولاً فرعياً من التنسيق 4 أو التنسيق 12 على أي حال. وتتراجع أي رموز لا تنجو من أي من هذه المسارات إلى رسم GDI لرمز الخط الفردي ذلك، وبالتالي فإن الحرف غير القابل للمطابقة يقلل من جودة رمز خط واحد، وليس النص بأكمله

ما لا يفعله المسار المضمن

تستحق الحدود التوضيح صراحة. لا يتم تنفيذ التلميح (hinting) — يتم ملء المخططات التفصيلية كما تمت كتابتها، وهو أمر لا يمكن تمييزه عن المخرجات الملمحة عند 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
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      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 لدلفي وC++Builder — مكتبة VCL أصلية تغطي إنشاء ملفات PDF وتحريرها واستخراج النصوص وعرض الصفحات دون تبعيات DLL خارجية