مقال تقني

ملء دفعات ذرية لحقول نماذج PDF في Delphi مع PDFium

يملأ PDFium Component لـ Delphi نموذج AcroForm ذريًا عبر TPdf.SetFormFieldValues: تمرر من 1 إلى 256 طلبًا منمَّقًا معنونًا بالصفحة والـ widget، والمكوّن إما يطبق كل قيمة في استبدال مستند واحد أو لا يطبق شيئًا. وحين يكون أحد الطلبات غير صالح، يسمي TPdfFormValuesWriteReport.FailedRequest الطلب الذي أغرقت الدفعة

ذلك الضمان أهم ما يكون في المهام التي لا يراقبها أحد: استيراد ليلي يدفع صفوف CRM في نموذج مطالبة، أو خدمة طباعة ترسم فوق قالب مطبوع مسبقًا بيانات وثيقة. شغلها حقلًا حقلًا وأول قيمة سيئة، كود ليس قيمة export صالحة لراديو أو سلسلة أطول من MaxLen للحقل، تتركك تمسك PDF نصفه بيانات قديمة ونصفه جديد. لم ينهار شيء فلم ينبه أحد. وAPI الدفعة موجود لكي يكون الفشل رفضًا نظيفًا لا مستندًا مكتوبًا نصفًا

لماذا يكون الملء حقلًا حقلًا خطرًا لاستيراد البيانات؟

الملء حقلًا حقلًا خطر لأن كل كتابة ثبتت لوحدها، فخطأ في منتصف الحلقة يترك مستندًا لا يطابق سجل المصدر ولا القالب الأصلي. والنمط الكلاسيكي حلقة على إسنادات FormField[Index]، كل واحدة تمر عبر بيئة ملء النماذج الأصلية في PDFium widgetًا واحدًا في كل مرة. تهبط القيم من 1 إلى 17، وترفع 18، وعلى المستدعي الآن أن يعرف بالضبط أي القيم دخلت لكي يتراجع عنها

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

المشكلة الهادئة الأخرى التركيز: يقود الضابط الكلاسيكي محرر PDFium التفاعلي فيقتل أي تركيز قائم، ويركز widget الهدف، ويحدد كل النص، ويستبدله، ويقتل التركيز مجددًا. ذلك جيد لمهمة headless، لكن في viewer ينهي أيضًا أي جلسة تحرير فتحها المستخدم في حقل آخر. ومسار الكتابة المنمَّق الذي وصل في PDFium Component ‏3.231.0 لا يمر عبر تركيز الحقول الأصلي إطلاقًا، ووسع 3.233.0 نطاقه من الحقول المنفردة إلى دفعات عبر الصفحات

كيف يطبق SetFormFieldValues دفعة في استبدال واحد

يجهز TPdf.SetFormFieldValues كل طلب مقابل لقطة واحدة ملتقطة من المستند، ويكتب كل قواميس الحقول في تمريرة واحدة، ويسبدل النتيجة المنجزة باستبدال واحد. وتبقى الوثيقة الأصلية حية حتى الخطوة الأخيرة تمامًا، فأي فشل قبل تلك الخطوة يتركها دون مساس

تسلسل ملء دفعة ذرية بخمس مراحل في PDFium Component لـ Delphi، من preflight إلى التدقيق والاستبدال بنتائج كل شيء أو لا شيء
كل طلب يجهز مقابل لقطة واحدة، والوثيقة الحية لا تلمس إلا عند الاستبدال المدقق الأخير

التسلسل يشرح الضمان والتكلفة معًا:

  1. فحوص preflight تعمل أولًا: يجب أن تكون الوثيقة نشطة، وعدد الطلبات بين 1 و256، وألا تكون معاملة كائن صفحة أو دفتر تحرير مفتوحة، وألا يملك حقل أصلي التركيز، وأن تمنح الوثيقة إذن ملء نماذج أو تعديل
  2. كل طلب يتحقق منه لنطاق الصفحة وميزانية الدخل، ويتتبع FailedRequest الفهرس الذي يفحص
  3. يلتقط المكوّن لقطة تحرير، ويوحد AcroForm في تلك النسخة، ويطبق كل طلب بالترتيب، كاتبا /V و/I لاختيارات الاختيار و/AS لكل widget يتشارك الحقل
  4. تلحق التغييرات باللقطة كتحديث تزايدي نادر واحد بمعنى §7.5.6 من ISO 32000-1، فتنقل الكائنات غير المتصلة بايتًا بايت
  5. نسخة مدققة منفصلة تحمل بايتات المرشح أولًا؛ وفقط بعد أن تفتح نظيفة يفرغ المكوّن الوثيقة الحية ويحمل الجديدة، عائدًا إلى اللقطة الأصلية إن فشل ذلك التحميل

