مقاله فنی

پیمایش فیلد فرم PDF در دلفی (کامپوننت PDFium)

کلید Tab را در فرم PDF که کد شما ساخته است فشار دهید، و مکان‌نما دو فیلد دورتر از جایی که باید، قرار می‌گیرد، یا ستون دوم را کاملاً نادیده می‌گیرد، یا پس از فیلد سوم به جای رفتن به فیلد چهارم، به بالا می‌پرد. شخصی که فاکتور را در نمایشگر شما پر می‌کند انتظار دارد کیبورد فرم را به همان روشی که در همه فرم‌های وب که تا به حال استفاده کرده است، پیمایش کند. وقتی این اتفاق نمی‌افتد، آنها به سراغ موس می‌روند، به دنبال کادر بعدی می‌گردند، و در دل تصمیم می‌گیرند که ابزار شما ناتمام است. پیمایش قابل‌پیش‌بینی فیلدها تفاوت بین یک نمایشگر ورود اطلاعات که مردم آن را تحمل می‌کنند و نمایشگری است که به آن اعتماد دارند، و این تقریباً به طور کامل مربوط به استفاده از API فوکوس صحیح به جای شبیه‌سازی ورودی کیبورد با کلیک‌های مصنوعی است

مثال‌های زیر از PDFium Component استفاده می‌کنند، یک کامپوننت VCL/LCL مبتنی بر PDFium برای دلفی، C++Builder، و لازاروس. پیمایش یکی از سه چیزی است که یک نمایشگر فرم باید به درستی انجام دهد؛ دو مورد دیگر، باز کردن صحیح فرم و ذخیره مقادیر پر شده به طوری که واقعاً نمایش داده شوند، مواردی هستند که بیشتر غافلگیری‌ها در آنها پنهان است، بنابراین هر سه مورد در زیر پوشش داده شده‌اند

باز کردن فرم: FormFill، FormType و مسئله XFA

دسترسی به فیلد نیازمند فعال بودن زیرسیستم فرم (form-fill) است که توسط ویژگی FormFill کنترل می‌شود و باید قبل از باز شدن سند فعال شود. پس از فعال شدن، FormType به شما می‌گوید با چه نوع فرمی روبرو هستید، و این پاسخ مجموعه ویژگی‌هایی که می‌توانید قول دهید را تغییر می‌دهد:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

دو نکته عملی از آن ساختار switch ناشی می‌شود. AcroForm مدل فرم استاندارد ISO 32000 است، و این چیزی است که هر API در اینجا هدف قرار می‌دهد. اسناد XFA معماری فرم XML خاص خود را جاسازی می‌کنند، بنابراین قول دادن ویرایش کامل XFA به یک مشتری پس از یک دموی سریع از AcroForm تعهدی است که از آن پشیمان خواهید شد. نکته دوم در مورد اثرات جانبی است: تنظیم FormFill به True همچنین جاوا اسکریپت سند را مقداردهی اولیه می‌کند. در یک نمایشگر ورود اطلاعات، این دقیقاً درست است، زیرا اسکریپت‌های محاسباتی همان چیزی هستند که هنگام تایپ کاربر، مجموع را به‌روز نگه می‌دارند. در یک پنجره پیش‌نمایش برای فایل‌هایی با منشأ ناشناخته، این دقیقاً اشتباه است. مقاله پیش‌نمایش امن PDF به جنبه FormFill := False این مبادله می‌پردازد

پیمایش با کلید Tab که در جای مورد انتظار کاربران قرار می‌گیرد

بازگشت به مشکل کیبورد از بالا. وسوسه این است که Tab را با شبیه‌سازی کلیک موس بر روی مستطیل ویجت بعدی جعل کنیم، که در لحظه‌ای که فیلد از صفحه خارج می‌شود یا دو ویجت همپوشانی دارند، خراب می‌شود. در عوض، API فوکوس به طور مستقیم فوکوس خود فرم را جابجا می‌کند، بدون حدس و گمان در مورد هندسه. پنج فراخوانی این را پوشش می‌دهند: FocusFormField بر اساس شاخص، FocusNextFormField و FocusPreviousFormField برای حرکت گام‌به‌گام، FocusedFormFieldIndex برای خواندن جایی که هستید، و ClearFormFieldFocus برای رها کردن کامل فوکوس

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

