مقال تقني

ضبط قيم حقول النماذج في PDF محمّل باستخدام Delphi

يملأ HotPDF Delphi Component حقل AcroForm قائماً على PDF محمّل عبر THotPDF.SetFormFieldValue، مخاطَباً إما بفهرس حقل صفري الأساس وإما باسم حقل مؤهَّل بالكامل. كتابة إدخال /V الجديد هي الجزء السهل؛ أما ما يجعل الاستدعاء موثوقاً على النماذج الواقعية فهو أن الطريقة نفسها تبقي ثلاث قطع حالة متسقة لا تُرى حتى تفسد: الهوية المفكوكة للحقل حتى يمكن أصلاً العثور على اسم غير لاتيني، وحالة المظهر /AS على ودجات مربعات الاختيار والراديو، ومصفوفة فهارس التحديد /I على حقول الاختيار. أما تدفق المظهر المرئي فهو خطوة منفصلة صريحة عبر EnsureLoadedFieldAppearanceStream

السيناريو هو السيناريو المألوف: يرسل لك عميل نموذجه الخاص، إقراراً ضريبياً، أو مطالبة تأمين، أو أمر شراء بناه أحدهم في Acrobat قبل سنوات، وعلى تطبيق Delphi لديك أن يملؤه من قاعدة بيانات ويسلّم ملفاً يفتح صحيحاً في كل مكان. لا سيطرة لك على كيفية تأليف النموذج. فقد تكون أسماء الحقول مشفرة UTF-16، وقد تكون قيم تصدير مربعات الاختيار 2 لا Yes، وقد تستخدم القوائم المنسدلة أزواج خيارات [export display]. ولكل تفصيل من تلك قاعدة في ISO 32000-1، وكل قاعدة منها شيء تعالجه SetFormFieldValue الآن عنك. هذه المقالة عما تفعله، ولماذا، وأين تتوقف. أما للمشكلة الشقيقة، إنشاء حقول غير موجودة بعد، فانظر إضافة حقول AcroForm إلى PDF محمّل في Delphi

لماذا يفشل SetFormFieldValue في العثور على حقل باسم غير لاتيني؟

قبل v2.752.1 كان الجواب الترميز: كان الحقل يسكن الملف تحت اسم UTF-16BE سداسي عشري، وكانت ذاكرة الأسماء المخزنة تحفظ التهجئة السداسية لا النص. يعرّف ISO 32000-1 §12.7.3.1 اسم الحقل الجزئي /T كسلسلة نصية، ويقول §7.9.2.2 إن السلسلة النصية قد تكون UTF-16BE بعلامة ترتيب بايتات FE FF مبتدئة. وتصوغ أدوات التأليف تلك الأسماء اعتيادياً كسلاسل سداسية عشرية وفق §7.3.4.3، فيصل حقل اسمه Straße كـ <FEFF005300740072006100DF0065>. داخل HotPDF يبقي THPDFStringObject.Value النص السداسي الخام كلما ضُبط IsHexadecimal، وهو بالضبط ما تريده لجولة ذهاب وإياب بلا خسارة للقاموس الأصلي، وبالضبط ما لا تريده كمفتاح بحث. تفصل HPDFLoadedFormTextName الشاغلين. عند بناء ذاكرة العلاقات يمر كل قيمة /T عبرها: إذا كان كائن السلسلة سداسياً أعادت HPDFHexToBytes تتابع البايتات؛ وإذا بدأت البايتات بـ FE FF وكان طولها زوجياً فُكّت الحمولة كـ UTF-16BE وأُعيد ترميزها UTF-8؛ ثم يُربط النتيجة باسم أمها بنقطة لتشكيل الاسم المؤهَّل بالكامل الذي يصفه §12.7.3.1، فيُسجل ابن اسمه City تحت أم اسمها Address بوصفه Address.City. ويُطبَّع مفتاح الذاكرة إلى حروف صغيرة، مما يجعل SetFormFieldValue('address.city', ...) ينجح أيضاً؛ تلك ميسّرة فوق المعيار، لأن المواصفة تعامل الأسماء كحساسة للحالة. والأهم أن مفتاح الذاكرة وحده يتغير. يبقي كائن /T في قاموس الحقل ترميزه السداسي، فلا تعيد حفظ المستند كتابة هوية حقل ملأته فحسب

