Technical Article

فرم‌های تعاملی PDF در Delphi: اکشن‌ها و JavaScript

یک فیلد فرم PDF به تنهایی فقط جعبه‌ای است که یک مقدار را نگه می‌دارد. آنچه باعث می‌شود یک فرم مانند یک اپلیکیشن کوچک رفتار کند، اکشنی است که به آن پیوست شده است: کلیکی که یک بخش را پنهان می‌کند، مقادیر ذخیره‌شده را از یک فایل بازخوانی می‌کند، به آخرین صفحه می‌پرد، یا اسکریپتی را اجرا می‌کند که مجموع یک ستون را محاسبه می‌نماید. هیچ‌کدام از این‌ها در خود فیلد قرار ندارند، بلکه در یک دیکشنری اکشن (action dictionary) زندگی می‌کنند و استاندارد ISO 32000-1 کل این خانواده را در بخش ۱۲.۶ سازماندهی می‌کند. این مقاله اکشن‌هایی را که یک برنامه Delphi بیشتر به آن‌ها نیاز دارد بررسی می‌کند و نشان می‌دهد که چگونه PDFlibPas هر یک را به یک فیلد یا یک لینک متصل می‌نماید.

مدل ذهنی که ارزش حفظ کردن دارد این است که یک فیلد و یک اکشن، اشیای مجزایی هستند که توسط یک مرجع (reference) به هم متصل شده‌اند. یک ویجت حاشیه‌نویسی (widget annotation) یا حاشیه‌نویسی لینک (link annotation) اکشنی را در ورودی /A خود حمل می‌کند. اکشن نام فیلدی را که روی آن عمل می‌کند با عنوان (title) مشخص می‌کند، نه با ایندکس؛ بنابراین عنوانی که به یک فیلد می‌دهید، هندلی است که هر اکشن بعدی برای یافتن آن استفاده می‌کند. هنگامی که این تفکیک روشن شد، API دیگر شبیه به مجموعه‌ای درهم از فراخوانی‌ها به نظر نمی‌رسم و مانند یک الگو به نظر می‌رسد که برای چهار نوع عمل به کار می‌رود.

اکشن‌های نام‌گذاری شده: ناوبری بدون شماره صفحه

ساده‌ترین اکشن‌ها هیچ پارامتری را حمل نمی‌کنند. بخش ۱۲.۶.۴.۱۱ استاندارد ISO 32000-1، جدول ۱۹۴، اکشن‌های نام‌گذاری شده (named actions) را تعریف می‌کند: نمایشگر به جای دنبال کردن یک مقصد ذخیره‌شده، یک نام نمادین را در زمان اجرا تفسیر می‌کند. چهار نام به طور سراسری پشتیبانی می‌شوند و دقیقاً همان‌هایی هستند که یک خواننده از نوار ابزار انتظار دارد: NextPage ،PrevPage ،FirstPage و LastPage. از آنجا که مقصد نسبت به هر صفحه‌ای است که نمایشگر در حال حاضر نشان می‌دهد، یک دکمه Next که به این روش ساخته شده است در هر صفحه بدون نیاز به محاسبه مقصد توسط شما کار می‌کند.

در PDFlibPas یک اکشن نام‌گذاری شده به یک مستطیل نقطه حساس (hotspot) در صفحه فعلی متصل می‌شود. آرگومان‌های صحیح چهارم و پنجم، عمل و ظاهر را انتخاب می‌کنند.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // jump to last page

هیچ مقصدی برای همگام نگه داشتن وجود ندارد که کل هدف نیز همین است. یک اکشن نام‌گذاری شده در برابر درج و حذف صفحه دوام می‌آورد زیرا در وهله اول هرگز نام صفحه‌ای را نمی‌آورد. این را با یک لینک صریح انتقال (go-to) مقایسه کنید که ایندکس صفحه مقصد را ذخیره می‌کند و شما باید به محض بزرگ شدن سند، شماره آن را تغییر دهید.

اکشن Hide و مشکل آرایه آن

اکشن Hide، استاندارد ISO 32000-1 بخش ۱۲.۶.۴.۱۰، جدول ۱۹۶، وضعیت نمایش یک یا چند فیلد را تغییر می‌دهد. این تمیزترین راه برای ساخت رفتار نمایش و پنهان‌سازی بدون اسکریپت‌نویسی است و همان چیزی است که برای لینک «نمایش جزئیات» یا برای دو پنل متقابل که نمایان شدن یکی باعث پنهان شدن دیگری می‌شود، می‌خواهید. این اکشن یک هدف را در ورودی /T خود و یک مقدار بولی /H را حمل می‌کند که جهت را تعیین می‌کند: پنهان کردن در صورت true، نشان دادن در صورت false.

