مقال تقني

الخطوط المتغيّرة في PDF: التنسيق الثابت في Delphi

لا يملك PDF مفهوم الخط المتغيّر. فالخط المضمَّن في ملف PDF مجموعة ثابتة من المخططات بقياسات ثابتة، لذا يجب اختزال أي خط متغيّر إلى نسخة ثابتة واحدة قبل أن يدخل مستندًا. ويؤدي HotPDF عملية التنسيق هذه داخليًا: تفحص محاور الخط المتغيّر، وتختار إحداثيات مثل وزن 620 أو عرض 87.5، وتقوم المكتبة بترسيخ تلك القيم في برنامج خط كامل ومستقل بذاته يستطيع أي قارئ PDF متوافق مع المعيار عرضه

وسبب أهمية هذا الأمر عملي لا نظري. فمصانع الخطوط تشحن على نحو متزايد ملفًا متغيّرًا واحدًا بدلًا من دزينة أوزان ثابتة، وفرق التصميم تختار قيمًا لا توفّرها أي نسخة مسمّاة. ومن دون تنسيق، فإن مولّد التقارير إما يعود إلى النسخة الافتراضية، ما يُلغي قرار التصميم، وإما يُضمِّن الخط المتغيّر بأكمله ويأمل أن يحترم العارض إحداثيات محاور لا سبيل له لمعرفتها، وهو أمر لا يُلزَم أي قارئ بفعله

ما الذي يجب على التنسيق إعادة بنائه فعليًا؟

يخزّن خط OpenType المتغيّر مخططًا افتراضيًا واحدًا لكل حرف رسومي إضافةً إلى مجموعة من الفروق مُفهرَسة حسب الموضع في مساحة التصميم. وتطبيق إحداثية محور ليس مسألة كتابة رقم في ترويسة؛ بل يعني اجتياز جدول gvar، واستيفاء الفروق للموضع المطلوب، وتحريك النقاط، ثم إعادة حساب كل ما اشتُقّ من تلك النقاط. ويُعيد HotPDF بناء مخططات الأحرف الرسومية، وجدول loca الطويل، والقياسات الأفقية والرأسية الكاملة، والمربع المحيط العام للخط، وتصحيح مجموع التحقق لـ sfnt

ولا يقل أهمية عن ذلك ما يجب إزالته. فلا يجوز أن تحتفظ النسخة الثابتة بـ fvar أو avar أو gvar أو HVAR أو VVAR أو MVAR أو STAT أو cvar، ويجب إزالة DSIG القديم أيضًا، لأن البايتات الموقَّعة لم تعد موجودة. وترك أي من هذه الجداول ينتج خطًا يزعم أنه متغيّر بينما يحمل مخططات جرى تحريكها بالفعل، وحينها ستُطبّق القارئات التي تُطبّق التغيّرات فعليًا تلك التغيّرات مرة ثانية

النقاط الوهمية وفخ التطبيق المزدوج

أدقّ قاعدة في العملية بأكملها تتعلق بالقياسات. ففي gvar، يشمل عدد نقاط الحرف الرسومي نقاط المخطط، أو نقاط المكوّنات في حالة الحرف الرسومي المركّب، إضافةً إلى أربع نقاط وهمية تُرمّز الهامش الجانبي الأيسر وعرض التقدّم ومكافئاتهما الرأسية. وهذه النقاط الوهمية نفسها خاضعة للفروق

لذا حين يحتوي الخط على جدول gvar، يشتق HotPDF القياسات الأفقية والرأسية من النقاط الوهمية المستوفاة، ولا يُطبّق HVAR أو VVAR إضافةً إلى ذلك. وإضافة الاثنين معًا هو الخطأ الكلاسيكي: يُطبَّق التغيّر نفسه مرتين، فيخرج كل عرض تقدّم أوسع قليلًا مما ينبغي، وهو ما يظهر كنص ينحرف تدريجيًا نحو اليمين عبر سطر مُبرَّر. ولا تُرسِّخ المكتبة مخزن تغيّر القياسات مباشرة في hmtx أو vmtx إلا حين لا يحتوي الخط على gvar

وثمة تفصيلان إضافيان يحافظان على دقّة الهندسة. فالنقاط الوهمية لا تشارك أبدًا في استيفاء المحيط، لذا تُستنتَج النقاط غير المدرَجة صراحةً لحرف رسومي بسيط عبر IUP لكل محيط، مع استبعاد النقاط الوهمية. أما الأحرف الرسومية المركّبة فتُطبَّق فروقها على إزاحات المكوّنات التي تستخدم معاملات XY، وبعدها تُعاد حسابات حدود العناصر الفرعية تكراريًا. وهذا التكرار محدود العمق ومُتحقَّق من الدورات، لأن رسمًا بيانيًا للمكوّنات معادٍ أو معطوبًا فحسب يمكن أن يتكرر من دون نهاية لولا ذلك

فحص مساحة التصميم قبل الاختيار

أول استدعاء في أي سير عمل للتنسيق هو InspectVariableFont، الذي يُبلغ عن المحاور والنسخ المسمّاة التي حدّدتها مصنعة الخط. وتحمل سجلات المحاور الوسم المكوّن من أربعة بايتات، والقيم الدنيا والافتراضية والقصوى، والأعلام، ومعرّف اسم؛ بينما تحمل النسخ المسمّاة معرّف اسم عائلة فرعية، وأعلامًا، ومعرّف اسم PostScript اختياريًا، وإحداثية واحدة لكل محور:

