مقال تقني

ترتيب Tab لحقول النماذج وسياسة /Tabs في PDFium Component

انطلاقًا من الإصدار 3.223.0، يرتب PDFium Component لـ Delphi التنقل بين حقول النماذج وفق سياسة /Tabs المعلنة لكل صفحة: ‏R للترتيب الصفي، وC لترتيب الأعمدة، وS لترتيب البنية، إضافة إلى قيمتي A وW في PDF 2.0. ويعيد TPdf.FormFieldTabOrder ذلك الترتيب، ونسخته ذات الحالة تسجل هل طُبقت السياسة المعلنة أو استخدم بديل

الشكوى التي تحرك هذا كله واحدة دائمًا: مستخدم يملأ نموذج تأمين بعمودين، ويضغط Tab بعد حقل الاسم الأول، فيقفز المؤشر إلى أسفل العمود الأيمن. لا شيء مكسور في الفهارس: كتب المنتج ببساطة العمود الأيمن في /Annots أولًا، وأعلن /Tabs /R على الصفحة لتثبيت الترتيب البصري، فـ viewer يمشي /Annots ويتجاهل الإعلان يهبط في الموضع الخطأ. ومقالة فهرس widget مقابل فهرس التعليق تغطي كيف ترتبط أرقام الـ widgets بمصفوفة /Annots. وفي PDFium Component تفوز السياسة المعلنة كلما أمكن إثباتها، وحين لا يمكن يقول لك المكوّن لماذا

ماذا يعلن إدخال /Tabs فعلًا؟

‏/Tabs اسم اختياري في كائن الصفحة (‏§7.7.3.3 من ISO 32000-2؛ الجدول 30 في ISO 32000-1) يخبر معالج بأي ترتيب يزور التعليقات على تلك الصفحة، ودلالته مفصلة تحت التعليقات في §12.5.1. أدخل PDF 1.5 ثلاث قيم، وأضاف PDF 2.0 اثنتين:

  • ‏/R، ترتيب صفوف: الصفوف من أعلى إلى أسفل، وداخل كل صف اتجاه قراءة الـ viewer
  • ‏/C، ترتيب أعمدة: الأعمدة باتجاه القراءة، ومن أعلى إلى أسفل داخل كل عمود
  • ‏/S، ترتيب بنية: الترتيب الذي تشير به شجرة البنية المنطقية إلى التعليقات
  • ‏/A، ترتيب مصفوفة التعليقات: ترتيب /Annots نفسها (PDF 2.0)
  • ‏/W، ترتيب widgets: الـ widgets أولًا بترتيب المصفوفة، ثم التعليقات الأخرى (PDF 2.0)

التفصيلة التي تعثر التنفيذات المكتوبة يدويًا هي التوريث: ‏/Tabs تعود لقاموس الصفحة وحده. يحصر §7.7.3.4 من ISO 32000-2 سمات الصفحة القابلة للتوريث في Resources وMediaBox وCropBox وRotate، فمفتاح /Tabs جالس على عقدة /Pages بلا معنى، وقارئ يصعد سلسلة الآباء بحثًا عن واحد يبتكر سياسة لم يضبطها المؤلف أبدًا. ويقرأ PDFium Component ‏/Tabs من الصفحة نفسها ومن لا مكان آخر

قراءة الترتيب المعلن عبر FormFieldTabOrder

يعيد TPdf.FormFieldTabOrder حقول نموذج الصفحة الحالية أرقامًا للـ widgets بترقيم من 0 بترتيب Tab المعلن، والنسخة ذات معامل out Status: TPdfFormTabOrderStatus تشرح كيف أنتج ذلك الترتيب. والزوج نفسه موجود على TPdfView، والنسخة الصافية تستخدم السياسة المماثلة مهملة الحالة

uses
  PDFium;

const
  ModeNames: array[TPdfFormTabOrderMode] of string =
    ('unspecified', 'R', 'C', 'S', 'A', 'W', 'unsupported');

