مقال تقني

تضمين خطوط CID من النوع 2 في PDF من Delphi

يكتب PDFium Component نصًا في صفحة PDF على مستوى الحرف باستخدام AddCidType2Text، مُضمِّنًا بيانات TrueType التي توفّرها كخط CID من النوع 2. ويُخصِّص لكل نسخة حرف قيمة CID متسلسلة، ويولّد خريطة CID إلى GID الصريحة وخريطة ToUnicode، ويدمج الحروف على خط الأساس نفسه في كائن نص أصلي واحد. والتجزئة الفرعية الاختيارية تُبقي الملف صغيرًا، بسياسة صريحة لما يحدث عند فشل التجزئة

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

يسند AddCidType2Text في Delphi لكل نسخة محرف مشكَّل CID متسلسلًا، ويكتب CIDToGIDMap و CMap للـ ToUnicode، ويدمج المقطع في كائن نص صفحة واحد يستخرج بنظافة
تأخذ كل نسخة محرف CID الخاص بها، ويحوّل قاموس الخط معرفات CID إلى معرفات محارف وعائدةً إلى Unicode، وتهبط المقطع على الصفحة كائن نص واحد يُستخرج راجعاً إلى النص المقصود

لماذا يحصل كل نسخة حرف على قيمة CID خاصة به

التحسين المُغري هو إزالة التكرار: قيمة CID واحدة لكل معرّف حرف مميز، تُعاد استخدامها أينما ظهر ذلك الحرف. وينتج هذا خطًا أصغر ويُفسد استخراج النص، لأن الحرف نفسه يمكن أن يقابل بشكل مشروع محتوى Unicode مختلفًا في أماكن مختلفة

وحرف الرابطة الحالة الواضحة. فحرف "ffi" نفسه قد يمثل الأحرف الثلاثة في كلمة واحدة، وبعد قرار تشكيل مختلف، شيئًا آخر في مكان آخر. وخريطة ToUnicode مُفهرَسة بـ CID، بحيث لا يمكن لقيمة CID مشتركة أن تحمل إلا تعيينًا واحدًا، وأي نص يخسر تلك المنافسة يصبح غير قابل للاستخراج

لذلك تأخذ كل نسخة حرف قيمة CID الخاصة بها، وكل تعيين حر في أن يكون عدة وحدات ترميز UTF-16. أما الحرف الذي لا يحمل نصًا ذا معنى — عنصر زخرفي، حرف مخصص للتموضع فقط — فيُعيَّن صراحة إلى U+200B، وهو مسافة بعرض صفري، بحيث تحمل كل قيمة CID نتيجة استخراج قابلة للملاحظة بدلاً من فجوة

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // إدخال واحد لكل حرف مُشكَّل: معرّف الحرف، والنص الذي يمثله،
    // وتقدّمه وإزاحاته في فضاء النص
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // التجزئة الفرعية مفضَّلة
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

التجزئة الفرعية، والفحوصات التي لا يمنحك إياها رمز إرجاع

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

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

وما يحدث بعد ذلك سياسة المستدعي. فتحت pcfemSubsetPreferred، الخيار الافتراضي، ترجع مجموعة فرعية مرفوضة إلى تضمين الخط الكامل، بحيث تكون الصفحة صحيحة وأكبر حجمًا فقط. وتحت pcfemSubsetRequired، تفشل العملية قبل تعديل الصفحة، وهذا ما يريده خط أنابيب مقيَّد بالحجم. ويتخطى pcfemFull التجزئة الفرعية كليًا. ويخبرك التقرير بأي مسار اتُّبع عبر SubsetAttempted و SubsetApplied و UsedFullFontFallback و SubsetErrorCode

يجتاز تقسيم الخطوط الفرعي CID Type 2 في Delphi ثلاثة فحوص تحقق للحجم وسلامة sfnt وتغطية المحارف قبل القبول، ويتبع التقسيم المرفوض سياسة pcfem ببديل الخط الكامل أو فشل صارم
لا يُقبل جزء إلا بعد اجتياز فحوص المقاس وsfnt وتغطية المحارف، ويتبع الجزء المرفوض عندئذ سياسة المستدعي — التراجع إلى الخط الكامل أو الإخفاق قبل تعديل الصفحة

تحقق لا يمكن أن ينتج نتيجة إيجابية زائفة

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

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

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

الميزانيات والفشل الذري

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

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

أين يقع هذا في خط أنابيب نصي

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

وبالنسبة للنص اللاتيني العادي دون تشكيل سياقي، فإن واجهات النص الأبسط القائمة على السلاسل النصية هي الأداة الصحيحة وتنتج كودًا أصغر. الجأ إلى الكتابة بـ CID من النوع 2 عندما يكون لديك مخرجات مُشكَّلة، وعندما تحتاج تحكمًا دقيقًا في معرّفات الحروف، أو عندما يجب تضمين الخط من بايتات تملكها بدلاً من حله بالاسم — والبديل لحل الخط هو آلية المزوّد الموصوفة في التحكم في استبدال خطوط PDF

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

تشترك كتابة النص على مستوى الحرف، وتوفير الخطوط، واستخراج النص في نموذج الصفحة نفسه في Delphi و C++Builder و Lazarus؛ وتُوصف الواجهة البرمجية الكاملة على صفحة PDFium Component لـ Delphi