مقال تقني

تشكيل النص القابل للتوصيل: Uniscribe وHarfBuzz في Delphi

يمر تشكيل النص في مكون PDFium عبر كائن واحد قابل للتثبيت. يثبّت ConfigureTextShaper المكوِّن الذي تمر عبره كل نقطة دخول تشكيل، مع استبدال وتحرير ما كان هناك؛ ويعيد ActiveTextShaper المثبت وينشئ افتراضي المنصة عند أول استخدام؛ ويفيد ActiveTextShaperName أي مسار خلفي حي؛ ويسقط ClearTextShaper التثبيت ويسمح بأن ينشأ الافتراضي مرة أخرى. وفي Windows الافتراضي هو TPdfUniscribeTextShaper. وتحت Free Pascal يوجد TPdfHarfBuzzTextShaper، الذي يربط libharfbuzz زمن التشغيل فتكون المكتبة المفقودة حالة تُفاد بدل إخفاق تحميل

معمارية تشكيل النص القابل للتوصيل في مكون PDFium لـ Delphi: تدير ConfigureTextShaper وActiveTextShaper وClearTextShaper مسارا خلفيا مثبتا واحدا، Uniscribe على Windows وHarfBuzz مربوطا زمن التشغيل تحت Free Pascal
كل نداء تشكيل يمر عبر كائن المكوِّن المثبت الواحد، مع افتراضي منصة على كل هدف

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

لماذا يكون المسار الخلفي في Windows صنفا واحدا والمحمول ثلاث قطع؟

لأن Uniscribe أربع واجهات تتظاهر بأنها واحدة. يقسم ScriptItemize سلسلة بحسب النظام الكتابي ويحل المستويات ثنائية الاتجاه؛ ويطابق ScriptShape المحارف بالمحارف الرسومية؛ ويحسب ScriptPlace التقدمات والإزاحات؛ ويضع ScriptLayout المقاطع الناتجة في ترتيب بصري. فالمسار الخلفي المبني عليها لم يبق له ما يضيف، ولهذا يكون المكوِّن في Windows صنفا واحدا بتابع واحد

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

مقارنة مسار التشكيل لنص PDF: يوفر Uniscribe كلا ScriptItemize وScriptShape وScriptPlace وScriptLayout داخل صنف واحد، بينما يغطي HarfBuzz التشكيل والموضعة فقط حول مراحل UAX #9 الخاصة بالمكون
Uniscribe يغطي المراحل الأربع كلها؛ وعلى المسار المحمول توفير التجزيء والترتيب البصري بنفسه

المكوِّن لا يحل الخطوط، وذلك مقصود

يقرأ Uniscribe الثنائي للخط من سياق جهاز GDI. ولا يوجد لمقابل ذلك نظير محمول، واختراع واحد داخل وحدة تشكيل يعني القرار، بالنيابة عن كل تطبيق، فيما إذا كانت الخطوط تأتي من fontconfig أم من CoreText أم من مجلد خطوط تطبيق أم من قاعدة بيانات. لذا يأخذ المسار الخلفي HarfBuzz محللا: دالة استدعاء عكسية تطابق اسم خط إلى بايتات TrueType أو OpenType. وإعادة False تُفشل طلب التشكيل بالطريقة نفسها التي يفشل بها خط GDI غير قابل للقراءة في Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // سياستك: fontconfig أو CoreText أو مجلد خطوط تطبيق أو قاعدة بيانات
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // ينتقل التملك إلى الوحدة؛ استدع مرة أثناء الإقلاع،
  // قبل أن يشكّل أي شيء النص
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // في Delphi ينشأ افتراضي المنصة (Uniscribe) عند الطلب،
  // فلا حاجة إلى أي تثبيت إطلاقا
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

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

سجل النتيجة محايد للمسار الخلفي، والمجموعات هي السبب

يملأ المساران الخلفيان TPdfShapedText نفسه: النص المصدر واسم الخط والحجم وبايتات الخط ومصفوفة مقاطع والعرض الكلي وعدد المحارف الرسومية وعدد المحارف المنطقية. ويحمل كل TPdfShapedRun مداه في النص المصدر وموضعه البصري X وعرضه ومستواه ثنائي الاتجاه وعلم من اليمين إلى اليسار، بالإضافة إلى محارفه الرسومية. ويحمل كل TPdfShapedGlyph معرف محرف رسومي وتقدما وإزاحتي X وY والمجموعة التي ينتمي إليها كبداية وطول في النص المصدر

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

مدايات مجموعات المحارف في TPdfShapedText: محرف رسومي واحد لمقطع ديفاناغاري من أربعة محارف، وترابط عربي من اثنين، وأساس وعلامة من محرف واحد، وكلها تقابل عائدة عبر ClusterStart وClusterLength
مدايات المجموعة تقابل كل محرف رسومي بمحارفه المصدرية كي تعمل مؤشرات الإدخال وفحوص الإصابة والتحديدات
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // تصل المقاطع أصلا بترتيب بصري مع ملء VisualX
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

الميزانيات تنتمي إلى سجل الخيارات

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

ضبط الاتجاه صراحة بدل تركه تلقائيا يستحق الفعل كلما كنت تعرفه أصلا. يطبق التلقائي قواعد اتجاه الفقرة للتخمين من أول محرف قوي، وهو صائب للنص الحر وخاطئ لحقل نموذج اتجاهه خاصية للحقل لا للقيمة التي كتبها أحدهم فيه

ربط زمن التشغيل، لا اعتمادية بناء

يحمل المسار الخلفي HarfBuzz المكتبة ديناميكيا. وذلك قرار نشر بعواقب حقيقية: ثنائي واحد يعمل على آلة بها HarfBuzz وعلى آلة بلا هي، مفيدا بقدرة منقوصة في الحالة الثانية بدل الفشل في الإقلاع. ولمكتبة تشحن إلى مطورين آخرين ذلك هو الترتيب الوحيد العملي، لأنك لا تستطيع اشتراط أن يقتني كل مستهلك لمكون PDF مكتبة تشكيل ويطابق إصدارها وهو قد لا يحتاجها

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

ثبت مرة واحدة، قبل أن يشكّل أي شيء

يستبدل التثبيت ويحرر المكوِّن السابق، فاستدعاؤه مرارا آمن لكنه بلا جدوى، واستدعاؤه بينما يشكّل خيط آخر ليس آمنا إطلاقا. افعله أثناء الإقلاع. وإن احتجت إلى العودة إلى افتراضي المنصة لاحقا فمرر nil، وهي أيضا طريقة التراجع عن بديل اختبار في نهاية اختبار

بمجرد تثبيت مسار خلفي، يتصرف القياس واللف بنفس الطريقة على كلا المنصتين، لأنهما يستهلكان مقاييس المقاطع والمحارف الرسومية بدل النداء على المنصة مباشرة؛ ونموذج اللف موصوف في مقال قياس النص ولف الكلمات. والمنصات وسلاسل الأدوات المدعومة للمكون مدرجة في صفحة منتج PDFium Delphi component