مقال تقني

حقول Comb بالعربية والعبرية في Delphi مع HotPDF

يملأ HotPDF حقول comb في AcroForm بالعربية والعبرية والعلامات المركبة والنص المختلط الاتجاه عبر THPDFHeadlessDocument.SetTextFieldValueWithFamilies. شبكة comb تعد بوحدات Unicode scalars، ويتمركز الرباط أو الحرف الأساس بعلاماته كعنقود واحد عبر الخلايا k التي تملكها scalars خاصته، ويمر السطر كله عبر خوارزمية Unicode ثنائية الاتجاه قبل إسناد أي خلية

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

طريقتان لملء حقل comb عربي مع HotPDF في Delphi: اختصار كل-محرف يسحق رباط لام-ألف، والملء المشكَّل يتمركز كل عنقود
عد Unicode scalars وإعادة ترتيب السطر قبل إسناد الخلايا يبقيان الرباط متمركزًا عبر خانتيه ولا يتركان خلية فارغة

ماذا يطلب علم Comb فعلًا من كاتب PDF؟

يطلب علم Comb من الكاتب أن يقسم الحقل إلى MaxLen موقعًا متباعدًا بالتساوي بالضبط ويضع محرفًا واحدًا في كل منها. يعرف §12.7.4.3 من ISO 32000-1 الـ Comb كموضع بت 25 في الجدول 228، أعلام الحقول الخاصة بحقول النص، ولا يجعله ذا معنى إلا حين يكون إدخال MaxLen من الجدول 229 موجودًا وأعلام Multiline وPassword وFileSelect صافية. والبت نفسه هو $1000000 في قيمة /Ff ذات 32 بت؛ وإن سبق أن رأيت حقول comb تتصرف بسوء بعد جولة عبر متغير فاصلة عائمة، فـ مقالة أعلام حقول AcroForm ذات 32 بت تشرح لماذا يكون ذلك البت أول من يسقط

المواصفة تقول «محرف» وتتوقف عنده: كتبت بعين قلبات لاتينية ثابتة الخطوة ولا تقول ما المحرف حين يصير زوج حرفين عربيين glyph واحدًا، أو حين تكون علامة حركة بلا عرض خاص بها. وعلى HotPDF أن يختار وحدة، وهو يفرض القواعد البنيوية بصرامة وهو يفعل: حقل comb كثير الأسطر أيضًا، أو حامل MaxLen غير موجب، يرفض بتشخيص، وحقول password لا يمكن أن تكون comb أصلًا

لماذا تعد Unicode scalars بدل وحدات UTF-16 أو glyphs أو graphemes؟

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

  • وحدات كود UTF-16 هي ما يعيد Length في Delphi، فمحرف تكميلي مثل U+20BB7 سيأكل خانتين وموقعين من ميزانية الحقل
  • عد الـ glyphs يعتمد على الخط: معظم الخطوط العربية تجعل اللام مع الألف glyph واحدًا، فيتغير عدد الخلايا لحظة تبديل الخطوط
  • عد الـ graphemes يدمج الحرف الأساس مع علاماته، أي أن الشبكة ستخالف فحص MaxLen الذي يعد كل scalar كتبه المستخدم

التحقق من MaxLen في THPDFHeadlessDocument يمشي في السلسلة فقط حين يتجاوز طولها UTF-16 الحد، وحين يفعل يعد زوج surrogate صالحًا مرة واحدة ويرفض surrogate غير مقترن جملة. وإن أراد UI الخاص بك التحذير قبل الاستدعاء بدل التقاط استثناء بعده، فعُد بالطريقة نفسها

مصفوفة تقارن وحدات UTF-16 وglyphs وgraphemes بـ Unicode scalars لخلايا comb في HotPDF على Delphi، مع لام-ألف وباء مع فتحة وU+20BB7 وe منوَّطة
عمود Unicode scalar وحده يوافق فحص MaxLen — زوج surrogate يحسب مرة واحدة ولا يعتمد عدد الخلايا على الخط أبدًا
// يعد Unicode scalars بالطريقة التي تعد بها شبكة comb:
// زوج surrogate صالح هو scalar واحد
function CountScalars(const S: UnicodeString): Integer;
var
  I: Integer;