procedure DumpTabOrder(const FileName: string; PageNo: Integer);
var
  Pdf: TPdf;
  Order: TPdfIntegerArray;
  Status: TPdfFormTabOrderStatus;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.FormFill := True;          // هي الافتراضي أصلًا؛ مطلوبة للحقول
    Pdf.Active := True;
    if not Pdf.Active or (Pdf.FormType <> ftAcroForm) then
      Exit;
    Pdf.PageNumber := PageNo;
    Order := Pdf.FormFieldTabOrder(Status);
    Writeln('requested ', ModeNames[Status.RequestedMode],
      ', applied ', ModeNames[Status.EffectiveMode]);
    if Status.UsedFallback then
      Writeln('fallback: ', Status.ErrorMessage);
    if Status.UnstructuredWidgetCount > 0 then
      Writeln(Status.UnstructuredWidgetCount, ' widgets not in the structure tree');
    for I := 0 to High(Order) do
      Writeln(I, ': field ', Order[I], ' ',
        string(Pdf.FormFieldInfo[Order[I]].Name));
  finally
    Pdf.Free;
  end;
end;

سجل الحالة هو ما يحول قرار ترتيب صامتًا إلى شيء تستطيع تسجيله: ‏RequestedMode ما أعلنته الصفقة، وEffectiveMode ما طُبق، وUsedFallback true حين تعذّر الوفاء بالسياسة المعلنة وعاد ترتيب مصفوفة التعليقات بدلًا، مع سبب قصير في ErrorMessage. وتقول SnapshotAvailable هل استطاع المكوّن قراءة البيانات الوصفية للمستند أصلًا. والـ /Tabs المفقودة تسجل بديلًا إلى ترتيب مصفوفة التعليقات لا تمرر سياسة معلنة، فيستطيع log الدعم تمييز "طلب المؤلف هذا الترتيب" عن "لم تطلب الصفقة شيئًا"

قيم سياسة /Tabs للصفحة R وC وS وA وW مع حقول الحالة التي يسجلها PDFium Component لـ Delphi حين يطبق الترتيب المعلن أو يعود للبديل
الـ /Tabs المفقودة بديل مسجل إلى ترتيب مصفوفة التعليقات، لا سياسة معلنة تمرر كأنها واحدة

لماذا لا يستطيع ترتيب الصفوف والأعمدة مجرد فرز قيم /Rect؟

يجب حساب ترتيب الصفوف والأعمدة في هندسة العرض لا إحداثيات التعليقات الخام، لأن /Rotate للصفقة واتجاه قراءة المستند كلاهما يغيران معنى "صف": فرز مصفوفات /Rect لصفقة بـ /Rotate 90 بحسب إحداثي y يعطيك ما يعرضه الـ viewer كأعمدة. لذا يخط كل مستطيل widget عبر تحويل عرض الصفقة، دورانها الذاتي شاملًا، بدقة 1/64 نقطة، ويجمع من هناك

اتجاه القراءة يأتي من قاموس ViewerPreferences في الفهرس (‏§12.2 من ISO 32000-2): يعكس /Direction /R2L الترتيب داخل الصف ويعكس ترتيب الأعمدة، وهو ما يتوقعه نموذج عربي أو عبري. وحين تكون /Direction غائبة ينطبق L2R. وحين تحمل شيئًا سوى L2R أو R2L، يعود ترتيب الصفوف والأعمدة إلى ترتيب مصفوفة التعليقات مع السبب في الحالة، لأن تخمين اتجاه أسوأ من الاعتراف بعدم وجوده

التجميع موضع الاختلاف المشروع بين المعالجات، فيستحق معرفة القاعدة الدقيقة: في ترتيب الصفوف، يصير أعلى widget لم يزر مرساة، وكل widget يسقط مركزه الرأسي داخل أعلى المرساة وأسفلها ينضم لصفها. ويعكس ترتيب الأعمدة هذا أفقيًا. وتحسم التعادلات حتمًا على رقم الـ widget. وعلى شبكة نظيفة يوافق ذلك أي viewer معقول. وعلى تخطيط بحقول متداخلة بشدة أو متدرجة، قد يجمع معالج آخر بتسامح مختلف جمعًا مختلفًا، ولا جزء من ISO 32000 يجعل أيًا من الإجابتين خاطئًا

‏PDFium Component على Delphi: ترتيب Tab لمصفوفة التعليقات يقفز أسفل العمود الأيمن بينما يزور ترتيب الصفوف المعلن /Tabs R نموذج العمودين صفًا صفًا
يحسب ترتيب الصفوف في هندسة العرض بعد /Rotate واتجاه القراءة، لا في ترتيب /Annots الخام

