مقال تقني

عرض محارف Composite وحركات CFF seac في PDF

يعرض HotPDF الحروف المُحرَّكة من خطوط مضمّنة بشكل صحيح عبر معالجة آليتي خطين تبنيان محرفاً واحداً من محارف أخرى: محارف TrueType المركبة (composite glyphs)، التي يجب تطبيق إزاحات مكوّناتها بوحدات المحرف قبل مصفوفة التحويل من المحرف إلى الجهاز، وصيغة seac في endchar بـ CFF، التي تصل حرف أساس وحركة معاً عبر كود StandardEncoding. أخئئ أحداهما وسيخرج é أو Å أو ü أو علامة اقتباس منحنية مفقود جزءاً، بينما تبدو الحروف العادية حوله مثالية

تلك التفصيلة الأخيرة هي ما يجعل هذه الخلل مكلفة. يُعرض تقرير، النص الأساسي سليم، و"München" وحدها فيها فراغ، أو اقتباس يفتح بعلامة واحدة وفجوة. لا أحد يشتبه في العارض. النظرية الأولى دائماً خط تالف أو مولّد سيئ، وتبقى التذكرة تتنقل أسبوعاً. تستعرض هذه المقالة ما يحدث فعلاً في كل آلية، وكيف يعالجها مكون HotPDF لـ Delphi منذ v2.768.10 و v2.768.11، وإصلاح الاقتطاع المرتبط في v2.770.83 الذي يبقي المحارف المركبة سليمة عندما يكتب HotPDF الخطوط بدلاً من أن يقرأها

لماذا تختفي الحركة وحدها من محرف في PDF؟

تختفي الحركة لأنها الجزء الوحيد من المحرف الذي يحمل إزاحة غير صفرية. يوفّر كل من TrueType و CFF المساحة بإعادة استخدام المخططات: é لا يُرسم شكلاً جديداً بل بوصفه "محرف e زائد محرف acute منزاحاً لأعلى ولليمين". حرف الأساس يجلس دائماً تقريباً عند الإزاحة (0, 0)، فأي خطأ في طريقة تطبيق الإزاحات يتركه دون مساس. الحركة هي المكوّن الذي يتحرك، فهي المكوّن الذي يهبط في المكان الخطأ أو لا يُرسم إطلاقاً

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

كيف تضع محارف TrueType المركبة مكوّناتها؟

يضع المحرف المركب في TrueType كل مكوّن بتحويله الأفيني الصغير الخاص، معبَّراً بوحدات الخط، وعلى ذلك التحويل أن يعمل على مخطط المكوّن قبل أن تفعلها مصفوفة التحويل من المحرف إلى الجهاز. في جدول glyf من OpenType، يملك المحرف المركب عدد مسارات سالباً، يليه سجل من سجلات المكوّنات. يحمل كل سجل كلمة أعلام، ومؤشر محرف المكوّن، ومعاملين إما بايتَين أو كلمتَين (ARG_1_AND_2_ARE_WORDS، 0x0001)، ومقياساً اختيارياً: قيمة F2Dot14 واحدة لـ WE_HAVE_A_SCALE (0x0008)، وقيمتَين لـ WE_HAVE_AN_X_AND_Y_SCALE (0x0040)، أو مصفوفة 2x2 كاملة لـ WE_HAVE_A_TWO_BY_TWO (0x0080). عندما تُضبط ARGS_ARE_XY_VALUES (0x0002)، يكون المعاملان إزاحة dx/dy بوحدات الخط. وتقول MORE_COMPONENTS (0x0020) إن سجلاً آخر يتبع

إذن لكل مكوّن تبني مصفوفة مكوّن CM من جزء المقياس والإزاحة، ثم تدمجها مع M، المصفوفة التي تحول وحدات الخط إلى بكسلات جهاز. ترتيب ذلك الدمج هو الخلل كله. ضرب المصفوفات الداخلي في HotPDF يستخدم اتفاقية متجه العمود، فحاصل ضرب A وB يطبّق B أولاً ثم A. التركيب الصحيح يطبّق CM أولاً، في فضاء المحرف، وM ثانياً. مكتوباً بالمقلوب، يعمل M أولاً وتُضاف إزاحة CM بعد ذلك، وهي حينها ببكسلات جهاز ولم تُقسَّم أبداً

// توضيح فقط: سجل أفيني ثنائي الأبعاد باتفاقية متجه العمود،
// حيث يمرر Mul(A, B) نقطة عبر B أولاً ثم عبر A
type
  TAffine = record
    a, b, c, d, e, f: Double;
  end;