خطوة الاستبدال الداخلية هي الآلية نفسها التي يستخدمها المكوّن لـ snapshots دفتر تحريره، ولهذا لا يستطيع دفتر التراجع واسترداد ما بعد الانهيار أن يتداخل مع كتابة دفعة: أتم معاملة الدفتر أو ألغها أولًا. وتكبت إجراءات دورة الحياة الآلية بينما تسبدل الوثيقة، فلا يستطيع سكربت فتح مستند إعادة كتابة الحقول أو الاستيلاء على التركيز في منتصف الاستبدال

بناء دفعة طلبات منمَّقة في Delphi

الدفعة مصفوفة ديناميكية من سجلات TPdfFormValueRequest، كل سجل يحمل Page بترقيم من 1، وFieldIndex محليًا للصفحة بترقيم من 0، وTPdfFormValue منمَّقة. وسجل القيمة يحمل Kind من fvkText أو fvkChoice أو fvkCheckBox أو fvkRadioButton، مع أعضاء Text وChoiceIndices وChecked المستخدمة بحسب ذلك النوع

بيانات الاستيراد تصل عادة مفهرسة باسم الحقل، فأول مهمة ترجمة الأسماء إلى عناوين صفحات وwidgets. يعد FieldIndex تعليقات الـ widget على صفحة بالترتيب نفسه الذي يعد به FormFieldCount وFormFieldInfo، فمسح بسيط ينجح. ويأخذ المساعد أول widget يجده لاسم، وهو ما يهم للحقول المشتركة، كما يشرح القسم التالي

type
  TFieldAddress = record
    Page, FieldIndex: Integer;
  end;

function FindFieldAddress(Pdf: TPdf; const FieldName: WString;
  out Address: TFieldAddress): Boolean;
var
  P, I: Integer;
begin
  Result := False;
  for P := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := P;
    for I := 0 to Pdf.FormFieldCount - 1 do
      if Pdf.FormFieldInfo[I].Name = FieldName then
      begin
        Address.Page := P;
        Address.FieldIndex := I;
        Exit(True);      // أول widget فقط: طلب واحد لكل حقل
      end;
  end;
end;

procedure AddRequest(var Requests: TPdfFormValueRequests;
  const FieldName: WString; Pdf: TPdf; const Value: TPdfFormValue);
var
  Address: TFieldAddress;
  N: Integer;
begin
  if not FindFieldAddress(Pdf, FieldName, Address) then
    raise Exception.CreateFmt('Template has no field "%s"', [FieldName]);
  N := Length(Requests);
  SetLength(Requests, N + 1);
  Requests[N].Page := Address.Page;
  Requests[N].FieldIndex := Address.FieldIndex;
  Requests[N].Value := Value;
end;

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