كيف يقرر ترتيب البنية التسلسل؟

يتبع ترتيب البنية قواميس مرجع الكائن (OBJR) في شجرة البنية، بالترتيب الذي تبلغه فعليًا مشية عمق-أولًا لمصفوفات /K: لا يتبع ترتيب مفاتيح /ParentTree، ولا يتبع أي ترتيب يصادف أن تعداد StructTreeRoot ينتجه. وغالبًا ما يتطابق تلك الترتيبات في ملفات من منتج واحد وتتباعد بعد أن يعيد محرر ترتيب الشجرة، وهو بالضبط متى يهم ترتيب قراءة نموذج موسوم أكثر. وإن كنت تصون تلك الشجرة بنفسك، فـ دليل إبقاء /ParentTree و/StructParents متزامنين يغطي الخطة العكسية التي يجب أن تبقى متسقة معها

الـ widgets التي لا تشير إليها شجرة البنية قط يتركها المعالج المواصفة: يلحقها PDFium Component بعد الـ widgets المهيكلة بترتيب مصفوفة التعليقات ويسجل كم ألحقها في UnstructuredWidgetCount. والصفقة التي تعلن /S على مستند بلا شجرة بنية على الإطلاق تعامل مشية فارغة: كل widget يهبط في ذلك الذيل، والعدد يقول ذلك، والناتج لا يوسم بديلًا كاملًا. وشجرة بنية بحلقة، أو OBJR أشير إليه مرتين، أو OBJR تناقض /Pg خاصته الصفقة التي تملك التعليق، تجعل ترتيب البنية يعود للبديل. تلك العيوب تجعل S غير قابلة للاستخدام، لكنها لا تزعج R أو C على صفحات أخرى

لماذا ليست قيم ترتيب Tab فهارس تعليقات؟

القيم في المصفوفة التي يعيدها FormFieldTabOrder تعد الـ widgets وحدها، بينما يحدد OBJR كائن تعليق يسكن مصفوفة /Annots الكاملة. وخلط فضاءي الفهرس هذين هو أشيع عيب واحد في أي كود يستهلك ترتيب البنية: خذ صفقة تكون /Annots خاصتها [Link, Widget A, Widget B]. يشير OBJR الخاص بـ Widget B إلى فهرس تعليق 2، وWidget B هي رقم الحقل 1. أطع 2 إلى FocusFormField فتركز حقلًا خطأ أو لا شيء أصلًا، بحسب كم widget تملكه الصفقة

يوجه OBJR البنيوي فهرس التعليق 2 بينما تعد APIs نماذج PDFium Component على Delphi الـ widgets، فـ Widget B هي رقم الحقل 1
فهرس التعليق 2 ليس رقم الحقل 1 — حوّل صراحة قبل استدعاء FocusFormField

ينفذ المكوّن ذلك التحويل داخليًا، ولهذا يستطيع ناتجه الدخول مباشرة إلى FocusFormField وFormFieldInfo. وإن كان كودك يقرأ أهداف OBJR أو فهارس تعليقات أخرى، فحولها صراحة قبل معاملتها حقولًا:

// يحول فهرس تعليق صفقة إلى رقم widget المستخدم في
// ‏FocusFormField وFormFieldInfo وFormFieldTabOrder؛ و-1 إن لم يكن widget
function AnnotationToFieldOrdinal(Pdf: TPdf; AnnotIndex: Integer): Integer;
var
  I: Integer;
begin
  Result := -1;
  if (AnnotIndex < 0) or (AnnotIndex >= Pdf.AnnotationCount) then
    Exit;
  if Pdf.Annotation[AnnotIndex].Subtype <> anWidget then
    Exit;                          // ‏links وnotes وpopups ليست حقولًا
  Result := 0;
  for I := 0 to AnnotIndex - 1 do
    if Pdf.Annotation[I].Subtype = anWidget then
      Inc(Result);
end;

عامل الناتج صالحًا فقط ما دامت الصفقة محملة وغير محررة: إضافة أي تعليق أو إزالته أمام widget يزيح فضاءي الفهرس، والخطة المخزنة من قبل التحرير هي بالضبط نوع الخطأ off-by-one الذي لا يستنسخ إلا على ملفات العملاء

التنقل عبر الصفقات مع FocusNextFormField