function Mul(const A, B: TAffine): TAffine;
begin
  Result.a := A.a * B.a + A.c * B.b;
  Result.b := A.b * B.a + A.d * B.b;
  Result.c := A.a * B.c + A.c * B.d;
  Result.d := A.b * B.c + A.d * B.d;
  Result.e := A.a * B.e + A.c * B.f + A.e;
  Result.f := A.b * B.e + A.d * B.f + A.f;
end;

// M  : وحدات الخط -> بكسلات الجهاز (مقياس ضئيل، y معكوس)
// CM : تحويل المكوّن؛ e/f تحمل dx/dy بوحدات الخط
Sub := Mul(M, CM);   // صحيح: الإزاحة تتقلص مع المخطط
// Sub := Mul(CM, M); // خطأ: dx/dy تهبط بكسلات جهاز خام

الأرقام تجعل الفشل بديهياً بمجرد أن تراها. يستخدم Arial وحدة em بحجم 2048، ومكوّن الحركة في é يحمل إزاحته بتلك وحدات الخط. بالترتيب الصحيح تتقلص تلك الإزاحة مع المخطط إلى بضع بكسلات عند التكبير العادي. وبالترتيب المعكوس تُضاف قيمة وحدات الخط الخام بوصفها بكسلات جهاز، وفي عرض اختبار HotPDF انتهى الأمر بمحرك é في Arial على بعد 243 بكسلاً من حرفه، بعيداً جداً عن المحرف وغالباً خارج أي شيء مرئي. وعلامة الاقتباس اليسرى المزدوجة في Arial نسختان من شكل علامة الاقتباس المفردة، الثانية منهما منزاحة، فعرضت بعلامة ‘ واحدة تليها مساحة فارغة

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

الاستدعاء الذاتي والدورات

قد تشير المحارف المركبة إلى محارف مركبة أخرى، فيسير فيها العارض استدعاءً ذاتياً ولا بد أن يدافع عن نفسه. يحدد HotPDF عمق التداخل بـ 5 مستويات وعدد المكوّنات الكلي لكل محرف بـ 1024، ويتخطى مكوّناً يسمّي مؤشر محرفه الخاص، ويعامل وضع مطابقة النقاط نادر الاستخدام (معاملات هي أرقام نقاط لا إزاحات) بوصفه إزاحة صفرية. يستطيع خط معادٍ أو تالف أن يفقد مكوّناً إذن، لكنه لا يستطيع أن يعلّق عرض الصفحة

ما هي صيغة seac في endchar بـ CFF؟

صيغة seac في endchar طريقة أُهملت ليقول charstring من CFF Type 2 "هذا المحرف حرف أساس زائد حركة": عندما يكون لـ endchar أربعة معاملات، فهي adx ady bchar achar. تصفها مذكرة Adobe التقنية رقم 5177 في ملحقها حول التوافق مع Type 1. الاسم من معامل seac في Type 1 (standard encoding accented character)، ولا تزال المجموعات الفرعية لـ CFF المحوَّلة من خطوط Type 1 تستخدمه بكثافة للحروف اللاتينية المُحرَّكة

ثلاث تفاصيل تعثر المنفذين. أولاً، bchar و achar ليسا معرّفي محارف. إنهما أكواد في Adobe StandardEncoding، فيحوّل العارض كل كود إلى اسم محرف ثم يبحث عن ذلك الاسم عبر charset الخاص بالخط ليجد الـ GID. ثانياً، لا يمتلك Type 2 معامل asb، خلافاً لـ seac في Type 1 الذي يأخذ asb adx ady bchar achar. في Type 2 يذهب أصل الحركة مباشرة إلى (adx, ady) نسبةً إلى أصل محرف الأساس، دون تصحيح side-bearing. ثالثاً، خطوط CFF من نوع CID-keyed لا يجوز أن تستخدم seac إطلاقاً، لأنه لا توجد مجموعة أسماء محارف يُحل عبرها أكواد StandardEncoding

ثم هناك العرض. كل charstring من Type 2 قد يبدأ أول معامل تصفير للمكدس بمعامل عرض تقدم اختياري، و endchar أحد تلك المعاملات. الـ endchar العادي بمعامل واحد يحمل عرضاً؛ والـ endchar بنوع seac بخمسة معاملات يحمل عرضاً يليه قيم الـ seac الأربع. القاعدة أن تُحصى المعاملات مقابل العدد الثابت المتوقع، وهو أربعة للـ seac. قبل v2.768.11 كان HotPDF يعامل أي endchar بوصفه "خذ العرض وتوقف"، فأنتجت محارف seac مخططاً فارغاً: بلا أساس، وبلا حركة، مجرد فجوة بحجم صحيح في السطر