ظرافت کار کاملاً در نحوه رمزگذاری آن هدف است و این از آن جزئیاتی است که فرمی را تولید می‌کند که روی سیستم شما کار می‌کند اما روی سیستم مشتری خراب می‌شود. هنگامی که اکشن نام یک فیلد واحد را می‌برد، /T به عنوان یک رشته متنی نوشته می‌شود. وقتی نام چندین فیلد را می‌برد، /T به عنوان آرایه‌ای از رشته‌های متنی نوشته می‌شود. نمایشگرهای قدیمی‌تر با یک آرایه تک‌عضوی مانند یک رشته ساده رفتار نمی‌کنند، بنابراین رمزگذاری باید بر اساس تعداد شاخه داشته باشد: یک نام واحد باید به عنوان یک رشته صادر شود، نه به عنوان یک آرایه با طول یک، تا طیف وسیع‌تری از خوانندگان آن را بپذیرند. PDFlibPas این تصمیم را برای شما می‌گیرد. شما نام فیلدها را با کاما، نقطه‌کاما یا شکست خط جدا می‌کنید و نویسنده یک رشته واحد برای یک نام و یک آرایه برای دو یا چند نام صادر می‌کند.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

از آنجا که اکشن به هیچ منبع خارجی ارجاع نمی‌دهد، با PDF/A سازگار می‌ماند. نام‌هایی که پاس می‌دهید عناوین کاملاً واجد شرایط فیلد (fully qualified field titles) هستند، به همین دلیل است که یک فیلد فرزند در یک گروه باید از طریق مسیر کامل نقطه‌دار آن آدرس‌دهی شود تا نام برگ ساده آن.

ImportData: پیش‌پر کردن از FDF

در حالی که اکشن Hide چیدمان آنچه را که در صفحه وجود دارد تغییر می‌دهد، اکشن import-data مقادیر را از بیرون وارد می‌کند. بخش ۱۲.۶.۴.۸ استاندارد ISO 32000-1، جدول ۱۹۸، آن را به عنوان اکشنی تعریف می‌کند که AcroForm را از یک فایل Forms Data Format روی دیسک پر می‌کند. این اکشن پشت کنترل‌های Reload sample data یا Reset to defaults قرار دارد، جایی که یک فایل FDF در کنار PDF ارسال می‌شود و مقادیر فیلد استاندارد را نگه می‌دارد. این فراخوانی مشابه بقیه است و مستطیل نقطه حساس، مسیر FDF و بیت‌ماسک ظاهر را می‌گیرد: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). فایل نیازی به وجود در زمان ساخت PDF ندارد، اما در زمان کلیک کاربر باید موجود باشد و هر بک‌اسلش در مسیر به فرم اسلش استاندارد PDF برای شما بازنویسی می‌شود.

یک محدودیت ارزش بیان صریح دارد زیرا اغلب مایه شگفتی می‌شود. یک اکشن import-data به یک فایل خارجی اشاره می‌کند، بنابراین در PDF/A مجاز نیست. هنگامی که سند در حالت PDF/A قرار دارد، این فراخوانی مقدار صفر را برمی‌گرداند و هیچ چیزی اضافه نمی‌کند، نه اینکه فایلی تولید کند که در اعتبارسنجی رد شود. اگر خط لوله شما خروجی آرشیوی را هدف قرار داده است، پیش‌پر کردن باید در زمان تولید با نوشتن مستقیم مقادیر فیلد انجام شود، نه با موکول کردن آن‌ها به یک کلیک.

JavaScript: پکیج‌های سراسری و اسکریپت‌های اختصاصی اکشن

برای منطقی که فراتر از نمایش، پنهان‌سازی و ایمپورت است، خانواده اکشن به سراغ JavaScript در سطح سند می‌رود. دو مکان متمایز وجود دارد که یک اسکریپت می‌تواند در آن‌ها زندگی کند و تفاوت آن‌ها مهم است. یک پکیج JavaScript در سطح سند یک بار برای کل فایل ذخیره می‌شود و هنگام باز شدن سند اجرا می‌گردد که این امر آن را به خانه مناسبی برای تعاریف توابع و حالت‌های مشترک تبدیل می‌کند. یک اسکریپت اختصاصی اکشن به یک لینک یا فیلد متصل است و تنها زمانی اجرا می‌شود که آن شیء فعال گردد، که آن را به خانه مناسبی برای تک‌خطی تبدیل می‌کند که تابعی را فراخوانی می‌کند که پکیج از قبل تعریف کرده است.

مجموعه PDFlibPas هر دو را در دسترس قرار می‌دهد. AddGlobalJavaScript یک پکیج نام‌گذاری شده را در سطح سند ذخیره می‌کند؛ استفاده مجدد از یک نام، هر آنچه را که تحت آن ذخیره شده بود جایگزین می‌کند. AddLinkToJavaScript یک اسکریپت را به یک نقطه حساس متصل می‌کند تا کلیک روی آن، اسکریپت را اجرا کند.

// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

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

فیلدها، فیلدهای فرزند و ثابت کردن نتیجه

اکشن‌ها به فیلدهایی برای اقدام روی آن‌ها نیاز دارند، بنابراین مفید است که ببینیم یک فیلد چگونه به وجود می‌آید. NewFormField یک فیلد در صفحه فعلی ایجاد می‌کند و ایندکس آن را برمی‌گرداند؛ نوع عدد صحیح نوع فیلد را انتخاب می‌کند که در آن ۱ برای Text، ۲ برای Pushbutton، ۳ برای Checkbox، ۴ برای Radiobutton، ۵ برای Choice، ۶ برای Signature و ۷ یک Parent است که مالک فرزندان است اما خودش چیزی رسم نمی‌کند. عنوانی که پاس می‌دهید نمی‌تواند شامل نقطه باشد، زیرا نقطه به عنوان جداکننده در نام‌های کاملاً واجد شرایط است که اکشن‌ها برای آدرس‌دهی به فرزندان استفاده می‌کنند.

گروه‌های رادیویی و فرم‌های سلسله‌مراتبی با دادن فرزندان به فیلد والد ساخته می‌شوند. NewChildFormField یک فرزند را تحت یک والد نام‌گذاری شده اضافه می‌کند و برای موارد رادیویی و گزینشی، AddFormFieldSub گزینه‌های تکی را اضافه کرده و یک ایندکس موقت را برمی‌گرداند که برای تعیین موقعیت هر کدام استفاده می‌کنید. هنگامی که فاز تعاملی به پایان رسید و می‌خواهید یک فیلد را ثابت کنید تا ظاهر فعلی آن به محتوای دائمی صفحه تبدیل شود، FlattenFormField فیلد را روی صفحه رسم می‌کند و آن را از فرم حذف می‌نماید. پس از تخت کردن (flatten)، ایندکس فیلدهای بعدی یکی به پایین شیفت پیدا می‌کند که این تنها موردی است که در صورت تخت کردن چندین فیلد در یک حلقه باید به خاطر بسپارید.

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

فراخوانی flatten عمداً کامنت شده است. آن را نادیده بگیرید تا سند به عنوان یک فرم زنده که اکشن‌های آن در خواننده اجرا می‌شوند ارسال شود. آن را فعال کنید تا فیلد به علائم ایستا رندر شود، که این همان چیزی است که وقتی فرم تکمیل شده و نتیجه باید به عنوان یک سند ثابت جابه‌جا شود، می‌خواهید. یک فیلد یکسان، یک کد یکسان، دو سند بسیار متفاوت بسته به اینکه آن را ثابت کنید یا خیر.

انتخاب عمل مناسب

چهار اکشن بر اساس آنچه لمس می‌کنند به طور واضح تقسیم می‌شوند. یک اکشن نام‌گذاری شده نمای نمایشگر را حرکت می‌دهد و به هیچ فیلدی نیاز ندارد. یک اکشن Hide قابلیت دید را تغییر می‌دهد و به عناوین فیلد نیاز دارد که رمزگذاری رشته در مقابل آرایه برای شما مدیریت می‌شود. یک اکشن import-data به یک فایل روی دیسک دسترسی پیدا می‌کند و بنابراین در PDF/A ممنوع است. یک اکشن JavaScript منطق دلخواهی را اجرا می‌کند و بهتر است بین یک پکیج سراسری از توابع و فراخوانی‌های کوچک اختصاصی اکشن تقسیم شود. به سراغ ساده‌ترین موردی بروید که کار را انجام می‌دهد: یک اکشن Hide پرتابل‌تر از اسکریپتی است که یک پرچم پنهان را تنظیم می‌کند، و یک اکشن نام‌گذاری شده بادوام‌تر از یک مقصد صفحه ذخیره‌شده است زیرا هیچ شماره‌ای برای نگهداری وجود ندارد.

از اینجا، دو موضوع همسایه تصویر را کامل می‌کنند. اگر فرم بخشی از یک سند دسترس‌پذیر است، درخت ساختاری که صفحه‌خوان‌ها طی می‌کنند در مقاله ما در مورد PDF تگ‌شده و ساختار دسترس‌پذیری پوشش داده شده است. هنگامی که فرم تکمیل‌شده باید قفل و امضا شود، جریان کاری در راهنمای گام‌به‌گام کارگاه انطباق و امضا توصیف شده است. هر سه بر روی یک موتور ساخته شده‌اند که به عنوان کتابخانه PDF برای Delphi در کنار APIهای ایجاد، فرم و امضا که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، عرضه می‌شود.