مقاله فنی

ساخت فیلدهای AcroForm و اکشن‌ها با HotPDF در دلفی

یک اکشن (action) در AcroForm دیکشنری‌ای است که به یک ویجت متصل می‌شود و به نمایشگر می‌گوید که وقتی اتفاقی برای آن ویجت می‌افتد چه کاری انجام دهد. روی دکمه‌ای کلیک کنید و نمایشگر دیکشنری اکشن آن را می‌خواند: یک اکشن URI آدرس وب را باز می‌کند، یک اکشن JavaScript اسکریپتی را اجرا می‌کند، یک اکشن SubmitForm مقادیر جمع‌آوری‌شده فیلد را به یک نقطه پایان (endpoint) پست می‌کند، یک اکشن ResetForm آن‌ها را به حالت پیش‌فرض برمی‌گرداند. اکشن داده است، نه رفتاری که در داخل فایل پخته شده باشد. استاندارد ISO 32000-1 §12.6 شکل دیکشنری را تعریف می‌کند؛ نمایشگر موتوری را ارائه می‌دهد که آن را تفسیر می‌کند. این تفکیک اهمیت دارد زیرا اکشنی که به صورت بی‌نقص در PDF نوشته شده است، اگر خواننده در طرف دیگر موتوری برای آن نداشته باشد، هیچ کاری انجام نمی‌دهد، و بسیاری از مشکلات AcroForm به جای اینکه از یک فیلد بد شکل سرچشمه بگیرند، به این شکاف برمی‌گردند

موتور HotPDF این دیکشنری‌ها را مستقیماً از Delphi و C++Builder، در کنار ویجت‌های فیلدی که از آن‌ها آویزان هستند، می‌نویسد. دو ساختار در هر فرم تعاملی در جریان است: ویجتی که کاربر روی صفحه می‌بیند، و ماشین‌آلاتِ فیلد به علاوه اکشنِ زیرینِ آن که داده‌ها و سیم‌کشی را حمل می‌کند. آن‌ها به طور مستقل ویرایش می‌شوند، و هر یک می‌تواند اشتباه باشد در حالی که دیگری خوب به نظر می‌رسد. بخش‌های زیر به نام‌گذاری فیلدها، خودِ اکشن‌های دکمه، جاوا اسکریپت در سطح فیلد، و کلاسی از نقص می‌پردازند که از یک بررسی بصری جان سالم به در می‌برد، زیرا کاملاً در ساختار دوم زندگی می‌کند

نام فیلدها کلیدهای مسیریابی هستند، نه کپشن‌ها

هر فیلد AcroForm یک نام کاملاً مشخص را یدک می‌کشد. استاندارد ISO 32000-1 §12.7.3 این نام (نه کپشنِ قابل مشاهده) را تبدیل به کلیدی می‌کند که با آن، هنگام صادر شدن یا ارسال (submit) فرم، مقدار فیلد جابجا می‌شود. توسعه‌دهندگانی که از طراحی VCL می‌آیند تمایل دارند با نام یک کنترل به عنوان یک شناسه کد خصوصی رفتار کنند، در حالی که در اینجا چنین نیست. این نام در واقع فرمت ارتباط در شبکه (wire format) است

اولین چیزی که در پی می‌آید این است که دو فیلد با نام کاملاً یکسان، دو فیلد جداگانه نیستند. PDF با آن‌ها به عنوان دو حاشیه‌نویسی (annotation) ویجتِ یک فیلد رفتار می‌کند که یک مقدار را به اشتراک می‌گذارند، بنابراین تایپ کردن در یکی، دیگری را در همان لحظه به‌روزرسانی می‌کند. این دقیقاً همان چیزی است که وقتی می‌خواهید نام مشتری در هر صفحه از قرارداد تکرار شود به آن نیاز دارید. اما این یک باگ است وقتی یک حلقه تولید به طور تصادفی از 'Field1' در سه صفحه مجدداً استفاده می‌کند. هیچ بررسی بصری مورد دوم را نمی‌گیرد. هر صفحه همچنان کادر (box) خود را می‌کشد، و پیوند تنها زمانی ظاهر می‌شود که فردی شروع به تایپ کند

