مقال تقني

لماذا تتراجع قيم حقول نماذج PDF: الحقول المباشرة في Delphi

قيمة حقل نموذج PDF "تتراجع" حين يخزن الحقل قاموسًا مباشرًا: يرقى PDFium النسخة في مصفوفة /Annots للصفحة كائنًا غير مباشر، بينما يحفظ /Fields نسخة مباشرة ثانية. ويعالج PDFium Component لـ Delphi كتابات الحقول المنمَّقة بتوحيد المرجعين كليهما في مالك غير مباشر واحد قبل الكتابة

العرض عادة يقرأ كمنزل مسكون: تعيد الكتابة True، ويسلم القراءة التالية مباشرة القيمة القديمة. احفظ، وافتح من جديد، والقيمة القديمة ما تزال هناك. لم يرفع شيء، والـ PDF يبدو عاديًا تمامًا في كل viewer تملكه

العرض: كتابة ناجحة لا تثبت

نمط الفشل ضيق لكنه قابل للتكرار جدًا: لا يظهر إلا على نماذج كتبت قواميس حقولها لاصقة (ككائنات مباشرة) لا ككائنات غير مباشرة مرقمة. ومعظم أدوات التأليف تصدر حقولًا غير مباشرة، فيختبئ المشكلة شهورًا ثم يظهر على ملفات من مولد معين، غالبًا مصدّر يدوي الصنع يكتب /AcroForm كله لاصقًا

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

لماذا تتراجع قيمة حقل نموذج PDF بعد كتابة ناجحة؟

تتراجع قيمة حقل نموذج PDF لأن قاموسين منفصلين يدّعيان كلتاهما أنهما الحقل نفسه، والكتابة حدثت النسخة التي لا تشاور شجرة الحقول معها. وقيمة الحقل تسكن إدخال /V خاصته، ويعرف §12.7.3.1 من ISO 32000-1 أين يكون ذلك الإدخال معتمدًا: على قاموس الحقل المبلغ من /AcroForm /Fields، نازلًا عبر /Kids إلى الحقل الطرفي، وحسابًا للإدخالات القابلة للتوريث من /Parent

تسمح الفقرة نفسها بتسهيل هو سبب الشغب كله: حين يكون لحقل تعليق widget واحد بالضبط، يجوز دمج قاموس الحقل وقاموس تعليق الـ widget في قاموس واحد. وفي ملف حسن التشكيل يكون ذلك القاموس المدموج كائنًا غير مباشر واحدًا، مشارًا إليه من مكانين: من /Fields (أو /Kids لأب) ومن مصفوفة /Annots للصفحة. مرجعان، كائن واحد، /V واحدة

نسختان من قاموس واحد

ينص §7.3.10 من ISO 32000-1 صراحة أن الكائن غير المباشر وحده يملك هوية قابلة للمشاركة: القاموس المباشر بلا رقم كائن، فحين يكتب مولد القاموس المدموج للحقل/widget لاصقًا في /Fields و/Annots معًا، لا يحوي الملف كائنًا واحدًا أشير إليه مرتين، بل يحوي قاموسين متشابهي المظهر يصادف أن مفاتيحهما وقيمهما واحدة

يتسامح PDFium مع هذا، وهو بذلك يجعل الانشقاق دائمًا: حين يحتاج مقبضة للتعليق، يستطيع PDFium ترقية قاموس الـ widget المباشر في مصفوفة /Annots للصفحة كائنًا غير مباشر، لكن النسخة المباشرة المكافئة تبقى حيث كانت داخل /AcroForm /Fields (أو داخل /Kids للأب). ومن تلك اللحظة تحمل الوثيقة مالكين لحقل منطقي واحد. والكاتب الذي يحرر /V على تعليق الصفحة يحصل على نجاح حقيقي على مستوى القاموس. أما شجرة الحقول، وهي ما تعامله طبقة النماذج وكل قارئ لاحق مصدر الحقيقة، فلا ترى التغيير أبدًا

ولهذا تنجو القيمة القديمة من حفظ وإعادة فتح: سلسلت القيمة الجديدة بأمانة في كائن لا تملكه سلسلة الحقول