كيف يحل HotPDF أسماء AcroForm غير اللاتينية: تستعيد HPDFHexToBytes حمولة UTF-16BE خلف سلسلة /T سداسية، وتفك علامة ترتيب البايتات FE FF وتعيد ترميزها UTF-8، وينضم الاسم المؤهَّل إلى أمه فيهبط Applicant.FullName وحقل اسمه Straße كلاهما إلى ذاكرة البحث
مفتاح الذاكرة وحده يتغير: يبقي قاموس الحقل ترميزه السداسي، وتُطبَّع عمليات البحث إلى حروف صغيرة كميسّر فوق المعيار، ولا تعيد حفظ المستند أبداً كتابة هوية حقل ملأته فحسب
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // الأسماء المؤهلة تفك من سلاسل /T ذات UTF-16BE وتُربط
    // بالنقاط، فتحل الأسماء المتداخلة وغير اللاتينية
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
    Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');

    // القيم غير اللاتينية-1 تسافر كسدس UTF-16BE مسبوق بـ FEFF
    // وتُكتب كسلسلة PDF ست عشرية
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

ماذا يكتب SetFormFieldValue فعلاً؟

يجري كلا الحملين الزائدين الخطوات الخمس نفسها: تعيين قاموس الحقل، وكتابة /V عبر HPDFSetDictFormValue، وموازنة فهارس تحديد الاختيار، وتمييز القاموس متسخاً، وموازنة حالات مظهر الأزرار، وأخيراً تسجيل فهرس الحقل عبر NoteLoadedFormFieldDirty. تلك الخطوة الأخيرة تهم إذا حمل النموذج سكربتات حساب، لأن المجموعة المتسخة هي ما يستهلكه الحمل الزائد بلا وسائط من RecalculateLoadedFormFieldsIncremental لإعادة تشغيل الحسابات التي تقرأ نقلّياً حقلاً تغيّر فحسب. و HPDFSetDictFormValue ذاتها حذرة بشأن نوع الكائن الذي تستبدله. إن كان /V القائم كائن اسم، وهو ما تستخدمه حقول مربعات الاختيار والراديو لقيمة تصديرها، كُتبت القيمة الجديدة اسماً لا سلسلة أبداً، لأن أسماء PDF لاتينية فقط بالبناء. وإلا كتبت كائن سلسلة وفحصت القيمة التي مررت: سلسلة تبدأ بـ FEFF وطولها زوجي وتتألف من أرقام سداسية وحدها تعامل كصيغة سلك UTF-16BE من §7.9.2.2 وتخزن مع ضبط IsHexadecimal، فتصاغ <FEFF...> لا (FEFF...) حرفية. تلك هي الآلية التي يتكئ عليها سطر City أعلاه؛ وأي سلسلة أخرى تخزن كسلسلة حرفية بالبايتات التي أعطيتها إياها، فللنص اللاتيني العادي تمرر نصاً عادياً

لماذا يبقي مربع الاختيار علامته القديمة بعد تغيّر القيمة؟

لأن حقل زر لا تقرر قيمته وحدها ما يُرسم. يحدد ISO 32000-1 §12.7.4.2.3 أن ودجة مربع اختيار تحمل حالة مظهر /AS تسمي أي تدفق في /AP /N معروض حالياً، والعارضون يرسمون من /AS لا من /V. إن غيّرت /V إلى Yes وتركت /AS على Off صار الملف متناقضاً داخلياً، وستسعد التسطيح بخبز مظهر قديم غير مؤشر في الصفحة بينما تقول بيانات النموذج مُشار. و ReconcileLoadedButtonAppearanceStates وُجدت لتغلق تلك الفجوة: لحقل /FT فيه Btn تزور قاموس الحقل نفسه وكل إدخال في مصفوفة /Kids، وتقرأ اسم الحالة الفعالة من /AP /N، وتعيد كتابة /AS إلى ذلك الاسم حين يطابق قيمة الحقل أو إلى Off حين لا يطابق

لماذا يبقي مربع اختيار HotPDF علامته القديمة حين تتغير /V وحدها: يرسم العارضون من حالة المظهر /AS إلى /AP /N، فتزور ReconcileLoadedButtonAppearanceStates الحقل وكل ابن، وتقرأ اسم الحالة الفعالة كأول مفتاح غير Off، وتعيد كتابة /AS عند التطابق أو إلى Off بخلاف ذلك
تقارن مجموعات الراديو كل ابن مقابل قيمة الأم التي تستعيدها InheritedButtonValue بتجول سلسلة /Parent، فيشغّل ضبط المجموعة على قيمة تصدير واحدة تلك الودجة بالضبط ويطفئ كل أشقائها

