في PDFium Component، مكوّن VCL/LCL المبني على PDFium لـDelphi وC++Builder وLazarus، فهرس حقل نموذج ليس فهرس تعليق توضيحي. تحمل الصفحة تعليقات Link وText وInk إلى جانب عناصر واجهتها widgets، فتعداد الحقول يجب أن يصفّي حسب FPDFAnnot_GetSubtype ويكشف فهرسًا منطقيًا مرقَّمًا من صفر، مُخطَّطًا مجددًا إلى موضع تعليق حقيقي فقط عند الاستدعاء الأصلي
العلّة التي تكشف هذا لا تُخطئ بمجرد أن تراها. يضغط مختبِر Tab في نموذج فاتورة مُعبَّأ ويختفي المؤشر، لأن التركيز ذهب إلى رابط تشعبي في التذييل. أو أسوأ، لا يحدث شيء على الإطلاق: يسجّل كودك الحقل 3 كمُركَّز عليه، وتُحدَّث لوحة واجهة المستخدم، بينما تُعيد FORM_SetFocusedAnnot بصمت القيمة false طوال الوقت. كلا العرَضين يأتيان من الخطأ التصميمي نفسه، وأحدهما له سبب جذري ثانٍ مختبئ تحته
فضاءا الفهرسة اللذان يمنحهما PDFium
يكشف PDFium نظامي ترقيم فوق الصفحة نفسها، ولا يتطابقان إلا على مستندات يصادف أنها لا تحتوي شيئًا سوى عناصر واجهة النموذج. الأول فهرس التعليق التوضيحي: موضع في مصفوفة /Annots الخاصة بالصفحة، وهو ما تعدّه FPDFPage_GetAnnotCount وما تأخذه FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). الثاني الفهرس المنطقي للحقل الذي ينبغي أن تعرضه واجهة برمجية على مستوى التطبيق، ممتدًا من صفر عبر الحقول التفاعلية التي يستطيع مستخدم الوصول إليها فعلًا. يعرّف ISO 32000-1 §12.5.6.19 تعليقات الواجهة widget annotations كالتمثيل البصري لحقول النموذج التفاعلية، ويعرّف §12.7 النموذج نفسه. كل شيء آخر على الصفحة نوع فرعي مختلف بدلالات مختلفة: تعليق Link له وجهة، وتعليق Ink له قائمة ضربات قلم، وتعليق Text ملاحظة لاصقة. لا شيء من هؤلاء ينتمي إلى عدّ حقول، ولا شيء منهم يستطيع قبول تركيز نموذج. ومع ذلك فهي تجلس في مصفوفة /Annots متداخلة مع عناصر الواجهة بأي ترتيب كتبه التطبيق المُنتِج، وهو غالبًا ليس الترتيب الذي يقترحه أي شيء آخر بشأن المستند
لماذا يهبط Tab على رابط تشعبي بدل الحقل التالي؟
لأن عدّ الحقول كان في الحقيقة عدّ تعليقات. التطبيق الأصلي أعاد FPDFPage_GetAnnotCount مباشرة من FormFieldCount، بينما مُستوصِل معلومات الحقل، ومساعد ترتيب الجولان tab order، ومساعد التركيز كلها عاملت العدد الصحيح نفسه كموضع عنصر واجهة. على صفحة AcroForm نظيفة بستة عناصر واجهة ولا شيء غيرها، ستة تساوي ستة وينجح كل اختبار. أضف رابطًا تشعبيًا في التذييل وتعليق مراجع في الهامش، ويُبلغ العدّ عن ثمانية حقول، وتُحَل الفهارس 6 و7 إلى كائنات غير نموذجية، ويسير Tab مباشرة داخلها
الإصلاح في طرف التعداد هو عدّ الأنواع الفرعية لا التعليقات. افتح كل تعليق، اسأل عن نوعه الفرعي، أبقِ عناصر الواجهة، وأغلق المقبض داخل كتلة finally، لأن FPDFPage_GetAnnot تعيد مقبضًا مملوكًا يجب إعادته عبر FPDFPage_CloseAnnot
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
لاحظ ما لا يفعله هذا عمدًا. لا يسأل بيئة تعبئة النموذج شيئًا، ولا يحتاج مقبض نموذج، لأن النوع الفرعي يعيش في قاموس التعليق التوضيحي وقابل للقراءة من الصفحة وحدها. هذا يهم للترتيب: العدّ متاح قبل أن تقرر ما إذا كان المستند يستحق أصلًا بيئة تعبئة نموذج، وهو ما يغطيه المقال عن JavaScript في AcroForm وأحداث المضيف كقرار أمني لا قرار راحة
تخطيط الفهرس المنطقي مجددًا عند الحدود الأصلية
القاعدة التي تمنع الفضاءين من التسرب أحدهما إلى الآخر بسيطة: الفهرس المنطقي هو الرقم الوحيد الذي يعبر واجهتك البرمجية العامة، ويُحوَّل إلى فهرس تعليق توضيحي في آخر دالة قبل الاستدعاء الأصلي. مساعد تخطيط واحد، تستخدمه معلومات الحقل والتركيز ومحددات الأعلام flag setters وترتيب الجولان على حد سواء، هو ما يجعل تلك القاعدة قابلة للفرض
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
خاصيتان لهذا المساعد تستحقان الذكر الصريح. إنه فحص خطي، فحلقة ساذجة فوق كل حقل تكلّف عددًا تربيعيًا من عمليات فتح تعليقات على صفحة بمئات عناصر الواجهة؛ إن كنت تعدّد الصفحة بأكملها، اجتز التعليقات مرة واحدة واجمع مقابض عناصر الواجهة أثناء المسير بدل استدعاء المُخطِّط لكل حقل. وهو يعيد -1 بدل رفع استثناء، ما يترك للمستدعي أن يقرر ما إذا كان فهرس قديم خطأ برمجيًا يستحق استثناءً أو سباقًا يستحق التجاهل، مثلًا بعد تعديل حذف تعليقًا ما تزال قائمة واجهة مستخدم مخزَّنة مؤقتًا تشير إليه
لماذا تفشل FORM_SetFocusedAnnot على صفحة بلا رأس؟
لأن PDFium يرفض تركيز عنصر واجهة لم يُعلَّم عرض صفحته page view صالحًا قط. تحلّ FORM_SetFocusedAnnot التعليق التوضيحي إلى عرض صفحة داخل بيئة تعبئة النموذج، وإن لم يكن ذلك العرض موجودًا تعيد false دون أي تشخيص. تصحيح تخطيط الفهرس وحده يصلح إذن هبوط Tab على رابط تشعبي لكن يترك العرَض الثاني دون مساس: سجلّ تركيزك المنطقي يقول حقل 3، وعنصر الواجهة المُركَّز عليه أصليًا ما يزال لا شيء، وكل مُستوصِل accessor مبني على التركيز الأصلي، النص المُركَّز عليه، القيمة المُركَّز عليها، حالة اختيار قائمة، يستمر بإعادة قيمة فارغة. عرض الصفحة يُنشَأ بـFORM_OnAfterLoadPage ويُتلَف بـFORM_OnBeforeClosePage. في عارض مبني حول عنصر تحكم بصري تحدث تلك الاستدعاءات كجزء من عرض صفحة، وهذا سبب أن الفشل غالبًا ما يبدو كعلّة حصرية بلا رأس فقط: الكود نفسه الذي يعمل في عرض GUI التوضيحي يفشل في أداة الدفعة batch. دورة الحياة تخص كائن المستند، لا العارض، فيُصدر PDFium Component الآن كلا الاستدعاءين كلما حُمِّلت صفحة أو أُلغي تحميلها مع وجود مقبض نموذج. توقيع C يأخذ الصفحة أولًا ومقبض النموذج ثانيًا، وهذا سهل الانعكاس عند كتابة الربط binding يدويًا
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
الفحص الذي يثبت الإصلاح هو الذي يقارن الجانبين. استدعِ FocusFormField بفهرس منطقي، ثم اقرأ قيمة عبر مُستوصِل يمر عبر عنصر الواجهة المُركَّز عليه أصليًا لا عبر سجلّك الخاص، مثل FocusedFormFieldValue أو FocusedFormOptionSelected. إن كان الفهرس المنطقي يذهب ويعود round-trip لكن المُستوصِل الأصلي يعيد قيمة فارغة، فإن عرض الصفحة مفقود، لا التخطيط
ما الذي لا يعده فهرس الحقل المنطقي
فهرس حقل مرقَّم من صفر راحة، لا هوية دلالية، وأربعة حدود تتبع ذلك. إنه لكل صفحة، لا لكل مستند، فالفهرس 0 على الصفحة 2 عنصر واجهة مختلف عن الفهرس 0 على الصفحة 1 ومقارنتهما بلا معنى. إنه موضعي، فإدراج تعليق توضيحي أو حذفه يبطل كل فهرس مخزَّن مؤقتًا فوق التغيير؛ عامل فهرسًا مخزَّنًا كصالح فقط طالما بقيت الصفحة محمَّلة وغير مُعدَّلة
الحد الثالث هو ما يفاجئ مراجعي قائمة حقول. الفهرس يُعدِّد عناصر الواجهة، لا الحقول. مجموعة أزرار اختيار من متعدد حقل واحد بعدة عناصر واجهة تابعة kids، فمجموعة من ثلاثة أزرار تسهم بثلاثة فهارس متتالية تُبلغ جميعها عن Name نفسه. يحمل سجلّ TPdfFormFieldInfo حقلي GroupCount وGroupIndex لهذه الحالة بالضبط، وواجهة مستخدم قائمة تتجاهلهما تُظهر الحقل نفسه ثلاث مرات. الحد الرابع يخص ترتيب الاجتياز traversal order: ترتيب الجولان المكشوف هنا هو ترتيب تعداد عناصر الواجهة، الذي يتبع مصفوفة /Annots، لا مدخل /Tabs الخاص بالصفحة (ISO 32000-1 §7.7.3.3) ولا شجرة حقول AcroForm. لمعظم المنتِجين يتفقان؛ لنموذج مُخطَّط في عمودين بواسطة مولِّد أصدر العمود الأيمن أولًا، لا يتفقان، ومسار لوحة المفاتيح الموصوف في مقال التنقل بين حقول النماذج سيبدو خاطئًا رغم أن كل فهرس صحيح. حين يتصرف ملف عميل بغرابة، اطرح كلا فضاءي الفهرسة جنبًا إلى جنب قبل التنظير: عرض التعليق وعرض الحقل للصفحة نفسها، مطبوعين معًا، عادة يجعلان السبب واضحًا بنظرة واحدة
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
عدّ تعليقات أعلى بكثير من عدّ الحقول يعني أن الصفحة تخلط أنواعًا فرعية، وهذا طبيعي في مستندات مُراجَعة وهو بالضبط الموقف الذي وُجد التخطيط لأجله؛ ينظر مقال سير عمل مراجعة التعليقات إلى الصفحة نفسها من جانب الترميز markup. الأعداد المتساوية على كل ملف اختبار، من جهة أخرى، تعني أن تجهيزاتك fixtures لا تستطيع اكتشاف هذا الصنف من العلل على الإطلاق، والاستجابة الصادقة هي إضافة تجهيزة نموذج تحمل رابطًا وملاحظة لاصقة
تعداد الحقول والتركيز وواجهات برمجة التعليقات الموصوفة هنا تُشحن مع PDFium Component لـDelphi وC++Builder وLazarus، وتحمل صفحة المنتج المرجع الكامل لحقل النموذج بما في ذلك سجلّ معلومات الحقل ومُستوصِلات التركيز