‏PDFium Component على Delphi: قاموسان مباشران يدّعيان حقلًا واحدًا، والكتابة تصيب التعليق المرقى وشجرة الحقول ما تزال تقرأ نسخة /Fields القديمة
سلسلت الكتابة القيمة الجديدة بأمانة في كائن لا تملكه سلسلة الحقول، فتنجو القيمة القديمة من حفظ وإعادة فتح

كيف يعيد PDFium Component مالك حقل واحد

يوحد PDFium Component ملكية الحقول المباشرة قبل كل كتابة منمَّقة: يرقى قواميس /AcroForm والحقول المباشرة كائنات غير مباشرة، ويربط كل widget مباشر بتعليق الصفحة المطابق له، ويعيد كتابة /Fields و/Kids و/Parent و/Annots بحيث يشير كل مرجع إلى الكائن غير المباشر نفسه. وفقط بعدها يغير /V، فيتشارك قيمة الحقل وwidget المختار والوثيقة المحفوظة مالكًا واحدًا

الجزء العسير الهوية: قاموسان مباشران بمحتوى متطابق لا يثبتان أنهما الـ widget نفسه؛ فالنموذج يمكن أن يحوي مشروعًا widgetين متشابهي المظهر. لذا يرفض الموحد التخمين، ويشترط اسم تعليق /NM صريحًا وفريدًا (§12.5.2 من ISO 32000-1) على كل widget مباشر. والقواعد التي يطبقها مسار الكتابة صارمة:

  • يحتاج widget مباشر /NM غير فارغة بحد أقصى 1024 وحدة UTF-16، ويجوز لتعليق صفحة واحد بالضبط أن يحمل الاسم نفسه
  • يجب أن تسلسل النسخة المباشرة وتعليق الصفحة بشكل متطابق سوى /Parent و/AP المعاد توليده من PDFium؛ وأي اختلاف آخر يرفض الكتابة
  • يفحص كلا رابطي الأب منفصلين، فـ /Parent صحيح على تعليق الصفحة لا يستطيع أن يخفي آخر متعارض على النسخة المباشرة الأصلية
  • ‏/Parent المفقود يعطى فقط حين تجعل ملكية شجرة الحقول الفريدة الجواب لا لبس فيه، وتعيد نسخ الأب المباشرة المكافئة ربطها إلى مالكها المرقى
  • أسماء الحقول المؤهلة كاملة المكررة، وأشجار الحقول الدائرية أو متعددة الملكية، وحقول التوقيع، ترفض قبل نشر أي شيء
  • التعليقات غير المتصلة، مثل links على الصفحة نفسها، وإدخالات /AcroForm غير المتصلة، تحفظ دون مساس

كما يعمل الموحد تحت ميزانيات صارمة: ‏4096 زيارة لشجرة الحقول أو صفحات أو أعضاء مصفوفة، وعمق تداخل 64، و256 كائن غير مباشر جديد، و1 MiB لكل قاموس مرقى، و4 MiB من البايتات المرقية إجمالًا، و128 MiB من بايتات القواميس المفحوصة، و4096 وحدة UTF-16 لكل اسم حقل كامل مع 262144 عبر النموذج. والملف العدائي أو المكسور يصطدم بميزانية ويفشل برسالة خطأ بدل أن يحول ملء نموذج إلى حجب خدمة

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

كيف يوحد PDFium Component لـ Delphi ملكية حقول AcroForm المباشرة: لقطة، وفحص هوية NM، وتوحيد مراجع، وكتابة قيمة، ونشر مدقق بمسارات رفض صريحة
ترقى قواميس الحقول المباشرة ويعاد ربطها لمالك غير مباشر واحد قبل تغيير /V، وترفض الملكية الملتبسة نظيفة

كتابة قيمة حقل مباشر والتحقق منها في Delphi

النمط الآمن: اكتب ثم اقرأ مرة أخرى عبر طبقة النماذج الأصلية، وعامل عدم التطابق فشلًا حتى حين أعادت الكتابة True. يعنون TPdf.SetFormFieldValue حقلًا بفهرس على الصفحة الحالية، ويأخذ TPdfFormValue منمَّقة ويملا TPdfFormValueWriteReport؛ وتقرأ TPdf.FormField[Index] القيمة مرة أخرى عبر طبقة نماذج PDFium التي تحل شجرة الحقول بدل الثقة بالـ widget