تفصيلان من نماذج حقيقية صاغا إصلاح v2.752.3. أولهما يُسمح لقاموس مظهر عادي أن يحوي الحالة الفعالة وحدها؛ يسمي §12.7.4.2.3 مظهر الإيقاف Off لكن أدوات التأليف تغفل تدفقه كثيراً وتترك العارض لا يرسم شيئاً. كان الكود الأقدم ينسحب حين يحوي القاموس أقل من إدخالين، فكانت تلك مربعات الاختيار أحادية الحالة تبقي علامتها القديمة بصمت. الفحص الآن ببساطة أن القاموس غير فارغ، ويؤخذ اسم الحالة الفعالة كأول مفتاح ليس Off. وثانيهما أن اسم الحالة الفعالة هو ما اختاره المؤلف. تستخدم النماذج الحقيقية 2 أو Yes أو On أو كلمة محلية، فالمقارنة تكون على المفتاح الفعلي وبلا حساسية للحالة، أبداً على Yes مكتوبة إسمنياً. تضيف أزرار الراديو عقدها أخرى موصوفة في §12.7.4.2.4: التحديد يسكن /V على حقل الأم، بينما تملك الأبناء الفردية الودجات وعادة بلا /V خاصة بها. لذلك تطلع مساعدة InheritedButtonValue المتداخلة سلسلة /Parent حتى 64 مستوى حتى تجد قيمة غير فارغة، فيقارن كل ابن بقيمة المجموعة التي يخصّها. ضبط الأم على قيمة تصدير أحد الأبناء يشغّل ذلك الابن وحده ويطفئ كل أشقائه

// مربع اختيار: يجب أن تطابق قيمة التصدير مفتاح الحالة الفعالة في /AP /N
// (غالباً 'Yes'، لكن النماذج الحقيقية تستخدم '2' أو 'On' أو أي شيء)
Pdf.SetFormFieldValue('Consent', 'Yes');

// مجموعة راديو: تُكتب /V على الأم؛ وكل ودجة ابن تحصل
// على /AS ضابطة باسم تصديرها الخاص أو بـ Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// تفريغ مربع اختيار: أي قيمة لا تطابق حالة فعالة تعطي /AS بقيمة Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

حقول الاختيار: إبقاء /I متزامنة مع /V

لصندوق منسدل أو قائمة تمرير، /V ليست الموضع الوحيد الذي يُسجَّل فيه تحديد. يعرّف الجدول 231 في §12.7.4.4 التابع /I كمصفوفة فهارس صفرية الأساس داخل /Opt تعرّف العناصر المختارة، والعارض الذي يجد /I تشير إلى الخيار 0 بينما تسمي /V الخيار 3 قد يبرز الصف الخطأ. منذ v2.754.1 تجري HPDFReconcileChoiceSelection داخل كل استدعاء SetFormFieldValue، وعندما يكون /FT الموروث Ch تعيد بناء /I من القيمة الجديدة. ترتيب العمليات مقصود. يُحذف إدخال /I المحلي أولاً، دون لمس محتواه: إن كانت المصفوفة القديمة كائناً غير مباشر يتشاركه حقل آخر، كان تعديلها في الموقع يفسد تحديد ذلك الحقل الآخر، فيسقط الروتين المرجع وينشئ مصفوفة مباشرة جديدة بدلاً منه. ثم يحل /Opt عبر سلسلة /Parent، لأن خيارات الاختيار قد تكون موروثة، ويمسح الإدخالات. خيار سلسلة عارية يُقارن مباشرة؛ وزوج [export display] يُقارن على عنصر تصديره، والزوج بأقل من عنصرين يتخطى. ويمر كلا الجانبين عبر HPDFLoadedFormTextName، فيطابق خيار UTF-16 سداسي قيمة UTF-16 سداسية دون أن تضطر إلى تهجئتيهما بالطريقة نفسها. عند أول تطابق تُكتب /I من عنصر واحد ويتوقف المسح؛ والقيمة العددية تستبدل دائماً أي تحديد متعدد سابق، أياً كان علم MultiSelect

كيف يبقي HotPDF حقل اختيار متسقاً: تحذف HPDFReconcileChoiceSelection مصفوفة /I المحلية قبل لمسها، وتحل /Opt عبر سلسلة /Parent، وتقارن نصف التصدير لكل خيار عبر HPDFLoadedFormTextName، وتكتب /I من عنصر واحد عند أول تطابق ولا تكتب شيئاً حين تكون لقيمة قائمة منسدلة قابلة للتحرير بلا فهرس
خيار سلسلة عارية يقارن مباشرة وزوج export display على عنصر تصديره، بينما تترك قيمة خارج /Opt بلا فهرس كما يجب — فهرس قديم يشير إلى الصف الخطأ أسوأ من لا فهرس