نام‌های نقطه‌دار مانند applicant.email یک سلسله مراتب می‌سازند. گره والد applicant فرزندان خود را گروه‌بندی می‌کند، که این همان چیزی است که اجازه می‌دهد یک عمل ریست (reset) یا ارسال فرم (submit)، فقط بخشی از یک فرم را هدف قرار دهد. نام‌گذاری فیلدها از ابتدا به این شکل هیچ هزینه‌ای ندارد، و در اولین باری که سیستمِ دریافت‌کننده فقط بلوک متقاضی (applicant) را بخواهد، ارزش خود را نشان می‌دهد

دکمه‌های رادیویی قانون خاص خود را دارند. دکمه‌هایی که باید با هم تغییر وضعیت دهند (toggle) باید یک نام گروه مشترک داشته باشند. در HotPDF، فراخوانی‌های AddRadioButton که نام گروه یکسانی را ارسال می‌کنند، ویجت‌های خود را به یک فیلد والد متصل می‌کنند، و مقدار صادر شده هر دکمه ('basic' یا 'full') گزینه انتخاب شده را شناسایی می‌کند. به هر دکمه یک نام متمایز بدهید و به جای یک گروه که در آن انتخاب یک گزینه نافی دیگری است، ردیفی از کلیدهای روشن/خاموش مستقل دریافت خواهید کرد که به طور یکسانی رندر می‌شوند اما رفتار اشتباهی دارند

ایجاد مجموعه فیلدها صفحه به صفحه

موتور HotPDF فیلدها را از طریق متدهای THPDFPage قرار می‌دهد، بنابراین هر فیلد متعلق به شیء صفحه‌ای است که آن را ایجاد کرده است. تله توالی که باید مراقب آن بود AddPage است. این متد به محض بازگشت، CurrentPage را به صفحه جدید اشاره می‌دهد، بنابراین هر فراخوانی فیلد پس از آن، روی صفحه جدید قرار می‌گیرد حتی اگر فیلد از نظر منطقی متعلق به صفحه‌ای باشد که به تازگی ترک کرده‌اید. کار هر صفحه، یعنی محتوای کشیده شده و فیلدها را با هم تمام کنید، قبل از اینکه AddPage را فراخوانی کنید

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Page 1: applicant block
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));

  Pdf.AddPage;  // CurrentPage now points at page 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

مختصات از قرارداد PDF استفاده می‌کنند، به طوری که مبدأ در گوشه سمت چپ پایین صفحه قرار دارد. این همان مبدئی است که TextOut برای متن کشیده شده استفاده می‌کند، بنابراین Rect(50, 100, 200, 120) نزدیک پایین یک صفحه نامه (Letter) قرار می‌گیرد، نه بالا. در VCL، مختصات Y در بالا قرار دارد و به سمت پایین رشد می‌کند، بنابراین یک جدول طرح‌بندی که مستقیماً پورت (port) شده است به صورت عمودی آینه (mirror) در می‌آید، و هر فیلد به انتهای اشتباه صفحه منتقل می‌شود. تبدیل را یک بار در یک تابع کمکی مشترک انجام دهید به جای اینکه در هر سایت فراخوانی تکرار کنید، در این صورت یک اصلاح واحد کل فرم را تصحیح می‌کند

سیم‌کشی دکمه‌ها به اکشن‌های URI، جاوا اسکریپت و submit

یک دکمه فشاری (push button) تا زمانی که اکشنی به آن متصل نشود، بی‌اثر است. HotPDF انواع اکشن‌های ISO 32000-1 §12.6.4 را از طریق نوع داده شمارشی THPDFButtonAction (مقادیر baURI، baJavaScript، baSubmitURL، baResetForm، baHide، baShow، baNamed) نمایان می‌کند، و دو متد ارائه می‌دهد که دکمه را ایجاد کرده و اکشن آن را در یک فراخوانی واحد پیوند می‌دهند

// Open a help page in the system browser
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Run viewer-side JavaScript
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Submit as XFDF and keep empty fields in the payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

