مقال تقني

علّة تسطيح مربع اختيار PDF: قيمة الحقل مقابل عنصر الواجهة في Delphi

تُسطَّح مربعات الاختيار وأزرار الاختيار من متعدد كغير محدَّدة لأن حالة المظهر /AS لم تُزامَن قط مع قيمة الحقل /V. مكوّن PDFium Component، مكوّن VCL وLCL المبني على PDFium لـDelphi وC++Builder وLazarus، يقرأ الآن تلك القيمة بـFPDFAnnot_GetFormFieldValue، الذي يحلّ قاموس الحقل الوالد بدل تعليق الواجهة

تقرير العلّة الذي قاد إلى هذا هو النوع الذي تشك فيه أولًا. يُسطِّح عميل نموذج موافقة موقَّعًا، ويفتح النتيجة، وكل مربع اختيار فارغ. افتح الملف المصدر في Acrobat وستجد المربعات محدَّدة بوضوح. اقرأ الملف المصدر مجددًا عبر المكوّن نفسه وقيم الحقول صحيحة. المُخرَج المُسطَّح فقط يفقدها، وفقط لمربعات الاختيار وأزرار الاختيار من متعدد: حقول النص على الصفحة نفسها تخرج سليمة

لماذا مربعات الاختيار غير محدَّدة بعد التسطيح؟

لأن التسطيح لا ينظر إلى /V أبدًا. يخبز FPDFPage_Flatten تدفق مظهر الواجهة في محتوى الصفحة، والمظهر الذي يختاره هو الذي يسمّيه /AS. إن كانت /AS ما تزال تقول /Off بينما تقول قيمة الحقل أن المربع محدَّد، فإن التسطيح يخبز بأمانة المظهر غير المحدَّد. القيمة لم تُفقَد قط؛ لم تُستشَر قط

يعرّف ISO 32000-1 §12.5.5 قاموس المظهر /AP بثلاثة مدخلات ممكنة، /N و/R و/D. لمربع اختيار أو زر اختيار من متعدد، مدخل /N ليس تدفقًا بل قاموسًا فرعيًا مفاتيحه أسماء حالات مظهر، ويجعل §12.5.2 من /AS المُحدِّد المطلوب حين يكون /N قاموسًا فرعيًا. فمربع الاختيار يحمل مظهرين مبنيين مسبقًا ومؤشرًا واحدًا. أخطئ المؤشر ويكون العرض خاطئًا بطريقة لا يصلحها أي قدر من /V الصحيحة. هذا أيضًا سبب اختلاف نمط الفشل عن حقول النص، التي لا تملك أي مظهر مبني مسبقًا لتختاره على الإطلاق: /N لحقل نص تدفق واحد يجب إعادة توليده من الصفر بعد تغيّر القيمة، فتعالج GenerateFormAppearances الحالتين عبر مسارات كود منفصلة كليًا وكان مسار الأزرار وحده المكسور

أين تعيش قيمة مربع الاختيار فعلًا؟

على قاموس الحقل، لا على عنصر الواجهة widget. يصف ISO 32000-1 §12.7.5.2 مربعات الاختيار وأزرار الاختيار من متعدد كحقول أزرار تكون /V فيها كائن اسم يسمّي حالة المظهر الحالية، ويضع §12.7.3.1 مدخل /V بين المدخلات المشتركة لكل قواميس الحقول. تعليق الواجهة المعرَّف في §12.5.6.19 يسهم بـ/AS و/AP. لا شيء في المواصفة يُلزم عنصر واجهة بحمل /V

// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off

{ What the two objects look like when the field has several widgets:

  12 0 obj                          % field dictionary (the parent)
  << /FT /Btn  /T (Consent)  /V /On
     /Kids [ 13 0 R 14 0 R ] >>
  endobj

  13 0 obj                          % widget annotation (a kid)
  << /Type /Annot  /Subtype /Widget  /Parent 12 0 R
     /AS /Off
     /AP << /N << /On 20 0 R  /Off 21 0 R >> >> >>
  endobj }

FPDFAnnot_GetStringValue ليست معطوبة. عقدها هو بالضبط ما يقوله اسمها: جلب مدخل سلسلة نصية من قاموس التعليق التوضيحي الذي سلّمته له. سؤالها عن /V على الكائن 13 يعيد لا شيء لأن الكائن 13 لا يملك فعلًا /V. العيب كان في المستدعي، الذي افترض نموذج كائنات مسطَّحًا لم يعده ISO 32000-1 قط

متى يشترك الحقل وعنصر الواجهة في قاموس واحد؟

كلما امتلك حقل عنصر واجهة واحدًا بالضبط. يسمح §12.5.6.19 بدمج قاموس الحقل وتعليق واجهته الوحيد في كائن واحد، وتأخذ معظم أدوات التأليف ذلك الاختصار. في كائن مدموج تجلس /FT و/T و/V و/AS و/AP جنبًا إلى جنب، فقراءة /V على مستوى الواجهة تنجح وتبقى العلّة بأكملها غير مرئية

في اللحظة التي يملك فيها حقل عنصري واجهة أو أكثر يصبح الدمج مستحيلًا، ويفرض §12.7.3.1 أن تصبح عناصر الواجهة /Kids لقاموس حقل منفصل. كل مجموعة أزرار اختيار من متعدد على هذا الشكل بالبناء. كذلك مربعات موافقة مكرَّرة في ترويسة وتذييل، وأي حقل نسخته أداة تأليف إلى صفحة ثانية. هذا هو التفسير الكامل لسبب نجاة العلّة عبر مجموعة اختبارات انحدار: مجموعة الاختبار كانت مليئة بنماذج عنصر واجهة واحد، وملفات العميل لم تكن كذلك. إن كنت تجتاز عناصر الواجهة بنفسك بدل الاعتماد على المكوّن، فإن عدم التماثل نفسه يظهر في ترتيب التعداد، وتغطي ملاحظات التنقل بين حقول نماذج PDF مع PDFium Component كيف يرتبط اجتياز تعليق توضيحي على مستوى الصفحة بشجرة الحقول على مستوى المستند