نمط الكتابة والتدقيق لحقول نماذج PDF في Delphi مع PDFium Component: ‏SetFormFieldValue منمَّقة، وقراءة عودة عبر FormField، ثم فحصا حفظ وإعادة فتح
إعادة True وحدها تثبت أن القاموس تغير — اقرأ مرة أخرى عبر شجرة الحقول واختبر حفظًا وإعادة فتح
uses
  System.SysUtils, PDFium;

procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
  const NewText: WString);
var
  Value: TPdfFormValue;
  Report: TPdfFormValueWriteReport;
begin
  Pdf.PageNumber := PageNo;          // يوجه SetFormFieldValue الصفحة الحالية
  Value := Default(TPdfFormValue);
  Value.Kind := fvkText;
  Value.Text := NewText;

  if not Pdf.SetFormFieldValue(FieldIndex, Value, Report) then
    raise Exception.CreateFmt('Field %d on page %d rejected: %s',
      [FieldIndex, PageNo, Report.ErrorMessage]);

  // دقق مقابل شجرة الحقول لا مقابل متغيرنا الخاص
  if Pdf.FormField[FieldIndex] <> NewText then
    raise Exception.CreateFmt('Field "%s" did not keep the new value',
      [Pdf.FormFieldInfo[FieldIndex].Name]);
end;

على ملف بملكية لا يمكن حلها، مثل widget مباشر بلا /NM، تعيد SetFormFieldValue ‏False، وتكون Report.UpdatedWidgets صفرًا وتسمي Report.ErrorMessage المشكلة، مثل "Direct widgets require an explicit unique NM identity". ذلك هو السلوك الذي تريده: رفض واضح بدل كتابة نصفية صامتة. وشرط مسبق آخر يستحق المعرفة: الكتابة المنمَّقة على التعليقات فقط ولا تكتسب تركيزًا أصليًا، فإذا كان محرر حقل أصلي ما يزال مركزًا رفضت المعاملة كلها قبل التقاط اللقطة؛ استدع Pdf.ClearFormFieldFocus بقصد أولًا بدل إعادة المحاولة في حلقة

لعدة حقول، خاصة عبر الصفحات، استخدم شكل الدفعة: يقبل TPdf.SetFormFieldValues من 1 إلى 256 طلبًا، ويطبقها كلها على مرشح واحد، وينشر الكل أو لا شيء. ويخبرك TPdfFormValuesWriteReport.FailedRequest أي طلب كسر الدفعة

procedure FillInvoiceHeader(Pdf: TPdf);
var
  Requests: TPdfFormValueRequests;
  Batch: TPdfFormValuesWriteReport;
begin
  SetLength(Requests, 2);
  Requests[0] := Default(TPdfFormValueRequest);
  Requests[0].Page := 1;
  Requests[0].FieldIndex := 0;
  Requests[0].Value.Kind := fvkText;
  Requests[0].Value.Text := 'ACME Tooling Inc.';

  Requests[1] := Default(TPdfFormValueRequest);
  Requests[1].Page := 2;
  Requests[1].FieldIndex := 0;
  Requests[1].Value.Kind := fvkText;
  Requests[1].Value.Text := '2026-10-27';

  if not Pdf.SetFormFieldValues(Requests, Batch) then
    raise Exception.CreateFmt('Request %d failed: %s',
      [Batch.FailedRequest, Batch.ErrorMessage]);
  // ‏Batch.UpdatedFields = 2؛ وBatch.UpdatedWidgets تعد كل widget متزامن
end;

طلبان يعنونان الحقل الطرفي نفسه يرفضان كمالك مكرر بدل فوز آخر كتابة، وتحفظ وثيقة المصدر. وقيم checkbox والراديو تتبع القواعد نفسها؛ والتقسيمة بين /V و/AS للأزرار مغطاة في قيمة حقل checkbox مقابل حالة widget في PDFium

إثبات أن القيمة تنجو من حفظ وإعادة فتح

قراءة العودة في الجلسة نفسها تثبت أن الوثيقة الحية متماسكة، والحفظ وإعادة الفتح تثبتان أن الملف كذلك، والفحص رخيص بما يكفي لإبقائه في اختبارات التكامل الخاصة بك

procedure SaveAndReopenCheck(Pdf: TPdf; const Target: string;
  PageNo, FieldIndex: Integer; const Expected: WString);
var
  Reopened: TPdf;