// توضيح قرار endchar في مفسر Type 2.
// المكدس يحمل المعاملات السابقة لـ endchar؛ Count هو عددها.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 معاملات: الأول هو عرض التقدم، والبقية seac
  if Count = 5 then
    TakeWidthAndShift;                 // أسقط العرض واحتفظ بـ 4 معاملات
  if StackDepth = 4 then
  begin
    BaseGID   := GIDForName(StdEncName(Round(Stack[2])));  // bchar
    AccentGID := GIDForName(StdEncName(Round(Stack[3])));  // achar
    DrawGlyph(BaseGID, Identity);
    DrawGlyph(AccentGID, Translate(Stack[0], Stack[1]));   // adx, ady
    Exit;
  end;
end;
// وإلا: endchar عادي، عرض اختياري، أغلق المسار

في المفسر الحقيقي يُنفَّذ محرفا المكوّنَين كلاهما بمحرك charstring نفسه، ومصفوفة الحركة هي مصفوفة المحرف الحالية مركَّبة مع ترجمة إلى (adx, ady)، وتُستعاد المصفوفة بعدها فلا تتأثر المحارف اللاحقة في السطر. وإذا لم يؤول أي من الاسمين إلى GID، يُتخطى ذلك المكوّن بدلاً من أن يفشل المحرف كله، وهو السلوك المتسامح الذي تريده عندما تكون مجموعة فرعية اقتُطعت بعنف زائد قليلاً

فحص الإصلاح من كود Delphi الخاص بك

أسرع طريقة للتحقق من معالجة المحارف المركبة و seac هي عرض صفحة بنص مُحرَّك من خطوط مضمّنة عبر مسارَي الإخراج العامين معاً: THotPDF.RenderLoadedPageToBitmap للإخراج النقطي و THotPDF.BuildLoadedPageSVG مع ضبط EmbedFonts على True للإخراج المتجهي. كلاهما يتشارك كود مخطط المحارف نفسه، فتلقى كلاهما إصلاحي v2.768.10 و v2.768.11. يغطي خط الأنابيب ككل في كيفية عرض HotPDF لمحارف TrueType و CFF و Type 3 المضمّنة، وخيارات الصورة النقطية في عرض صفحات PDF إلى صور نقطية في Delphi

uses
  System.Classes, Vcl.Graphics, HPDFDoc;