var
  Pdf: THotPDF;
  Axes: THPDFVariableFontAxisArray;
  Instances: THPDFVariableFontNamedInstanceArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
    begin
      for I := 0 to High(Axes) do
        Writeln(Format('%s  min=%.1f default=%.1f max=%.1f',
          [string(Axes[I].Tag), Axes[I].MinimumValue,
           Axes[I].DefaultValue, Axes[I].MaximumValue]));
      Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
    end
    else
      Writeln('not a variable font - embed it as an ordinary TrueType face');
  finally
    Pdf.Free;
  end;
end;

ويهم الإبلاغ عن نطاق المحور لأن قيم المحاور تُقيَّد بالنطاق الذي يُعلنه الخط، لا بالنطاق الذي توفّره واجهتك. فمنزلق يسمح للمستخدم بطلب وزن 1000 على خط يتوقف محوره wght عند 900 ينبغي تصحيحه في الواجهة، لا بصمت في طبقة الخط، وإلا فسيختلف الناتج المطبوع عن المعاينة

اختيار الإحداثيات وإصدار المستند

اختيار المحور ذو حالة وينطبق على الخطوط المسجَّلة لاحقًا. تأخذ SetVariableFontAxis وسمًا من أربعة بايتات بترميز ASCII قابل للطباعة وقيمة منتهية، وترفض أي شيء آخر برفع استثناء بدلًا من تجاهله بصمت. وتُعيد ClearVariableFontAxes ضبط الاختيار، بينما تُبلغ GetVariableFontAxisSelections عمّا هو معلَّق حاليًا، وهو أمر يستحق تسجيله في محركات التقارير حيث قد تكون عدة مسارات كود قد لامست كائن المستند نفسه. وتُختار العائلة نفسها بالاسم عبر SetFont، تمامًا كأي وجه TrueType مضمَّن آخر:

begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;

    Pdf.SetVariableFontAxis('wght', 620);   // شبه عريض، وليس نسخة مسمّاة
    Pdf.SetVariableFontAxis('wdth', 87.5);  // مُضيَّق قليلًا
    Pdf.CurrentPage.SetFont('Inter', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');

    Pdf.ClearVariableFontAxes;              // عودة إلى النسخة الافتراضية
    Pdf.CurrentPage.SetFont('Inter', [], 10);
    Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');

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

يُطلَق حدث OnVariableFontInstance عند إنتاج كل نسخة، ويُبلغ عن قيم المحاور المستخدمة، وهي أرخص طريقة لإثبات ما يحتويه ملف PDF معيّن فعليًا في سجل. ولأن كل مجموعة إحداثيات مختلفة تنتج برنامج خط مختلفًا، فعامِل اختيارات المحاور كجزء من مفتاح ذاكرة التخزين المؤقت للخط لديك؛ وآليات التخزين المؤقت موصوفة في ذاكرة التخزين المؤقت الدائمة لمجموعات الخط الفرعية

كيف يتفاعل التنسيق مع التجزئة والتشكيل

يُنفَّذ التنسيق قبل التجزئة، وهذا هو الترتيب الصحيح. فالخط المُنسَّق وجه TrueType ثابت عادي، لذا تتعامل معه أداة التجزئة العادية كأي وجه آخر: تحسب إغلاق الأحرف الرسومية، وتُبقي على الأحرف الرسومية التي يستخدمها المستند فعليًا، وتُسقط الباقي. والتفاعل الذي يجدر الانتباه إليه هو أن اختيارَي محور مختلفين للعائلة نفسها هما برنامجا خط مختلفان، لذا فإن مستندًا يمزج بين وزن 400 ووزن 620 يُضمِّن مجموعتين فرعيتين، لا وجهًا واحدًا مشتركًا بنسختين

ولا يتأثر التشكيل من حيث المبدأ، لكن يستحق التحقق منه عمليًا. فميزات التخطيط تقيم في GSUB وGPOS، وهما ما يحافظ عليه التنسيق، لذا تستمر الأربطة والبدائل الأسلوبية في العمل كما هو موصوف في بدائل OpenType GSUB الأسلوبية. أما ما يتغيّر فهو التموضع: فالنسخة المُضيَّقة لها تقدّمات أضيق من الافتراضية، لذا فإن أي تخطيط قاس النص قبل التنسيق يكون قد قاس عروضًا خاطئة. قِس باستخدام اختيار المحور نفسه الذي سترسم به، ويختفي التباين

وملاحظة دفاعية أخيرة من التنفيذ، مفيدة لمن يُوسّع هذا المسار. فالخطوط الخالية من القياسات الرأسية لا تزال تُقيّم معاملات المصفوفة الديناميكية عند موضع الاستدعاء في Delphi، لذا تُخصَّص مصفوفات وقت التحليل دائمًا بدلًا من الاعتماد على فحص HasVerticalMetrics لتفادي فهرس فارغ. وهذا هو نوع التفصيل على مستوى اللغة الذي يحوّل فرعًا يبدو محميًا إلى انتهاك وصول على الخطوط بالتحديد التي لم تختبرها

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