begin
  if not Pdf.SaveAs(Target, saNoIncremental, pvUnknown) then
    raise Exception.Create('Save failed');

  Reopened := TPdf.Create(nil);
  try
    Reopened.FileName := Target;
    Reopened.Active := True;
    Reopened.PageNumber := PageNo;
    if Reopened.FormField[FieldIndex] <> Expected then
      raise Exception.Create('Saved file lost the field value');
  finally
    Reopened.Free;
  end;
end;

مصيدتان أخريان على مستوى البايتات: Trim و/ID التذييلة

توحيد الحقول المباشرة يعني إعادة قراءة بايتات القواميس الخام، وهو ما يستحضر مصيدتين أصغر تعلقان بأي كود Delphi يلمس نحو PDF

الأولى هي SysUtils.Trim على بايتات PDF الخام المحفوظة في AnsiString: سلسلة literal مثل خيار اختيار يمكن مشروعًا أن تحوي نص UTF-16BE بعلامة ترتيب بايتات FE FF وبايتات NUL مضمَّنة. وفي Delphi يأخذ Unicode ‏SysUtils.Trim ‏string، فالاستدعاء يحول AnsiString إلى UTF-16 بصمت باستخدام صفحة كود ANSI النشطة ويعيد تحويل الناتج. وحسب تلك صفحة الكود، يمكن للجولة أن تعيد كتابة البايتات العالية، وتتلف الـ BOM، بل تزعج المحدد الذي يليه. والنهج الآمن يبقي محلل القواعد على البايتات: تخطَّ فراغات PDF بالموضع (يسرد §7.2.2 من ISO 32000-1 ‏NUL وtab وLF وFF وCR وspace) واقطع بـ Copy، ولا تفك إلا مرة واحدة بعد معرفة أن القيمة سلسلة نص PDF

// خطر على بايتات PDF الخام: ‎AnsiString -> string -> AnsiString ‏(W1057/W1058)
Token := Trim(Token);

// آمن بالبايتات: تخطَّ فراغات PDF بالموضع ثم اقطع
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
  Inc(P);
Token := Copy(Token, P, MaxInt);

المصيدة الثانية تجلس في التحديث التزايدي نفسه: يشترط §7.5.5 من ISO 32000-1 (الجدول 15) و§14.4 أن تكون /ID للتذييلة مصفوفة من سلسلتَي بايتات. والمساعد الداخلي الذي يركب معرف التحديث النادر يعيد محتوى السلسلتين فقط، بلا مفتاح /ID ولا أقواس المصفوفة، وعلى التذييلة أن تلتفه صريحًا كـ /ID [ ... ]. يخفي محمل PDFium المتساهل وإعادة حفظ كاملة لاحقة كلاهما غلافًا مفقودًا، فالفحص الموثوق تحليل ملفي المرشح والمحفوظ بمحلل صارم مستقل والبحث عن سلسلتَي 16 بايتة. وإن كتبت تحديثات تزايدية بنفسك فتنطبق المصيدة نفسها؛ ومقالة التحديث التزايدي لملفات PDF المشفرة تغطي سلسلة التذييلة بعمق أكبر

ماذا تفعل في كود نماذج Delphi الخاص بك؟

  • اقرأ القيمة مرة أخرى بـ FormField[Index] بعد كل كتابة دائمًا، وفي الاختبارات احفظ أيضًا وافتح من جديد واقرأ مرة أخرى
  • عامل False مع ErrorMessage معلومة: رفض يسمي NM أو تعارض أب أو اسمًا مكررًا يعني أن ملكية الملف ملتبسة حقًا
  • إن كنت تولد ملفات PDF، فأصدر الحقول والـ widgets كائنات غير مباشرة وأعط كل widget ‏/NM فريدة؛ فهذا يزيل الالتباس من المصدر
  • استخدم SetFormFieldValues لملء حقول متعددة أو عبر الصفحات بحيث لا يستطيع فشل متأخر ترك نصف النموذج محدثًا
  • لا تشغل Trim ولا مساعدات RTL الأخرى لـ string على بايتات PDF الخام المحفوظة في AnsiString أبدًا

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

يجلب PDFium Component كتابة النماذج المنمَّقة المعاملاتية هذه إلى تطبيقات Delphi وC++Builder وLazarus مع التصيير واستخراج النص وتحرير المستندات. وسجل الإصدارات والترخيص والتنزيلات على صفحة منتج PDFium Component لـ Delphi