تنها رفتاری که افراد را سردرگم می‌کند، چرخش (wrap) است. پیمایش از طریق ترتیب تب (tab order) صفحه فعلی عمل می‌کند و درون آن حلقه می‌زند: از فیلد آخر بگذرید و به فیلد اول بازمی‌گردید. هر دو تابع حرکت گام‌به‌گام شاخص فیلد جدید، یا -1 در صورتی که صفحه هیچ فیلدی نداشته باشد را برمی‌گردانند. این حلقه‌زدن در هر صفحه است، نه در هر سند، به این معنی که رفتن به صفحه بعدی وظیفه شماست، نه کتابخانه. شاخص برگشتی را با شاخصی که از آن شروع کرده‌اید مقایسه کنید، توجه کنید که چه زمانی چرخیده است، و PageNumber را خودتان افزایش دهید اگر قرار است فرم به صورت یک توالی پیوسته خوانده شود. از این بررسی بگذرید و یک فرم دو صفحه‌ای بی‌صدا مکان‌نما را در صفحه یک گیر می‌اندازد، که نوع خاص خود از شکایتِ Tab خراب است

پیمایش زمانی مفید می‌شود که بقیه UI به آن واکنش نشان دهد. رویداد OnFormFieldEnter هنگام رسیدن فوکوس اجرا می‌شود، و در نمایشگر، OnFormFieldFocusChange شاخص فیلد جدید را گزارش می‌دهد، بنابراین یک پنل جانبی می‌تواند با هر چیزی که کیبورد به تازگی انتخاب کرده است همگام بماند. وقتی به نقشه معکوس نیاز دارید، از موقعیت صفحه نمایش به یک فیلد، ویژگی نمایه‌دار FormFieldAt تست برخورد (hit-testing) را برای پیش‌نمایش‌های تولتیپ و پنل‌های کلیک-برای-ویرایش انجام می‌دهد. در تمام این موارد یک بازدهی پنهان دسترسی‌پذیری وجود دارد: از آنجایی که فوکوس از ترتیب فیلد خود سند پیروی می‌کند، مسیری که برای کلید Tab متصل می‌کنید همان مسیری است که یک صفحه‌خوان (screen reader) بدون هیچ کار اضافی اعلام می‌کند

نمایش نام فیلدها به جای اعداد شاخص خام، یک ویژگی دیگر می‌طلبد. FormFieldInfo[] یک رکورد TPdfFormFieldInfo در هر شاخص برمی‌گرداند، که حامل نام فیلد، نوع، اندازه فونت، وضعیت بررسی شده (checked state)، مقدار خروجی (export value)، و عضویت در گروه است، که این همان چیزی است که یک لیست پیمایش باید نمایش دهد ("فیلد ۴ از ۱۷: InvoiceDate" به جای "۴"). گروه‌های رادیویی (Radio groups) موردی هستند که ارزش یک فایل آزمایشی اختصاصی را دارند. چندین ویجت می‌توانند نام فیلد واحدی را به اشتراک بگذارند، بنابراین لیستی که به صورت ساده‌لوحانه از ویجت‌ها جمع‌آوری شده، گروه یکسانی را چندین بار نشان می‌دهد و همه کسانی که آن را می‌خوانند را گیج می‌کند

چرا مقادیر پر شده خالی خروجی می‌دهند، و فراخوانی که آن را رفع می‌کند

شکایت دیگری که صف‌های پشتیبانی را پر می‌کند، نگران‌کننده‌تر از کلید Tab خراب است: یک فرم به صورت برنامه‌ریزی‌شده پر می‌شود، مشتری آن را در آکروبات باز می‌کند و هر فیلدی خالی به نظر می‌رسد. روی یک فیلد کلیک کنید و مقدار آن نمایان می‌شود. داده‌ها تمام مدت در فایل هستند. چیزی که گم شده، تصویر داده‌هاست، و دلیل آن ارزش یک بار درک کردن را دارد زیرا کل خانواده‌ای از باگ‌ها را توضیح می‌دهد