procedure RenderAccentProbe(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  Svg: AnsiString;
  Lines: TStringList;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;

    // مسار النقطة: الـ 144 DPI يجعل الحركات المنزاحة سهلة الرصد
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // المسار المتجهي: EmbedFonts = True يكتب مخططات المحارف، فترى
    // تجميع الـ composite و seac في مسارات SVG نفسها
    Svg := Pdf.BuildLoadedPageSVG(0, True);
    if Svg <> '' then
    begin
      Lines := TStringList.Create;
      try
        Lines.Text := string(Svg);
        Lines.SaveToFile('accent-probe.svg');
      finally
        Lines.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

اختر مستندات الاختبار بوعي. تريد ملف PDF واحد بمجموعة فرعية TrueType مضمّنة تحوي é و Å وعلامات اقتباس منحنية، وملفاً بمجموعة فرعية CFF (FontFile3، من نوع Type1C) منتجاً من خط Type 1، وهو موضع عيش seac ما يزال، عادةً في مخرجات تنضيد أقدم وبعض سير عمل الطباعة. النص المضبوط بخطوط لم يضمّنها المولّد لا يخبرك شيئاً هنا، لأن تلك المحارف تأتي من خطوط Windows المثبتة لا من برنامج الخط المضمّن الموصوف في ISO 32000-1 §9.9

لماذا انكسرت المحارف المركبة أثناء اقتطاع الخطوط؟

انكسرت المحارف المركبة أثناء الاقتطاع لأن المقتطع كان يمشي في سجلات المكوّنات بخطوة خاطئة كلما حمل مكوّن مقياساً أو تحويل 2x2، ثم أساء قراءة مؤشر محرف المكوّن التالي. أصلح HotPDF ذلك في v2.770.83. الاقتطاع هو التوأم الكتابي لخلل العرض: عندما يضمّن HotPDF مجموعة فرعية من خط TrueType مثبت، عليه أن يحفظ كل محرف يشير إليه مركب وأن يعيد ترقيم تلك الإشارات إلى معرّفات المحارف الجديدة المضغوطة

كان الماشي القديم ينظر فقط إلى علم حجم المعاملات وعلم المزيد-من-المكونات، فيتقدم 6 أو 8 بايتات لكل سجل. وتجاهل الـ 2 أو 4 أو 8 بايتات من بيانات التحويل التي تضيفها أعلام المقياس. بعد مكوّن بمقياس، كان "مؤشر المحرف" التالي الذي يقرأه بايتات تحويل فعلياً، فكان يمكن أن يُترك المكوّن الحقيقي خارج المجموعة الفرعية، وكان خطوة إعادة الترقيم يمكن أن تكتب معرّف محرف جديداً مباشرةً داخل التحويل. كانت النتيجة حرفاً مُحرَّكاً يعرض في بعض العارضات بحركة خاطئة، أو حركة مشوهة، أو بلا حركة إطلاقاً، وفقط للخطوط التي تضبط مقياس مكوّناتها، ولهذا لم يمسكه معظم الاختبارات قط

مشكلتان إضافيتان جلستا بجوار خلل الخطوة. إعادة ترقيم مؤشر محرف ليست عملية مُدمِجة (idempotent)، وكانت المركبات المتداخلة تُعاد ترقيمها مرتين: مرة بمرور على كل مركب معلَّم ومرة بنزول استدعائي داخلها. يعيد HotPDF الآن ترقيم كل مركب مرة واحدة بالضبط ويقود خطوة "أي المحارف يحتاج هذا المركب" من طابور عمل، فتستقر دورة من مركبات تشير بعضها إلى بعض، وتُرفض مؤشرات المحارف التي تتجاوز عدد محارف maxp في الخط، ولا تُحلَّل المحارف الفارغة أبداً بوصفها مركبة

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'accents-subset.pdf';
    Pdf.FontEmbedding := True;          // ضمّن المحارف المستخدمة
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    // é و Å و ü وعلامات الاقتباس المنحنية كلها محارف composite في Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

مرّر ذلك الملف ذهاباً وإياباً عبر المسبار أعلاه وعبر عارض ثانٍ. منذ v2.770.83 ينجو كل مكوّن في كل محرف مركب من الاقتطاع، بمن فيهم المكوّنات خلف مقياس أو تحويل 2x2، فتعرض الحركات الشيء نفسه في HotPDF و Acrobat وعارضات المتصفحات

ما لا تغطيه معالجة المحارف المركبة

يغطي إصلاح المحارف المركبة الوضع القائم على الإزاحات، وهو ما يستخدمه كل خط حقيقي تقريباً، ويقرّب البقية تحفظياً. مركبات TrueType المطابقة للنقاط، حيث يسمّي المعاملان نقطتَين للمحاذاة بدلاً من إعطاء إزاحة، تُرسم بإزاحة صفرية. ويُتجاهل seac في خطوط CFF من نوع CID-keyed عمداً لأن المواصفة تحظره هناك. والـ seac الذي لا يمتلك كود أساسه أو حركته محرفاً في مجموعة فرعية مقتطعة يرسم أي مكوّن يجده. لا تنهار العارض في أي من تلك الحالات، لكن إذا أنتج ملفات PDF للأرشفة أو للاستخدام مع PDF/A، فالخطوة الأأمن هي تضمين خطوط تستخدم مركبات إزاحة و charstrings من Type 2 دون seac، حتى يرى كل مستهلك، لا عارضاً واحداً، الحروف نفسها

الأسئلة الشائعة

لماذا تعرض الحروف المُحرَّكة بلا حركاتها في عارض PDF لدي؟

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

هل يدعم HotPDF صيغة CFF seac في endchar؟

نعم. منذ v2.768.11 يرسم HotPDF endchar بأربعة معاملات، أو خمسة مع العرض، بوصفه محرف أساس زائد حركة منزاحة إلى (adx, ady)، محلولاً كليهما عبر أسماء StandardEncoding و charset الخاص بالخط، لكل من العرض النقطي وتصدير SVG بخطوط مضمّنة

هل تُحفظ المحارف المركبة عندما يقتطع HotPDF خطوط TrueType؟

نعم. منذ v2.770.83 يتخطى HotPDF تحويلات المكوّنات بحجم 2 أو 4 أو 8 بايتات بشكل صحيح، ويحفظ كل مكوّن مشار إليه في المجموعة الفرعية، ويعيد ترقيم المركبات المتداخلة مرة واحدة، ويتوقف بأمان عند إشارات محارف دائرية أو خارج النطاق

عرض المحارف المركبة ودعم CFF seac والاقتطاع الآمن لـ TrueType يأتيان جزءاً من HotPDF Delphi PDF Component، مكتبة VCL أصلية لـ Delphi و C++Builder تنشئ مستندات PDF وتحررها وتستخرج منها وتعرضها دون DLL خارجي