begin
  Result := 0;
  I := 1;
  while I <= Length(S) do
  begin
    if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
       (I < Length(S)) and
       (Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
      Inc(I, 2)
    else
      Inc(I);
    Inc(Result);
  end;
end;

قيم قليلة تريك إلى أي مدى تتشعب الوحدات، ولماذا يوافق عمود scalar وحده ما يضع له حقل comb ميزانية

القيمةوحدات UTF-16‏Scalars (الخلايا)‏glyphs معتادة
لام + ألف (لا)22‏1 رباط
باء + فتحة (بَ)222، والعلامة ملحقة بالأساس
U+20BB7 (𠮷)211
‏e مفكك + U+0301221 أو 2، بحسب الخط

كيف تتقاسم الربوط والعلامات المركبة خلاياها؟

العنقود المشكَّل الذي يغطي k من scalars يحتل خلايا k متجاورة ويتمركز كوحدة واحدة عبرها. بعد التشكيل يخط كل عنقود مرة أخرى إلى المدى الدقيق من نص المصدر الذي جاء منه، فيعرف HotPDF أي scalars أي خلايا يملك. ورباط لام-ألف المذكور أعلاه يحصل على خانتين ويجلس متمركزًا عبرهما. والباء الحاملة شادة وفتحة ثلاثة scalars، فتحصل على ثلاث خانات، والعلامات ما تزال ملحقة بالحرف

التمركز هو موضع انحراف التنفيذ البديهي: تمركز كل glyph في خانته يجذب العلامة بعيدًا عن أساسها، لأن موضع العلامة لا يعني شيئًا إلا نسبة إلى الحرف الذي تزينه. يحفظ HotPDF كل glyph داخل عنقود عند تقدمه المشكَّل وإزاحتي X وY خاصتين به، ثم يتمركز العنقود باستخدام المدى الأفقي المجمع للتقدم الطبيعي وأصول glyph المتموضعة وحبر glyph الفعلي. يتحرك العنقود ككتلة؛ ولا يعاد ترتيب شيء داخله

تبعة واحدة سهلة الفقدان: ضوابط الاتجاه مثل U+200F RIGHT-TO-LEFT MARK وU+061C ARABIC LETTER MARK وضوابط التضمين والعزل هي Unicode scalars بلا glyph. تحتسب مقابل MaxLen وتحجز خلية تبقى فارغة. وإن جاءت بيانات مصدرك من نظام يرش تلك العلامات في الأسماء، فجرفها قبل الملء، وإلا سيعرض الحقل فجوة وسيرفض القيم محرفًا واحدًا قبل ما يتوقعه المستخدم

لماذا يجب أن يحدث إعادة ترتيب bidi قبل إسناد الخلايا؟

يشغل HotPDF السطر الكامل عبر خوارزمية Unicode ثنائية الاتجاه (UAX #9) أولًا، ثم يعطي كل scalar خليته من موضعه البصري بعدها فقط. إسناد الخلايا بترتيب منطقي وقلب النتيجة لاحقًا يعمل على سلسلة عربية خالصة ويفشل بمجرد أن يحوي السطر أرقامًا أو رمزًا لاتينيًا، لأن تلك المسارات يجب أن تظل تقرأ من اليسار إلى اليمين داخل السطر من اليمين إلى اليسار. وقيمة ككود مرجعي ببادئة عربية ورقم تحفظ أرقامها بالترتيب الصحيح فقط إن رأت إعادة الترتيب السطر كله مرة واحدة. ونظرة تشكيل النصوص المعقدة تغطي مشكلة الترتيب المنطقي مقابل البصري بعمق أكبر

إسناد خلايا comb لقيمة عربية بأرقام في HotPDF لـ Delphi: الترتيب المنطقي يعكس الرقم، بينما UAX 9 أولًا يبقي الأرقام سليمة
شغل الخوارزمية ثنائية الاتجاه على السطر كله أولًا، ثم أعط كل scalar الخلية عند موضعها البصري فيبقى ترتيب الأرقام المضمَّنة

ثم تموضع المحاذاة الشبكة كاملة من الخلايا المستخدمة داخل الحقل: إدخال /Q الموروث للحقل يفوز حين يكون موجودًا، ولا ينطبق Options.Alignment إلا حين لا يملك النموذج شيئًا. وهذا مهم للنماذج من اليمين إلى اليسار: قيمة عربية قصيرة في حقل comb محاذى يسارًا تملأ الخانات الأيسر، وهو صحيح وفق المواصفة وما يقصده مصمم النموذج تقريبًا أبدًا. أصلح /Q الخاص بالنموذج، أو اضبط htaRight حين لا يملك الحقل ضبط محاذاة خاصًا به. ودوران الـ widget يأتي من إدخال /R في /MK، وعند 90 أو 270 درجة يتبادل viewport النص المنطقي العرض والارتفاع بينما يبقى منطق الخلايا كما هو

ملء حقل comb بخطوط مختلطة من Delphi

نص comb متعدد الخطوط يستخدم شبكة خلايا واحدة وحجم خط واحد وخط أساس واحد لكل خط معطى. مع FallbackFontFamilies مضبوطة على True، يختار HotPDF خطًا لكل grapheme كاملة من العائلات المعطاة بترتيبها المضبوط، فيحمل وجه عربي الحروف بينما يحمل وجه لاتيني ASCII مضمَّن. وكل مدى خط يشكَّل والسطر كله سياقًا، ولا تنفصل علامة عن أساسها عبر الخطوط أبدًا، ولا يخترع رباط عبر حد خط. تخطيط كل مدى بشبكته المحلية أو قياسه إلى صندوقه المحيط سيكون أبسط، وسيربك مواضع الخلايا والحقول المدارة لحظة تبديل الاحتياط للخطوط في منتصف السطر

uses
  System.SysUtils, System.IOUtils,
  HPDFHeadlessDocument, HPDFHeadlessAppearance;

procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  Families: THPDFHeadlessRichTextFontFamilies;
  Options: THPDFHeadlessTextAppearanceOptions;
  FieldIndex: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    FieldIndex := Doc.FindFormField('applicant.name_ar');
    if FieldIndex < 0 then
      raise Exception.Create('Field not found');
    Field := Doc.GetFormField(FieldIndex);
    if (Field.Flags and $1000000) = 0 then   // بت 25: ‏Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // من واحد إلى ستة عشر عائلات
    Families[0].FamilyName := 'Noto Naskh Arabic';
    Families[0].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
    Families[1].FamilyName := 'Noto Sans';
    Families[1].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoSans-Regular.ttf');

    Options := THPDFHeadlessTextAppearanceOptions.Default;
    Options.ShapeComplexText := True;        // هي الافتراضي أصلًا
    Options.FallbackFontFamilies := True;    // الافتراضي False
    Options.FontSize := 0;                   // ملاءمة تلقائية، سقفها 12
    Options.Alignment := htaRight;           // يستخدم فقط حين يكون /Q غائبًا

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // يجب أن يخالف SourceFile
  finally
    Doc.Free;
  end;
end;

لاحظ ما لا يضبطه المثال: أثناء ملء المستند، تشتق Width وHeight وRotation وCombCells من مستطيل كل widget وإدخال /MK خاصته وMaxLen الخاص بالحقل، فالقيم التي تضعها في تلك الأعضاء تكتب فوقها. ‏FontSize صفرية تطلب تحجيمًا تلقائيًا يفحص مدى كل عنقود مقابل عرض خلاياه k والارتفاع المتري وحبري المشترك لكل الخطوط المشاركة؛ والحجم الثابت يقص ببساطة إلى الـ widget. لا يحرك CharacterSpacing وWordSpacing مواضع comb أبدًا، لأن الشبكة وحدها تقرر أين يهبط العنقود. يحمّل HotPDF بيئتي تشغيل HarfBuzz وFriBidi فقط حين تتطلب قيمة فعلًا التشكيل، والقيم التي لا تحتاج تشكيلًا تحفظ تخطيط comb المباشر القائم دون تغيير. وسير ملء PDF محمل العام، بما فيه الأذونات والحفظ، انظر ضبط قيم حقول النماذج في PDF محمل

ماذا لا يضمنه مظهر comb مشكَّل؟

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

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

استخراج النص هو الثاني: يحفظ /V الخاص بالحقل القيمة المنطقية الصافية، ويحمل Form XObject الأب للمظهر مدى /ActualText واحدًا (§14.9.4 من ISO 32000-1) بالدخل الكامل؛ أما الـ Forms الأبناء فلا تحمل شيئًا. المستخرجات التي تحترم ActualText تسترجع السلسلة الأصلية. والمستخرجات التي تتجاهلها تبني النص من مواضع glyphs وقد تعيد ترتيبًا مختلفًا أو تكرب نص العنقود، لأن عنقودًا متعدد الـ glyphs لا قيمة Unicode واحدة بديهية لكل glyph. وإن احتاج نظام في المصب القيمة، فاقرأ الحقل لا نص الصفحة

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

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    if E.Code = hdLimitExceeded then
      ShowFieldError('The value has more characters than the field allows')
    else
      raise;
  on E: EHPDFHeadlessAppearanceError do
    if E.Failure = haMissingGlyph then
      ShowFieldError('None of the supplied fonts covers this text')
    else
      raise;
end;
// على أي من الخطأين يبقي الحقل قيمته ومظهره السابقين

موضع حقول comb المشكَّلة في خط أنابيب نماذج

دعم comb المشكَّل أهم ما يكون للنماذج الموجودة أصلًا: إقرارات الضريبة، وطلبات التأشيرات، وحزم الانضمام المصرفية، وتسجيلات الهوية المصممة بحقول مبربَّطة وتملأ اليوم بالعربية أو العبرية أو الفارسية أو الأردية. عُد بوحدات scalars، واحفظ القيمة الأصلية في /V، ودع شبكة واحدة تحكم كل خط، واختبر بقيمة تمزج حروفًا من اليمين إلى اليسار وأرقامًا وعلامة مركبة، فتلك السلسلة الواحدة تمارس كل قاعدة في هذه المقالة. وAPI المستند headless وتشكيل comb والاحتياط للخطوط الموصوفة هنا جزء من مكوّن HotPDF لـ PDF في Delphi