پرچم‌های (flags) ارسال (submit) سزاوار تفکر بیشتری هستند نسبت به آنچه معمولاً دریافت می‌کنند. متد AddPushButtonWithSubmitAction یک مجموعه از نوع THPDFSubmitFormFlags را می‌گیرد، و یک مجموعه خالی، یک پستِ کدگذاری شده در url ساده را تولید می‌کند، که فرمتی است که بسیاری از نقاط پایانِ (endpoints) نمونه می‌پذیرند و بسیاری از نقاط پایان تولید (production) آن را رد می‌کنند. افزودن sffXFDF قالب پیام (payload) را به XFDF تغییر می‌دهد. پرچم sffGetMethod فعل HTTP را تغییر می‌دهد. پرچم sffIncludeNoValueFields به جای اینکه فیلدهای خالی را بی‌سروصدا حذف کند، آن‌ها را در پیام نگه می‌دارد، که این موضوع دقیقاً زمانی اهمیت پیدا می‌کند که مصرف‌کننده بین "غایب" (absent) و "خالی" (blank) تمایز قائل می‌شود. مجموعه پرچم‌ها بخشی از قرارداد رابط شما با نقطه پایانیِ دریافت‌کننده است، بنابراین آن را با تیمی که فایل ارسالی را تجزیه می‌کند حل و فصل کنید، نه پس از رد شدن اولین دسته (batch)

جاوا اسکریپت در سطح فیلد: ضربه کلید، فرمت، اعتبارسنجی

کلیک‌های دکمه تنها جایی نیستند که اکشن‌ها در آن زندگی می‌کنند. HotPDF همچنین جاوا اسکریپت را به رویدادهای مربوط به هر فیلد متصل می‌کند که نمایشگرهای دارای قابلیت اسکریپت‌نویسی زمانی که کاربر در حال وارد کردن داده است، آن‌ها را اجرا (fire) می‌کنند. سه محرک (trigger) وجود دارد، و آن‌ها در نقاط مختلف چرخه حیات ورودی اجرا می‌شوند. اکشن ضربه کلید (keystroke) همزمان با رسیدن هر کاراکتر و دوباره در زمان تایید (commit) اجرا می‌شود. اکشن فرمت، پس از تایید شدن یک تغییر، مقدار نمایش داده شده را صرفاً برای ارائه بازنویسی می‌کند. اکشن اعتبارسنجی (validate) حرف آخر را می‌زند، و قبل از اینکه مقدار تایید شده به مقدار فیلد تبدیل شود، آن را می‌پذیرد یا رد می‌کند

