مقال تقني

تضمين خطوط CFF2 المتغيرة في PDF مع HotPDF لـ Delphi

يضمّن HotPDF خطوط OpenType المتغيرة ذات الملامح CFF2 بتحويلها، عند قيم المحاور المختارة، إلى برنامج خط CFF1 مفتاحي بـ CID تفهمه قارئات PDF مسبقًا. والنص الأصلي العادي المسجل عبر RegisterUnicodeTTF يستخدم جدولي cmap وhmtx الخاصين بالخط، ويدمّن الكاتب البرنامج المحول تحت /FontFile3 بـ /Subtype /CIDFontType0C، مبقيًا كل CID مساويًا لمعرف glyph الأصلي

مسار HotPDF لتضمين خط CFF2 متغير في Delphi كـ CFF1 مفتاحي بـ CID تحت FontFile3، مع منعطف استبدال GDI الصامت الذي يتجنبَه مسار البايتات المسجلة
يجمد HotPDF التغير عند قيم المحاور المختارة ويكتب برنامج CFF1 مفتاحيًا بـ CID حيث كل CID يساوي معرف glyph الأصلي، فلا تترجم أي طبقة بين الترقيمين

يستحق المسار البديهي الوصف، لأنه يفشل دون إخبار أحد. حلل وخزّن جداول CFF2 مؤقتًا، ثم اترك مسار النص الصافي يحل الخط عبر Windows GDI بمطابقة TrueType فقط: لا يعرف GDI ماذا يفعل بملامح CFF2 فيستبدل ArialUnicodeMS بهدوء. وأي شيء لاحق يقرأ المستبدل يحصل على ملامح خاطئة وعروض خاطئة واستخراج خاطئ لمحارف المستوى التكميلي. يفتح الـ PDF بلا مشاكل، لكنه غير منسوب ببساطة إلى الخط الذي طلبته، ولهذا يجب على مسار النص أن يعمل من البايتات المسجلة بدل بحث GDI

لماذا لا يستطيع PDF تضمين خط CFF2 مباشرة؟

لا يملك PDF خانة لبرنامج CFF2. يسرد ISO 32000-2 §9.9 أنواع برامج الخطوط المضمَّنة التي على القارئ التعامل معها: Type 1 تحت /FontFile، وTrueType تحت /FontFile2، وتحت /FontFile3 برنامج CFF صريح (Type1C أو CIDFontType0C) أو غلاف OpenType حول ملامح TrueType أو CFF. وكل نكهة CFF في تلك القائمة تعني CFF الإصدار 1، الصيغة التي حللتها القارئات منذ عقدين

خانات الخطوط المضمَّنة الثلاث في PDF، كلها محدودة بـ CFF الإصدار 1، والخانة CFF2 المفقودة التي تجبر HotPDF على التحويل قبل التضمين في Delphi
كل نكهة CFF يجب أن تعالجها قارئات PDF هي CFF الإصدار 1، فخط CFF2 المتغير بلا خانة حتى يتحول

‏CFF2 صيغة مختلفة يحدث أنها تتشارك الاسم. يُسقط INDEX الخاص بالأسماء والسلاسل، ويسقط charset، ويخرج تقدمات (advances) الـ glyphs من CharStrings ويتركها لـ hmtx، ويضيف معاملات blend بحيث يصف outline واحد الفضاء التصميمي كله. محلل CFF1 يصادف جدول CFF2 لا أساس له للتخمين، ولا شيء في مواصفة PDF يلزمه بالمحاولة. وتغليف خط CFF2 داخل stream ‏OpenType وتأمُل الخير هو بالضبط نوع الملفات التي تصير على آلة وتفشل على التالية، لذا لا يفعل HotPDF ذلك

كما يجب أن يكون التحويل ثابتًا. كما تشرح مقالة التجسيد الثابت للخطوط المتغيرة، خط PDF مجموعة ملامح ثابتة بمقاييس ثابتة، ولا يلزم أي قارئ بتطبيق إحداثيات المحاور. لذا يحل HotPDF التغير أولًا ثم يكتب CFF1 بعدها فقط

كتابة نص أصلي في خط CFF2

من وجهة كود التطبيق، يتصرف خط CFF2 كأي خط Unicode مسجل آخر: اختر قيم المحاور، وسجل الملف، واختر العائلة بالاسم، وارسم. يحفظ RegisterUnicodeTTF نسخة منفصلة من جدول الأسماء الأصلي، فيطابق SetFont اسم العائلة أو الاسم الكامل أو اسم PostScript أو اسم عائلة النمط (typographic family name) ويختار بايتات الخط الثابت المجمدة، دون تثبيت Windows أو بحث GDI

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure.pdf';
    Pdf.BeginDoc;
    // قيم المحاور تنطبق على الخطوط المسجلة بعد هذا الاستدعاء
    Pdf.SetVariableFontAxis('wght', 650);
    Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
    // اسم العائلة أو الكامل أو PostScript أو عائلة النمط من جدول الأسماء
    Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
    Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