يمشي FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) ترتيب Tab للصفقة الحالية ثم يواصل إلى الصفقات اللاحقة، وانطلاقًا من 3.223.0 تطبق كل صفقة يزورها سياسة المعلنة الخاصة. ووصلت النسخة عبر الصفقات في الإصدار 3.220.0، فالنصيحة في مقالة التنقل بين حقول النماذج لكشف الدوران وتغيير PageNumber بنفسك تنطبق الآن على النسخة بلا وسائط وحدها، التي ما تزال تدور داخل صفقة واحدة

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
  Field: Integer;
begin
  if ssShift in Shift then
    Field := PdfView.FocusPreviousFormField(True)  // عبر الصفقات، يدور
  else
    Field := PdfView.FocusNextFormField(True);
  if Field < 0 then
    Beep   // لا شيء مؤهل داخل المسح؛ لم تتغير الصفقة ولا التركيز
  else
    UpdateFieldStatus(PdfView.PageNumber, Field);
end;

قيمة الإعادة هي رقم الحقل المحلي للصفقة على أي صفقة تحمل التركيز الآن، فاقرأ PageNumber معها. وتتخطى المشية عبر الصفقات الـ widgets التي لا يستطيع المستخدم الإنتقال إليها بـ Tab بمعنى: المخفية وغير المرئية وNoView وللقراءة فقط وsignature والمجهولة النوع وذات المستطيل الفارغ. وتمرير WrapDocument = False يتوقف عند حد المستند بـ -1. ويجب أن يقع MaxPagesToScan بين 1 و4096، وكل صفقة مفحوصة تعد، الحالية منهم. وحين لا يجد المسح شيئًا أو يرفض المحرك الأصلي التركيز على الهدف، يعيد الاستدعاء -1 ويترك الصفقة والتركيز السابقين تمامًا كما كانا

أين يتوقف الترتيب المعلن عن الانطباق؟

لا يوفى بالسياسة المعلنة إلا حين يستطيع المكوّن قراءة البيانات الوصفية للصفقة بموثوقية، وكل حالة لا يستطيع فيها تعود إلى ترتيب مصفوفة تعليقات الـ widgets مع سبب صريح. والحدود محددة بما يكفي للسرد:

  • تأتي البيانات الوصفية من تسلسل خاص محدود الحدود للوثيقة الأصلية الحالية، لا بايتات الملف الأصلي، لأن تحريك الصفقات والتحريرات في الذاكرة يجعلان الملف على القرص قديمًا: تخزن اللقطة مؤقتًا لكل مراجعة محتوى، فالتحريرات المبلغ عنها تبطلها، والـ snapshots الفاشلة تخزن أيضًا
  • الوثائق المشفرة، والـ snapshots المطلوبة أثناء تحرير كائن صفقة، بلا لقطة وتعود للبديل دائمًا
  • تسقف اللقطة عند 64 MiB و65536 كائن نشط و4096 صفقة و4096 تعليقًا لكل صفقة و65536 تعليقًا إجمالًا وعمق بنية 64؛ والوثائق الأكبر تعود للبديل بدل تعطيل التنقل
  • تغطى وثائق AcroForm فقط، ونماذج XFA تبقى خارج هذا الملف
  • يحتاج ترتيب الصفوف والأعمدة مستطيلات widgets منتهية وغير فارغة وأبعاد صفقة معقولة؛ والهندسة المنحطة تطلق البديل
  • هذه الـ APIs تقود التنقل من كودك: معالجة PDFium الخاصة لمفتاح Tab مرسل إلى المحرك لا تستبدل، فوجّه المفتاح عبر FocusNextFormField إن أردت الترتيب المعلن

تحفظ الاستعلامات الصفقة الحالية والتركيز الأصلي وأي نص تحرير معلق في الحقل المركز، ولا تشغل أي إجراءات دورة حياة للمستند أو الصفقة. استدعاء FormFieldTabOrder عند كل ضغطة مفتاح آمن إذن، وبعد أول استدعاء على مراجعة محتوى معينة يكون رخيصًا

ترتيب Tab المعلن وتشخيصات الحالة والتنقل المركز عبر الصفقات جزء من PDFium Component لـ Delphi وC++Builder؛ وتصل صفحة المنتج بمرجع API الكامل، بما فيها إدخالا TPdfFormTabOrderMode وTPdfFormTabOrderStatus