یک فیلد متنی AcroForm مقدار خود را در ورودی /V دیکشنری فیلد ذخیره می‌کند (ISO 32000-1 §12.7.3.3). چیزی که یک نمایشگر در واقع رسم می‌کند (paint)، یک چیز جداگانه است: جریان ظاهر ویجت (appearance stream) زیر /AP (§12.5.5)، یک قطعه کوچکِ از پیش‌رندر شده از محتوا. /V را بنویسید و /AP را به حال خود رها کنید، و این دو از هم دور می‌شوند. مقدار آنجاست؛ نسخه رندر شده آن قدیمی یا غایب است. آکروبات به طور اتفاقی ظاهر یک فیلد را هنگامی که فوکوس می‌گیرد، بازسازی می‌کند، که این توضیح کامل برای مقادیری است که فقط هنگام کلیک ظاهر می‌شوند. پرچم قدیمی NeedAppearances، که از نمایشگرها می‌خواست ظاهرها را برای شما بازسازی کنند، هرگز به طور یکنواخت کار نکرد و در PDF 2.0 منسوخ شده است، و سرورهای چاپ و سازندگان تصویر بندانگشتی به کلی آن را نادیده می‌گیرند. آنها /AP و نه چیز دیگری را رسم می‌کنند، بنابراین اگر /AP خالی باشد، یک کادر خالی چاپ می‌کنند

تخصیص یک مقدار از طریق FormField[i] فقط /V را می‌نویسد. به همین دلیل است که پر کردن یک فرم، یک توالی سه‌مرحله‌ای است، و مرحله‌ای که تیم‌ها جا می‌اندازند، مرحله میانی است:

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances کل راه‌حل است. این فراخوانی جریان ظاهر هر ویجت را از مقادیر فعلی، فونت‌ها، و ترازبندی (quadding) بازسازی می‌کند، بنابراین نمایشگری که هرگز رویداد فوکوس را اجرا نمی‌کند، یک سرور چاپ یا سازنده تصویر بندانگشتی، وضعیت پر شده را در هر صورت رسم می‌کند. آن را یک بار پس از دسته‌ای از تخصیص‌ها فراخوانی کنید، نه یک بار برای هر فیلد. تولید ظاهر، کار چینش واقعی انجام می‌دهد و فراخوانی‌هایِ هر-فیلد این کار را در سراسر یک فرم بزرگ بی‌جهت چندبرابر می‌کند

بازسازی ظاهر همچنین لحظه‌ای است که فونت‌ها و ترازبندی خود را نشان می‌دهند، که منشأ یک شگفتی مرتبه دوم است. جریان جدید، هر مقدار را در داخل مستطیل ویجت با استفاده از فونت، اندازه و ترازبندیِ فیلد می‌چیند. مقداری که به راحتی در فرم آزمایشی شما جا می‌گیرد، می‌تواند در کپی مشتری که همان فیلد باریک‌تر است، بریده (clip) یا کوچک (shrink) شود. فیلدهای با اندازه خودکار (اندازه فونت صفر) متن را برای جا شدن کوچک می‌کنند؛ فیلدهای با اندازه ثابت، آن را به سادگی می‌بُرند. هر دو مجاز هستند، و تنها راه صادقانه برای فهمیدن اینکه یک فرم خاص کدام کار را انجام می‌دهد، نگاه کردن به خروجی بازسازی شده به جای رشته‌ای است که نوشتید. وقتی کسی بریدگی متن در لبه کادر را گزارش می‌دهد، این تقریباً همیشه دلیل آن است

تأیید را بخشی از پایان کار در نظر بگیرید، نه یک فکر ثانویه. فایل ذخیره شده را در آکروبات باز کنید و تأیید کنید مقادیر قبل از اینکه هر فیلدی را لمس کنید، قابل مشاهده هستند. سپس آن را به PDF چاپ کنید یا به یک تصویر از یک نمایشگر دیگر که منطق فرم را کاملاً نادیده می‌گیرد، و تأیید کنید مقادیر از آن مسیر نیز جان سالم به در می‌برند. در بین آنها، این دو بررسی هرگونه تغییر از انحراف /V-دربرابر-/AP را می‌گیرند

