مقاله فنی

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

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

مدل ذهنی که ارزش نگه داشتن را دارد این است که یک فیلد و یک اکشن اشیاء جداگانه‌ای هستند که توسط یک مرجع به هم پیوسته‌اند. یک حاشیه‌نویسی ویجت یا یک حاشیه‌نویسی لینک یک اکشن را در ورودی /A خود حمل می‌کند. اکشن، فیلدی را که روی آن عمل می‌کند با عنوان نام می‌برد، نه با ایندکس، بنابراین عنوانی که به یک فیلد می‌دهید، دستگیره‌ای است که هر اکشن بعدی برای یافتن آن استفاده می‌کند. هنگامی که این تفکیک روشن شد، API دیگر شبیه به یک کیسه درهم‌وبرهم از فراخوانی‌ها به نظر نمی‌رسد و شبیه به یک الگوی اعمال شده برای چهار نوع فعل می‌شود

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

ساده‌ترین اکشن‌ها هیچ پارامتری ندارند. ISO 32000-1 §12.6.4.11، جدول 194، اکشن‌های نام‌گذاری شده را تعریف می‌کند: بیننده در زمان اجرا به جای پیروی از مقصد ذخیره شده، یک نام نمادین را تفسیر می‌کند. چهار نام به صورت جهانی پشتیبانی می‌شوند، و آنها دقیقاً همان نام‌هایی هستند که خواننده از یک نوار ابزار انتظار دارد: 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 §12.6.4.10، جدول 196، دیداری بودن یک یا چند فیلد را تغییر می‌دهد. این تمیزترین راه برای ساختن رفتار نمایش و پنهان کردن بدون اسکریپت‌نویسی است، و این همان چیزی است که شما برای یک پیوند «نمایش جزئیات» یا برای دو پنل دوطرفه انحصاری که در آن نمایان شدن یکی دیگری را پنهان می‌کند، می‌خواهید. اکشن یک هدف را در ورودی /T خود حمل می‌کند و یک بولین /H که جهت را تصمیم می‌گیرد: زمانی که صحیح باشد پنهان می‌کند، زمانی که نادرست باشد نشان می‌دهد

ظرافت کاملاً در این است که چگونه آن هدف کدگذاری شده است، و این از آن نوع جزئیاتی است که فرمی را تولید می‌کند که در ماشین شما کار می‌کند و در ماشین مشتری خراب می‌شود. وقتی این اکشن یک فیلد منفرد را نام می‌برد، /T به عنوان یک رشته متنی نوشته می‌شود. وقتی چند فیلد را نام می‌برد، /T به عنوان یک آرایه از رشته‌های متنی نوشته می‌شود. نمایشگرهای قدیمی‌تر با آرایه یک عنصری به همان شکلی که با رشته خالی برخورد می‌کنند، رفتار نمی‌کنند، بنابراین کدگذاری باید روی تعداد، منشعب (branch) شود: یک نام منفرد باید به عنوان یک رشته ساطع شود، نه به عنوان آرایه‌ای با طول یک، اگر قرار باشد وسیع‌ترین طیف خوانندگان به آن احترام بگذارند. 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) هستند، به همین دلیل است که یک فیلد فرزند در داخل یک گروه باید از طریق مسیر نقطه‌دار کامل خود به جای نام برگ (leaf name) خالی آن خطاب شود

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

در جایی که اکشن Hide آنچه را که قبلاً در صفحه است مرتب می‌کند، اکشن import-data مقادیری را از خارج به آن می‌آورد. ISO 32000-1 §12.6.4.8، جدول 198، آن را به عنوان یک اکشن تعریف می‌کند که AcroForm را از یک فایل با فرمت داده‌های فرم (Forms Data Format) روی دیسک پر می‌کند. این اکشنِ پشتِ کنترل‌هایی مانند بارگیری مجدد داده‌های نمونه یا بازنشانی به پیش‌فرض‌ها است، جایی که یک فایل FDF در کنار PDF ارسال می‌شود و مقادیر کانونی فیلد را نگه می‌دارد. این فراخوانی منعکس‌کننده موارد دیگر است، مستطیل نقطه اتصال، مسیر به سمت FDF، و یک bitmask ظاهری را می‌پذیرد: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). هنگام ساخت PDF نیازی به وجود فایل نیست، اما با کلیک کاربر باید حضور داشته باشد، و هرگونه بک‌اسلش در مسیر برای شما به فرم اسلش کانونی PDF بازنویسی می‌شود

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