حين لا شيء يطابق لا تُكتب /I إطلاقاً. تلك هي النتيجة الصحيحة لقائمة منسدلة قابلة للتحرير، حيث يجيز §12.7.4.4 للمستخدم كتابة قيمة خارج قائمة الخيارات؛ فمثل تلك القيمة لا فهرس لها، والفهرس القديم أسوأ من لا فهرس. وهي أيضاً ما تحصل عليه إن مررت تسمية عرض بدل قيمة تصدير إلى قائمة خيارات مزدوجة، فحين يرفض صندوق منسدل عرض تحديدك افحص أي نصف الزوج سلّمته

// /Opt هي [[US United States] [CA Canada] [MX Mexico]]:
// طابق على قيمة التصدير وتصبح /I هي [1]
Pdf.SetFormFieldValue('Country', 'CA');

// قائمة منسدلة قابلة للتحرير بقيمة خارج /Opt: تُكتب /V،
// وتُزال /I، ولا يُفبرك أي فهرس
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

القيمة والمظهر عمليتان منفصلتان

لا تلمس SetFormFieldValue تدفق مظهر حقل نص أو اختيار أبداً. بعد الاستدعاء تحمل /V النص الجديد بينما ما زالت /AP /N ترسم القديم، وأيهما يعرضه العارض يتوقف على هل يحمل قاموس AcroForm تابع /NeedAppearances true وفق §12.7.3.3 وهل يحترم العارض ذلك. إن كنت تحتاج أن يعرض الملف القيمة الجديدة في كل قارئ، بما فيهم المسطّحون ومولدات الصور المصغرة الذين يتجاهلون العَلَم، استدعِ EnsureLoadedFieldAppearanceStream بفهرس الحقل. تبني Form XObject من سلسلة /DA الموروثة وتقسيم /Q وتخطيط /MaxLen الشبكي والقيمة، وتحل الخط المسمى عبر موارد /DR لـ AcroForm حتى يبقي خط Type0 خطه السليل ولا يتدهور إلى Helvetica، وتعيد True حين تستلم ودجة واحدة على الأقل تدفقاً. الحمل الزائد بالاسم من SetFormFieldValue لا يعطيك فهرساً، فاحصل عليه عبر GetFormField، التي تعيد THPDFLoadedFormField تملكه وتجب على تحريره. حزمة انحدار تغيير v2.752.1 صريحة بشأن هذا التقسيم: تضبط قيمة، وتستدعي EnsureLoadedFieldAppearanceStream، ثم تعرض الصفحة وتفحص أن البكسلات داخل مستطيل الودجة تغيرت وأن البكسلات خارجه لم تتغير. التحقق من تغيّر /V لا يثبت شيئاً عما سيراه مستخدم

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // ارسم القيمة الجديدة في /AP حتى يعرضها العارضون الذين يتجاهلون
    // /NeedAppearances مع ذلك
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

حدود يعرفها قبل أن تبني على هذا

يفحص ReconcileLoadedButtonAppearanceStates التابع /FT المحلي للقاموس الذي خاطبته، فيعمل على أم الراديو أو على مربع اختيار يحمل /FT خاصته؛ وودجة ابن خاطبها وحدها، بـ /FT على أمها وحدها، لا تُوازن عبر ذلك المسار. وتعالج HPDFReconcileChoiceSelection قيمة عددية واحدة وتكتب فهرساً واحداً على الأكثر؛ فصناديق قوائم متعددة التحديد بعدة إدخالات مختارة خارج ما يمثله SetFormFieldValue. ولا أي من الروتينين يتحقق من القيمة التي تمررها مقابل /Opt أو مقابل مفاتيح الحالة الفعالة، فخطأ إملائي ينتج مربع اختيار على Off أو قائمة بلا فهرس لا استثناء. ويعيد GetFormFieldValue نص /V المخزن كما يجلس في القاموس، وهو لقيمة مشفرة سداسياً يعني التهجئة السداسية لا النص المفكوك

بمجرد دخول القيم ورسم المظاهر تجلس الخطوتان التاليتان الطبيعيتان على جانبي هذه العملية. تبادل بيانات الحقول مع أنظمة خارجية بالجملة، لا استدعاء SetFormFieldValue واحد في كل مرة، هو ما تغطيه استيراد وتصدير XFDF في Delphi. وحين يصير النموذج المملوء نهائياً ولا ينبغي أن يبقى قابلاً للتحرير، يخبز تسطيح حقول AcroForm و XFA في Delphi بالضبط حالات /AS وتدفقات المظهر الموصوفة هنا في محتوى صفحة ثابت، ولهذا فالتناسق بينها قبل التسطيح ليس خياراً

واجهة تحرير النماذج المحمّلة في هذه المقالة، بما فيها SetFormFieldValue و EnsureLoadedFieldAppearanceStream ورسم إعادة الحساب التزايدي، تشحن جزءاً من HotPDF Delphi Component لـ Delphi و C++Builder