قراءة القيمة بالطريقة التي يقصدها PDFium

FPDFAnnot_GetFormFieldValue هي واجهة برمجة التطبيقات الصحيحة، وكانت مربوطة في المكوّن منذ فترة دون أن يستخدمها مسار مربع الاختيار. تأخذ مقبض النموذج form handle إضافة إلى تعليق الواجهة، وهذه هي الإشارة المهمة: مع توفر بيئة تعبئة النموذج، يحلّ PDFium تعليق الواجهة إلى عنصر تحكم النموذج الخاص به ويقرأ القيمة من كائن الحقل، فيعيد الإجابة الصحيحة لتخطيطات مدموجة ومنفصلة على حد سواء

FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
  begin
    // /AP is prebuilt per state; only /AS has to be synchronised with /V.
    // FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
    // which is where ISO 32000-1 12.7.5.2 keeps the value.
    buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
    if buflen >= 4 then
    begin
      SetLength(OrigVal, buflen div 2 - 1);
      FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
      FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
    end;
  end;

تفصيلان في ذلك المقتطف يسهل إخطاؤهما. الطول المُعاد عدد بايتات لنص UTF-16 متضمنًا المُنهي terminator، فعدد المحارف هو buflen div 2 - 1 وقيمة 2 تعني سلسلة فارغة. الحارس buflen >= 4 يعني إذن حرفًا حقيقيًا واحدًا على الأقل، وهذا ما يمنع الكتابة فوق /AS بحقل بلا /V على الإطلاق باسم فارغ

على ماذا يتفق /AS و/AP /N فعلًا

يتفقان على اسم، ومن يختار الاسم هو من أنتج الملف. يفرض §12.7.5.2 تسمية الحالة غير المحدَّدة /Off، ويترك الحالة المحدَّدة كليًا للمنتِج. /Yes اتفاقية، لا قاعدة. يكتب Acrobat /Yes، لكن كثيرًا من المولِّدات تكتب /On أو /1 أو /Choice1 أو كلمة محلَّية، ومجموعة أزرار اختيار من متعدد تعطي عادة كل عنصر واجهة فيها اسم حالة-محدَّدة مميزًا بحيث يمكن للمجموعة أن تعبّر عن أي زر مُختار. هذا بالضبط سبب أن نسخ /V حرفيًا إلى /AS هو العملية الصحيحة لا حيلة: لعنصر تحكم محدَّد يُبلغ PDFium عن اسم الحالة-المحدَّدة الذي يعرّفه الملف نفسه، وللواحد غير المحدَّد يُبلغ عن Off، فالقيمة التي تكتبها في /AS مضمونة كونها مفتاحًا موجودًا في قاموس /AP /N الفرعي الخاص بعنصر الواجهة ذاك. تثبيت /Yes بالكود hard-coding كان سينجح على مُخرَج Acrobat وينكسر بصمت في كل مكان آخر

ترتيب العمليات، وأين ما يزال يحتاج عناية

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

Pdf.FileName := FormPath;
Pdf.FormFill := True;          // required: FormHandle must exist
Pdf.Active := True;

Pdf.FormField[0] := 'On';      // writes /V only

Pdf.GenerateFormAppearances;   // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
  Pdf.SaveAs('consent-flat.pdf');

يبقى حدّان صادقان. أولًا، تكتب المزامنة قيمة الحقل في /AS الخاصة بكل عنصر واجهة لذلك الحقل، وهذا صحيح لمربعات الاختيار لكنه تقريبي لمجموعات أزرار الاختيار من متعدد التي يعرّف كل عنصر واجهة فيها اسم حالة-محدَّدة خاصًا به؛ عنصر واجهة لا يملك /AP /N فيه مدخلًا يطابق /AS المكتوبة ليس له مظهر يختاره تحت §12.5.5، فزر غير محدَّد يمكن أن يُسطَّح إلى لا شيء بدل دائرة فارغة. تدقيق مجموعة أزرار اختيار من متعدد بـFPDFAnnot_GetFormControlIndex قبل التسطيح يستحق الأسطر القليلة. ثانيًا، لا شيء من هذا ينطبق على XFA، حيث تعيش القيمة في حزمة بيانات XML بدل قواميس AcroForm، وهو فصل مشروح في ملاحظات تعديلات حقل XFA التي لا تُحفَظ. الدرس العام يستحق الاحتفاظ به بعد هذا الإصلاح: كلما أخذت واجهة برمجية مقبض النموذج إضافة إلى تعليق الواجهة، فهي تخبرك أنها ستحلّ التسلسل الهرمي للحقل نيابة عنك، وكلما أخذت تعليق الواجهة فقط ستقرأ بالضبط الكائن الذي مررته. ذلك التمييز يحكم أيضًا تبادل البيانات، إذ يعمل تصدير واستيراد بيانات نموذج XFDF بأسماء حقول مؤهَّلة كاملة، لا بمواضع عناصر الواجهة أبدًا

تسطيح النماذج واحدة من تلك السمات التي تبدو كاستدعاء واجهة برمجية واحد وتتضح أنها عقد بين ثلاثة قواميس. إن كنت تفضّل العمل مقابل مكوّن يشفّر ذلك العقد بالفعل، فإن PDFium Component لـDelphi وC++Builder يشحن إعادة توليد المظهر والتسطيح والوصول إلى حقول النماذج الموصوفة هنا كخصائص وطرق عادية