پیکربندی‌های فیلدی که از دمو عبور می‌کنند و در واقعیت شکست می‌خورند

فرم‌های دموی تمیز مجموعه‌ای از موارد لبه (edge cases) را پنهان می‌کنند که فایل‌های مشتری پنهان نمی‌کنند. چهار مورد از آن‌ها بیشتر گزارش‌های "روی ماشین من کار کرد" را به خود اختصاص می‌دهند

  • مقادیر خروجی چک‌باکس. وضعیت "روشن" همیشه Yes نیست. یک فرم آزاد است که مقدار خروجی خود را تعریف کند، و نوشتن رشته اشتباه باعث می‌شود در حالی که کد شما متقاعد شده که آن را تنظیم کرده، کادر از نظر بصری تیک‌نخورده باقی بماند. مقدار خروجی را از FormFieldInfo[] بخوانید به جای اینکه یکی را فرض کنید
  • گروه‌های رادیویی با نام مشترک. یک فیلد، چندین ویجت. مقداری که شما اختصاص می‌دهید تصمیم می‌گیرد کدام ویجت به عنوان انتخاب شده خوانده شود، بنابراین کد UI که فرض می‌کند یک نام به یک مستطیل مپ می‌شود، در نهایت حلقه فوکوس را روی دکمه اشتباه رسم می‌کند
  • فیلدهای محاسباتی. مجموع‌هایی که توسط جاوا اسکریپتِ سند نگهداری می‌شوند در پاسخ به رویدادهای فیلد به‌روزرسانی می‌شوند. یک پر کردن برنامه‌ریزی‌شده که آن رویدادها را دور می‌زند، باید یا محاسبه مجدد را راه‌اندازی کند یا فیلدهای محاسباتی را مستقیماً بازنویسی کند. فرمی که در آن اقلام خطی و مجموع کل با هم اختلاف دارند، از هر دو راه‌حل بدتر است
  • فیلدهای ضروری پنهان. فرم‌های شرطی فیلدهایی را پنهان می‌کنند که هنوز به عنوان ضروری (required) علامت‌گذاری شده‌اند. از قبل تصمیم بگیرید که آیا اعتبارسنجی شما به قابل مشاهده بودن احترام می‌گذارد یا به پرچم ضروری خام، سپس آن تصمیم را در جایی بنویسید که تیم پشتیبانی بتواند آن را پیدا کند

یک تمایز ارزش حل کردن را قبل از اینکه به شما آسیب برساند دارد: تولید ظاهرها به معنای تخت کردن (flattening) نیست. GenerateFormAppearances مقادیر را در همه‌جا قابل مشاهده می‌کند در حالی که فیلدها را قابل ویرایش می‌گذارد. تخت کردن ظاهر را در محتوای صفحه ثابت (static) پخته و قابلیت تعامل را برای همیشه از بین می‌برد، که برای یک کپی بایگانی درست است و برای فرمی که نفر بعدی هنوز باید پر کند اشتباه است. اگر FormType به جای ftAcroForm مقدار ftXfaFull را گزارش می‌دهد، هیچ یک از سطوح ویرایشی اینجا به تمیزی اعمال نمی‌شود، زیرا سند از قالب XML خاص خود رندر می‌شود؛ آن حالت را شناسایی کنید و به کاربر بگویید، به جای اینکه بگذارید خودش محدودیت را پیدا کند

زیرسیستم فرم (form-fill)، پیمایش فوکوس، و تولید ظاهر که در اینجا نشان داده شده است، بخشی از PDFium Component برای دلفی، C++Builder، و Lazarus/FPC است. اگر نمایشگر شما همچنین حاشیه‌نویسی بازبین را در کنار داده‌های فرم اداره می‌کند، مقاله مرور حاشیه‌نویسی (annotation review) آن مدل مجاور را پوشش می‌دهد