// Reject committed values that are not plausible email addresses
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Display US phone numbers as (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Refuse applicants under 18 at commit time
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

تنظیم event.rc = false در داخل یک اسکریپت keystroke یا validate به نمایشگر می‌گوید که ورودی را رد کند. نکته اینجاست که هیچ یک از این موارد اجرا نمی‌شوند مگر اینکه نمایشگر دارای موتور جاوا اسکریپت باشد. Acrobat و چند محصول دسکتاپ آن را دارند. اکثر خواننده‌های موبایل، رندرهای تعبیه‌شده در مرورگر، و خطوط لوله چاپ آن را ندارند، و آن‌ها اسکریپت‌ها را بدون شکایت نادیده می‌گیرند. بنابراین اسکریپت‌های فیلد کیفیت داده‌ها را برای زیرمجموعه‌ای از کاربرانی که خواننده‌شان آن‌ها را اجرا می‌کند بهبود می‌بخشد، و این تنها کاری است که انجام می‌دهند. آن‌ها یک مرز امنیتی نیستند. هر مقدار ارسال شده هنوز باید روی سرور پس از رسیدن اعتبارسنجی شود، زیرا نمی‌توانید فرض کنید که کلاینت چیزی را بررسی کرده است

نقص‌هایی که از بررسی بصری عبور می‌کنند

سخت‌ترین نقص‌های AcroForm برای کشف، مواردی هستند که در ساختار داده‌ها به جای رندر زندگی می‌کنند، زیرا باز کردن فایل و نگاه کردن به آن چیزی به شما نمی‌گوید. چهار مورد آنقدر زیاد پیش می‌آیند که ارزش نام بردن دارند، و هر کدام دارای یک آزمایش مکانیکی است که آن را قبل از انتشار پیدا می‌کند

  • رانش مقدار صادرات (Export value drift). یک چک‌باکس که به صورت AddCheckBox('consent', 'Yes', ...) ایجاد شده، مقدار Yes را پست می‌کند. مصرف‌کننده‌ای که با Y تطابق انجام می‌دهد، در حالی که صفحه کاملاً عالی به نظر می‌رسد، هر ارسال فرم را رد می‌کند. فرم را پر کنید، آن را به عنوان XFDF از Acrobat صادر (export) کنید و مقادیر را در برابر طرحواره‌ای که مصرف‌کننده واقعاً انتظار دارد دیف (diff) بگیرید
  • آینه شدن تصادفی مقدار (Accidental value mirroring). دو فیلد که نام کاملاً یکسانی دارند در یکی ادغام می‌شوند. علامت این مشکل در زمان ورود داده‌ها نشان داده می‌شود و هرگز در زمان تولید ظاهر نمی‌شود، بنابراین آزمایش این است که در فرم تایپ کنید، نه اینکه آن را رندر کنید و نتیجه را با چشم ببینید
  • مقادیر ترکیبی خارج از لیست گزینه‌ها. زمانی که مقدار فعلیِ فرستاده شده به AddComboBox یکی از گزینه‌های لیست شده نباشد، نمایشگرها در مورد نمایش دادن، خالی گذاشتن یا پرچم زدنِ آن اختلاف نظر دارند. پیش‌فرض را در داخل لیست نگه دارید تا این اختلاف نظر از بین برود
  • فیلدها پس از بسته شدن جریان کار هنوز قابل ویرایش هستند. موتور HotPDF هیچ فراخوانی برای صاف کردنِ ظاهر (appearance-flattening) فیلدهای AcroForm ندارد. راهِ پشتیبانی‌شده برای مسدود کردن یک فرمِ تکمیل‌شده، ایجادِ فیلدها با پرچم ffReadOnly است که در عینِ حال که ویرایش‌ها را رد می‌کند، مقدار را از طریقِ جریانِ ظاهرِ خودِ فیلد قابل مشاهده نگه می‌دارد. این فیلد به عنوان یک شیء فرمِ زنده باقی می‌ماند، و این همان چیزی است که ابزارهای جمع‌آوری و امضایِ پایین‌دست انتظار دارند پیدا کنند

یک رفتار سمت نمایشگر ارزش ثبت در نکات رگرسیون را دارد حتی اگر هیچ تغییر کدی به آن نپردازد. استقرار سازمانی Acrobat می‌تواند جاوا اسکریپت را غیرفعال کند یا اهدافِ submit را بر اساس سیاست (policy) محدود کند، بنابراین اکشنی که در تمام نسخه‌های در حال توسعه کار می‌کرد می‌تواند روی یک دسکتاپ قفل‌شده مشتری، مرده بنشیند. برای موردی که دکمه هیچ کاری انجام نمی‌دهد یک راهکارِ جایگزینِ (fallback) قابل مشاهده در نظر بگیرید، حتی اگر این جایگزین فقط یک دستورالعمل چاپ شده باشد که به کاربر می‌گوید به جای آن چه کاری انجام دهد

جایی که کار با فرم به بقیه سند متصل می‌شود

یک فیلد امضا به خودی خود یک نوع فیلد AcroForm است. برای فرمی که بعداً تایید یا امضا می‌شود، بهتر است که آن فیلد را در طول تولید رزرو کنید تا اینکه بعداً آن را به صورت پچ (patch) اضافه کنید، و دلایل در سطح بایت برای اینکه چرا چنین است، در مقاله همراه در مورد امضاهای دیجیتال و امضای PAdES با HotPDF آمده است. ورودی‌هایی که به صورت بسته‌های XFA به جای AcroForm بومی (native) می‌رسند، وضعیت متفاوتی دارند: صاف کردن (flattening) XFA به فیلدهای AcroForm گردش کارِ خاص خود را با مدلِ اتلافِ (loss model) خاصِ خود دارد، زیرا دو تکنولوژیِ فرم نمی‌توانند در یک فایل همزمان وجود داشته باشند

متدهای فیلد، اکشن و محرک که در اینجا نشان داده شده‌اند، بخشی از استانداردِ API کامپوننت HotPDF برای Delphi و C++Builder هستند؛ صفحه محصول، مرجعِ کامل از جمله اضافه‌بارهای (overloads) پرچم فیلد و شمارشگرِ (enumeration) کاملِ پرچم‌های submit را پیوند می‌دهد