var
  Pdf: TPdf;
  Requests: TPdfFormValueRequests;
  Report: TPdfFormValuesWriteReport;
  V: TPdfFormValue;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'claim-template.pdf';
    Pdf.Active := True;

    V := Default(TPdfFormValue);
    V.Kind := fvkText;
    V.Text := 'Maria Lopez';
    AddRequest(Requests, 'Claimant.Name', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkCheckBox;
    V.Checked := True;
    AddRequest(Requests, 'Consent', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkRadioButton;
    V.Text := 'Express';                // قيمة export للزر المختار
    AddRequest(Requests, 'Shipping', Pdf, V);

    V := Default(TPdfFormValue);
    V.Kind := fvkChoice;
    SetLength(V.ChoiceIndices, 1);
    V.ChoiceIndices[0] := 2;            // فهرس بترقيم من 0 داخل مصفوفة Opt للحقل
    AddRequest(Requests, 'Region', Pdf, V);

    if Pdf.SetFormFieldValues(Requests, Report) then
      Pdf.SaveAs('claim-filled.pdf')
    else
      LogBatchFailure(Requests, Report);
  finally
    Pdf.Free;
  end;
end;

تفحص الأنواع المنمَّقة مقابل نوع الحقل قبل كتابة أي شيء: قيمة نصية موجهة إلى checkbox، أو قيمة راديو موجهة إلى checkbox عادي، أو كتابة إلى زر ضغط، كلها ترفض. وفهارس الاختيار يجب أن تكون صالحة وفريدة وصاعدة؛ وأكثر من فهرس يتطلب علم MultiSelect، والنص الحر في حقل اختيار لا يقبله إلا combo box قابل للتحرير. وقيمة نصية أطول من MaxLen للحقل ترفض بدل أن تقطع، والحقول للقراءة فقط ترفض جملة. وللمجموعات الراديوية بعلم NoToggleToOff، قيمة export فارغة، أي "أفرغ المجموعة"، ترفض أيضًا

لماذا يفشل كتابة حقل مشترك مرتين الدفعة كلها؟

يرفض TPdf.SetFormFieldValues طلبين يحلان إلى الحقل الطرفي نفسه لأنهما سيكتبان قيمة واحدة كلتاهما، والمكوّن لا يسمح لطلب أن يكتب فوق آخر بصمت. ويعمل الفحص على مالك الحقل لا على الـ widget، فـ widget مختلفان لحقل واحد على صفحتين مختلفتين ما يزالان يعدان مكررًا

السبب نموذج الحقول في §12.7.3.1 من ISO 32000-1: الحقل الطرفي يملك القيمة، وتعليقات widget له مجرد مواضع عرض تلك القيمة. والحقل المسمى PolicyNumber الذي يظهر في ترويسة كل صفحة حقل واحد بعدة widgets، وكل واحد من تلك الـ widgets يعرض /V نفسها. والمجموعة الراديوية تعمل بالطريقة نفسها: كل زر widget، والمجموعة هي الحقل. وكتابة PolicyNumber عبر widget الصفحة 1 ثم عبر widget الصفحة 9 حقل واحد كتب مرتين، وأي قاعدة تختار فائزًا تخفي عيبًا في خطتك

فتعامل الدفعة معه خطأ وتسمي الطلب الثاني في FailedRequest. وعمليًا الإصلاح في المنبع: حلل كل اسم حقل إلى عنوان واحد بالضبط، كما يفعل FindFieldAddress، ودع المكوّن ينشر القيمة إلى الـ widgets الأخرى. ويفعل ذلك جزءًا من الكتابة، ضابطًا /AS على كل widget لـ checkbox مشترك أو مجموعة راديوية بحيث يوافق حالة المظهر قيمة الحقل، التقسيمة نفسها المغطاة في مقالة قيمة حقل checkbox مقابل حالة widget

حقل طرفي واحد يملك القيمة، وwidgets على صفحتين تعرضها، وطلب مكرر يفشل الدفعة في PDFium Component لـ Delphi
طلب واحد لكل حقل طرفي — يكتب المكوّن /V مرة ويضبط /AS على كل widget يتشاركه

قراءة تقرير الفشل

الدفعة الفاشلة تعيد False ولا ترفع، وتترك الوثيقة الأصلية وكل قيمة حقل كما كانت تمامًا. ويسجل TPdfFormValuesWriteReport بعدها ‏Succeeded = False، وصفرًا لـ UpdatedFields وUpdatedWidgets معًا، والفهرس بترقيم من 0 للطلب المخطئ في FailedRequest، وسببًا مقروءًا للبشر في ErrorMessage

procedure LogBatchFailure(const Requests: TPdfFormValueRequests;
  const Report: TPdfFormValuesWriteReport);
var
  R: Integer;
begin
  R := Report.FailedRequest;
  if R >= 0 then
    Log(Format('Request %d (page %d, widget %d) rejected: %s',
      [R, Requests[R].Page, Requests[R].FieldIndex, Report.ErrorMessage]))
  else
    Log('Batch rejected as a whole: ' + Report.ErrorMessage);
end;

‏FailedRequest بـ -1 يعني أن الرفض يعود إلى المعاملة لا إلى طلب: كان التركيز الأصلي نشطًا، أو الوثيقة موقعة، أو النموذج XFA، أو فشل فحص إذن. والقيمة غير السالبة تشير إلى إدخال واحد. وفي تجاوز ميزانية مجمعة تشير إلى الطلب الذي دفع الدفعة فوق الحد، وهو ليس بالضرورة الطلب الخطأ. احتفظ بمصفوفة موازية لأسماء أعمدة المصدر بجوار Requests فتحول الفهرس 14 مباشرة إلى "row 2087, column ShipDate" في log الاستيراد

عند النجاح، تساوي UpdatedFields عدد الطلبات ويعد UpdatedWidgets كل widget لمس، المشتركة منهم، فحص صيانة رخيص مقابل القالب

الشروط المسبقة والحدود التي ستصطدم بها

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

الميزانيات ثابتة وموثقة، فيمكنك تحجيم الاستيرادات مقابلها:

  • من 1 إلى 256 طلبًا لكل دفعة؛ قسّم السجلات الأكبر إلى عدة دفعات واقبل أن الذرية عندها تحفظ لكل دفعة
  • بحد أقصى 262,144 وحدة UTF-16 من النص و4,096 فهرس اختيار لكل طلب
  • بحد أقصى 1,048,576 وحدة UTF-16 من النص و16,384 فهرس اختيار عبر الدفعة كلها
  • ميزانيات المستند المعتادة لمحلل اللقطة ما تزال تنطبق، بما فيها مصدر 64 MiB و65,536 كائن

الاستبدال الناجح أيضًا يبطل المقبضات الأصلية والبحث والتحديدات المكتسبة قبل الاستدعاء، لأن المستند تحته جديد. وتحفظ تنشيط الصفحة والعرض ومؤشرات الحقول المنطقية؛ وكل ما خزنته مؤقتًا غير ذلك، مثل مقبضات التعليقات أو كائنات الصفحات، يجب جلبه من جديد

ما الفرق عن استيراد XFDF؟

كلا TPdf.ImportXFDF وTPdf.SetFormFieldValues يضعان بيانات في الحقول، لكن استيراد XFDF دمج متساهل بينما الدفعة معاملة صارمة: يطابق استيراد XFDF القيم بالحقول بالاسم، ويتخطى الأسماء التي لا يجدها، ويتجاهل المكررات، ويعيد كم إدخال طبق. ذلك صحيح تمامًا لدمج تعليقات مراجع، حيث ما زالت النتيجة الجزئية مفيدة

والدفعة تفعل العكس في كل نقطة: تعنون widgets صراحة، وترفض أهدافًا مجهولة أو خاطئة النمط، وتعامل المكررات أخطاء، ولا نتيجة جزئية لها. استخدم XFDF حين تكون البيانات ملف تبادل من أداة أخرى؛ واستخدم SetFormFieldValues حين يملك كودك الخطة بنفسه وتكون قائمة مطبقة نصفها عيبًا

مصفوفة قرار تقارن استيراد XFDF بدفعة SetFormFieldValues الذرية في PDFium Component لـ Delphi: التوجيه، والأهداف المجهولة والمكررة، والنتيجة، والأنسب
XFDF يدمج ملفات التبادل بتساهل بينما ترفض الدفعة الأسماء المجهولة والمكررات والنتائج الجزئية

ما لا يغطيه كتابة الدفعة

الدفعة ملف AcroForm محدود الحدود، وتقول لا بدل التخمين خارج ذلك الملف: نماذج XFA ترفض وتحتاج محررها المخصص. والوثائق الموقعة ترفض، لأن إعادة كتابة قواميس الحقول تحت توقيع قائم سير عمل توقيع لا عملية ملء؛ و snapshots المشفرة خارج الملف أيضًا. ومصفوفات export الأزرار المشوهة أو الملتبسة ترفض، وقيم export الراديو التي تطابق أكثر من زر ترفض ما لم تضبط المجموعة RadiosInUnison. وتوحد المعاملة أيضًا قواميس الحقول المباشرة قبل الكتابة، الحالة خلف قيم حقول تبدو أنها تتراجع

المظاهر تستحق جملة عناية: للنص المتغير وwidgets الاختيار، أسقطت الدفعة /AP القديمة بدل شحن مظهر يعرض القيمة القديمة، وأطفأت NeedAppearances بحيث لا يعيد PDFium بناء كل widget غير متصل في النموذج. ويعيد PDFium توليد المظاهر المفقودة حين يصير. وإن ذهب الـ PDF المملوء إلى خط طباعة صارم لا يركب مظاهر أبدًا، فصره أو سوّسه عبر PDFium أولًا، واختبر ذلك المسار بقوالبك الخاصة

‏SetFormFieldValues وسجلات القيم المنمَّقة يأتيان مع PDFium Component لـ Delphi وLazarus، حيث تسرد صفحة المنتج API ملء النماذج وAcroForm كاملًا مع عناصر التصيير والتحرير والعرض