جاوا اسکریپت: بسته‌های جهانی و اسکریپت‌های در سطح اکشن

برای منطقی که فراتر از نمایش، پنهان کردن و وارد کردن می‌رود، خانواده اکشن به جاوا اسکریپت در سطح سند دسترسی پیدا می‌کند. دو مکان متمایز وجود دارد که یک اسکریپت می‌تواند در آنها زندگی کند و این تفاوت اهمیت دارد. یک بسته جاوا اسکریپت در سطح سند یک بار برای کل فایل ذخیره می‌شود و هنگام باز شدن سند اجرا می‌شود، که آن را به خانه مناسبی برای تعاریف توابع و حالت اشتراکی تبدیل می‌کند. یک اسکریپتِ در سطحِ اکشن (per-action) به یک لینک یا فیلد متصل می‌شود و فقط زمانی اجرا می‌شود که آن شیء فعال شود، که آن را به خانه مناسبی برای خطِ واحدی که تابعی را که بسته از قبل تعریف کرده است فرامی‌خواند، تبدیل می‌کند

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);

نگه داشتن تابع در بسته جهانی و فراخوانی آن در لینک، یک ترجیح سبکی (style preference) نیست. این از تکرار همان بدنه (body) روی هر کنترلی که به آن نیاز دارد جلوگیری می‌کند و به این معنی است که نمایشگری که اسکریپت در آن غیرفعال است به سادگی با کلیک هیچ کاری انجام نمی‌دهد به جای اینکه روی یک حباب (blob) درون خطیِ بدشکل مسدود شود. همچنین ورودی‌های در سطح اکشن را کوچک نگه می‌دارد، که باعث می‌شود فایل زمانی که بعداً آن را بررسی می‌کنید، خوانا بماند

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

اکشن‌ها به فیلدهایی برای عمل کردن روی آنها نیاز دارند، بنابراین درک چگونگی به وجود آمدن یک فیلد کمک‌کننده است. NewFormField یک فیلد در صفحه فعلی ایجاد می‌کند و ایندکس آن را برمی‌گرداند؛ نوع صحیح (integer type) نوع را انتخاب می‌کند، که 1 برابر است با Text، 2 برابر Pushbutton، 3 برابر Checkbox، 4 برابر Radiobutton، 5 برابر Choice، 6 برابر Signature، و 7 یک والد (Parent) است که دارای فرزندانی است اما خودش چیزی ترسیم نمی‌کند. عنوانی که پاس می‌دهید نمی‌تواند حاوی یک نقطه باشد، زیرا نقطه به عنوان یک جداکننده در نام‌های کاملاً واجد شرایطی است که اکشن‌ها برای آدرس‌دهی به فرزندان از آنها استفاده می‌کنند

گروه‌های رادیویی و فرم‌های سلسله‌مراتبی با دادن فرزند به یک فیلد والد ساخته می‌شوند. NewChildFormField یک فرزند را زیر یک والدِ نام‌گذاری‌شده اضافه می‌کند، و برای مواردِ رادیویی و انتخابی (choice) 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 ممنوع است. یک اکشن جاوا اسکریپت منطق دلخواه را اجرا می‌کند و بهتر است بین یک بسته جهانی از توابع و فراخوانی‌های کوچک در سطح اکشن تقسیم شود. به دنبال ساده‌ترین موردی باشید که کار را انجام می‌دهد: یک اکشن Hide قابل حمل‌تر از یک اسکریپت است که پرچمِ پنهان (hidden flag) را تنظیم می‌کند، و یک اکشن نام‌گذاری شده با دوام‌تر از یک مقصد صفحه ذخیره شده است زیرا شماره‌ای برای نگهداری وجود ندارد

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