معرفات glyphs تأتي من cmap المسجل والتقدمات من hmtx المسجل. الـ glyph الذي تقدمه صفر حقًا يبقى صفرًا بدل العودة إلى قياس GDI، وزوج surrogate ‏UTF-16 ينتج تقدمًا واحدًا بالضبط لا اثنين. محارف BMP والمحارف التكميلية وحاملات التشكيل الاصطناعية تحفظ خرائط Unicode لكل خط، فالتبديل إلى خط مضمَّن آخر أو بدء صفحة جديدة لا يسمح لذلك الخط الآخر بتحديث حالة subset هذا الخط أو استعارة معرفات glyphs منه

القياس يتبع البايتات نفسها. تقرأ THPDFPage.GetWideTextWidth وTHPDFPage.GetWideCharAdvances مقاييس الخط المدعوم بالبايتات المختار، فيوافق كسر السطر والمحاذاة اليمنى ما يرسم. وتفصيلة ترتيب تمسك الناس: تغييرات SetVariableFontAxis بعد التسجيل تؤثر في الخط التالي الذي تسجله، لا في النسخة المجمدة أصلًا. وللحصول على وزن ثانٍ، اضبط القيمة الجديدة وسجل من جديد

كيف يبني HPDFConvertCFF2ToCFF البرنامج المفتاحي بـ CID؟

ينتج HPDFConvertCFF2ToCFF برنامج CFF1 مفتاحي بـ CID خام بـ Font DICT واحد، يكون فيه CID مساويًا لمعرف glyph الأصلي لكل خانة، بما فيها الخانات الفارغة والمهملة. تلك المطابقة هي التصميم كله: اسم الخط، وعروض /W في PDF، وتقدمات CFF، وToUnicode CMap، كلها تشير إلى الترقيم نفسه، فلا تحتاج أي طبقة إلى الترجمة بين معرفات glyphs والـ CIDs، ولا يمكن لأي شيء أن ينحرف عن الخطو

على جانب PDF، الابن خط CIDFontType0 بترميز Identity-H أو Identity-V، وstream البرنامج يجلس تحت /FontFile3 بـ /Subtype /CIDFontType0C. ولأن CID يساوي GID أصلًا، لا يحمل الابن /CIDToGIDMap، ويستثنى الـ stream من خطاف استبدال streams اللاحق المستخدم للخطوط TrueType المسجلة. وعلى خلاف غلاف /Subtype /OpenType، لا يرفع الـ stream المحول اشتراط إصدار PDF 1.6

احتاجت العروض حيلة صغيرة. يخزن CharString من Type 2 في CFF1 التقدم كمعامل أول اختياري نسبة إلى nominalWidthX، والمعاملات الصحيحة محدودة بمدى 16 بت مإشارة. وتقدم OpenType قيمة 16 بت بلا إشارة تصل إلى 65535. يضبط HotPDF ‏nominalWidthX عند 32768 وdefaultWidthX عند 0، ثم يكتب كل تقدم صراحة كـ advance - 32768. فالتقدم الصفري يصير -32768 وتقدم 65535 يصير 32767، فيصير مدى uint16 كله قابلًا للتمثيل دون قصر (clamping) أي glyph واحد

كيف يقابل HotPDF تقدمات 16 بت غير الإشارة إلى معاملات CFF1 بإشارة عبر nominalWidthX بـ 32768، مبقيًا كل تقدم خط في خرج PDF على Delphi دون قصر
‏nominalWidthX بـ 32768 يزيح نافذة المعاملات فيخزن التقدم الصفري -32768 ويخزن 65535 كـ 32767 دون قصر أي glyph

الهندسة تحصل على العناية نفسها: ‏FontMatrix هو [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0]، فيحفظ الخط ذو 2048 وحدة إحداثياته الأصلية. والتحريكات والخطوط النسبية الأكبر من معامل واحد تقسم إلى خطوات قابلة للتمثيل، والمنحنيات التكعيبية تجزأ بطريقة تحفظ شكلها. وتسافر سجلات النشر والترخيص الأصلية وجدول الأسماء الأصلي وأعلام التضمين في سلسلة Notice في CFF

استدعاء المحول خارج مستند

المحول تابع عام في وحدة HPDFOpenTypeAdvanced، مكتوب بـ Pascal، بلا اعتماد على HarfBuzz وبلا خطوة تثبيت خطوط نظام. وهذا يجعله صالحًا للاستخدام بمفرده، مثلًا لتحويل خط مسبقًا عند وقت البناء أو لتفقّد ما سيضمّنه كاتب PDF

uses
  System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;

procedure ConvertWeight650;
var
  Source, Cff: TBytes;
  Axes: THPDFVariableFontAxisValueArray;
begin
  Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
  SetLength(Axes, 1);
  Axes[0].Tag := 'wght';
  Axes[0].Value := 650;
  Cff := HPDFConvertCFF2ToCFF(Source, Axes);
  if Length(Cff) = 0 then
    raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
  TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;

يدخل الدخل المتغير عبر تجسيد ثابت هندسي فقط عند الإحداثيات المعطاة، شاملًا الملامح والتقدمات معًا. والخط CFF2 الثابت لا يقبل إلا اختيارًا فارغًا؛ وتمرير قيم محاور إليه يعامل خطأ لا يُهمل. والناتج برنامج CFF1 صريح لا حاوية sfnt، فلا تسلمه كودًا يتوقع ملف OpenType

الفشل يكون TBytes فارغًا لا استثناء، في الحالات المتوقعة: دخل ليس CFF2، واختيار محاور لا يقبله المُجسِّد، وقيم غير منتهية مثل NaN، وخطوط يضبط فيها OS/2 ‏fsType تضمين ترخيص مقيد (0x0002) أو تضمين نقطي فقط (0x0200). اختبر الطول دائمًا. وداخل EndDoc يفعل الكاتب ذلك عنك ويرفع "StoreFont: CFF2 outline conversion failed"، وهو القرار الصائب: برنامج خط فارغ في PDF يشحن أسوأ بكثير من خطأ مرئي في سجل log الخاص بك

ميزانيات وإلغاء لأحمال عمل الخوادم

المعامل الثالث الاختياري THPDFVariableFontControl يحصر الذاكرة ويتيح لك إلغاء تحويل طويل. دونه يستخدم المحول افتراضاته: حد 128 MiB على دخل وخرج الخط، وميزانية عمل تراكمية 1 GiB، وحد CFF1 البالغ 65535 بايتة لكل CharString

type
  TFontJob = class
  private
    FCancelled: Boolean;
    procedure CheckCancel;
  public
    function Convert(const Source: TBytes): TBytes;
    property Cancelled: Boolean read FCancelled write FCancelled;
  end;

procedure TFontJob.CheckCancel;
begin
  if FCancelled then
    Abort;  // يخرج من HPDFConvertCFF2ToCFF
end;

function TFontJob.Convert(const Source: TBytes): TBytes;
var
  Control: THPDFVariableFontControl;
begin
  Control := THPDFVariableFontControl.Create(
    32 * 1024 * 1024,           // أقصى بايتات خرج
    Int64(256) * 1024 * 1024,   // الميزانية التراكمية للعمل
    CheckCancel);
  try
    // ‏nil يعني النسخة الافتراضية لخط متغير، أو خط CFF2 ثابت
    Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
  finally
    Control.Free;
  end;
end;

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

ما وزن خط CFF2 المضمَّن؟

أهم ما تخطط له هو حجم الملف: يضمّن HotPDF مجموعة glyphs المحولة كاملة للخطوط CFF2، سواء كان EnableFontSubsetting مفعَّلًا أو لا. الخط ببضعة آلاف من glyphs يضيفها كلها إلى كل مستند يستخدمه، حتى لو عرضت الصفحة عنوانًا واحدًا. وفي كتيّب فذلك مقبول، أما في خادم تقارير يكتب آلاف ملفات PDF الصغيرة فهي تكلفة حقيقية ينبغي أن تقيسها قبل الالتزام

مزايا الحجم ذات الصلة تتبع القاعدة نفسها: الخطوط CFF2 المحولة تضمَّن كاملة، فلا تنطبق عليها مجموعات الخطوط الفرعية المدمجة التي يفعّلها CompressDocument ولا ذاكرة subsets الخطوط الدائمة. وتظل خطوط TrueType في المستند نفسه تجزأ وتخزَّن كالعادة. والمردود الوحيد لحفظ كل glyph هو أن الخطوط التي يمنع ترخيصها التجزئة تضمَّن بصيغة تحترم ذلك

حدود الموارد حدود تحويل منتهية، وCFF1 نفسه يسقف ما يحمله CharString واحد. الـ glyph الذي يتجاوز CharString المحول عنده 65535 بايتة، أو الخط الذي يكتسح ميزانية العمل، يفشل بشكل مرئي بدل أن يكتب بملامح فارغة. تلك هي المقايضة المقصودة: فشل صاخب على خط غريب خير من مستند بـ glyphs مفقودة بصمت

النص الأصلي CFF2 والمحول CFF1 القابل للحمل يأتيان مع مكوّن HotPDF لـ PDF في Delphi لـ Delphi وC++Builder، مع مزايا تجسيد الخطوط المتغيرة وglyphs الملونة وتجزئة الخطوط المغطاة في مواضع أخرى من هذه المدونة