Technical Article

ฟอร์ม PDF แบบโต้ตอบได้ใน Delphi: การดำเนินการและ JavaScript

ฟิลด์ฟอร์ม PDF ในตัวมันเองเป็นเพียงกล่องสำหรับเก็บค่าเท่านั้น สิ่งที่ทำให้ฟอร์มทำงานได้เหมือนกับแอปพลิเคชันขนาดเล็กคือการดำเนินการ (action) ที่แนบอยู่กับฟิลด์นั้น เช่น การคลิกที่ซ่อนบางส่วน การดึงค่าที่บันทึกไว้กลับมาจากไฟล์ การกระโดดไปยังหน้าสุดท้าย หรือการรันสคริปต์เพื่อรวมยอดคอลัมน์ ทั้งหมดนี้ไม่ได้อยู่ในฟิลด์ แต่อยู่ในพจนานุกรมการดำเนินการ (action dictionary) และมาตรฐาน ISO 32000-1 ได้จัดระเบียบโครงสร้างนี้ไว้ในหัวข้อ §12.6 บทความนี้จะแนะนำการดำเนินการที่โปรแกรม Delphi เรียกใช้งานบ่อยที่สุด และแสดงวิธีที่ PDFlibPas เชื่อมโยงแต่ละการดำเนินการเข้ากับฟิลด์หรือลิงก์

แบบจำลองความคิดที่ควรจำไว้คือ ฟิลด์และการดำเนินการเป็นวัตถุที่แยกจากกันซึ่งเชื่อมโยงกันด้วยการอ้างอิง คำอธิบายประกอบวิดเจ็ต (widget annotation) หรือคำอธิบายประกอบลิงก์ (link annotation) จะเก็บการดำเนินการไว้ในรายการ /A การดำเนินการจะระบุชื่อฟิลด์ที่ต้องการจัดการด้วยชื่อเรื่อง (title) ไม่ใช่ดัชนี (index) ดังนั้นชื่อเรื่องที่คุณตั้งให้กับฟิลด์จึงเป็นตัวจัดการที่การดำเนินการในภายหลังจะใช้เพื่อค้นหาฟิลด์นั้น เมื่อเข้าใจการแยกส่วนนี้อย่างชัดเจนแล้ว API จะไม่ดูเหมือนการเรียกใช้งานที่สะเปะสะปะอีกต่อไป แต่จะดูเหมือนเป็นรูปแบบเดียวที่ใช้กับคำสั่งสี่ประเภท

การดำเนินการตามชื่อ (Named actions): การนำทางโดยไม่ต้องระบุหมายเลขหน้า

การดำเนินการที่ง่ายที่สุดจะไม่รับพารามิเตอร์ใดๆ เลย มาตรฐาน ISO 32000-1 §12.6.4.11 ตารางที่ 194 ได้กำหนดการดำเนินการตามชื่อไว้ โดยโปรแกรมอ่านจะตีความชื่อสัญลักษณ์ในระหว่างการรันแทนที่จะปฏิบัติตามปลายทางที่เก็บไว้ ชื่อสี่ชื่อที่ได้รับการสนับสนุนอย่างสากล และเป็นสิ่งที่ผู้อ่านคาดหวังจากแถบเครื่องมือ ได้แก่ NextPage, PrevPage, FirstPage และ LastPage เนื่องจากปลายทางมีความสัมพันธ์กับหน้าใดก็ตามที่โปรแกรมอ่านกำลังแสดงอยู่ในขณะนั้น ปุ่มถัดไป (Next) ที่สร้างด้วยวิธีนี้จึงทำงานได้ในทุกหน้าโดยที่คุณไม่ต้องคำนวณหน้าเป้าหมาย

ใน PDFlibPas การดำเนินการตามชื่อจะแนบอยู่กับพื้นที่สี่เหลี่ยมฮอตสปอต (hotspot rectangle) บนหน้าปัจจุบัน อาร์กิวเมนต์จำนวนเต็มตัวที่สี่และห้าจะเลือกประเภทคำสั่งและรูปลักษณ์ภายนอก

// 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

ไม่มีปลายทางที่ต้องคอยซิงค์ข้อมูลให้ตรงกัน ซึ่งนั่นคือประเด็นสำคัญ การดำเนินการตามชื่อจะยังคงทำงานได้เมื่อมีการแทรกหรือลบหน้า เพราะมันไม่ได้ระบุชื่อหน้าตั้งแต่แรก ต่างจากลิงก์ไปที่หน้าโดยเฉพาะ (explicit go-to link) ซึ่งเก็บดัชนีหน้าเป้าหมายเอาไว้ ทำให้คุณต้องเปลี่ยนหมายเลขหน้าใหม่ทันทีที่เอกสารมีขนาดใหญ่ขึ้น

การดำเนินการซ่อน (Hide) และกับดักของอาร์เรย์

การดำเนินการซ่อน (Hide) ตามมาตรฐาน ISO 32000-1 §12.6.4.10 ตารางที่ 196 จะสลับการแสดงผลของฟิลด์ตั้งแต่หนึ่งฟิลด์ขึ้นไป นี่เป็นวิธีที่สะอาดที่สุดในการสร้างพฤติกรรมการแสดงและซ่อนโดยไม่ต้องใช้สคริปต์ และเป็นสิ่งที่คุณต้องการสำหรับลิงก์แสดงรายละเอียด หรือสำหรับแผงควบคุมสองแผงที่ทำงานสลับกันโดยการแสดงแผงหนึ่งจะซ่อนอีกแผงหนึ่ง การดำเนินการนี้จะเก็บเป้าหมายไว้ในรายการ /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) ซึ่งเป็นสาเหตุที่ฟิลด์ย่อยภายในกลุ่มจะต้องถูกอ้างอิงผ่านเส้นทางแบบจุด (dotted path) แบบเต็ม แทนที่จะใช้เพียงชื่อปลายทางเดี่ยวๆ

ImportData: การเติมข้อมูลล่วงหน้าจาก FDF

ในขณะที่การดำเนินการ Hide จัดเรียงสิ่งที่มีอยู่แล้วบนหน้ากระดาษ การดำเนินการนำเข้าข้อมูล (import-data) จะนำค่าจากภายนอกเข้ามา มาตรฐาน ISO 32000-1 §12.6.4.8 ตารางที่ 198 กำหนดให้การดำเนินการนี้เป็นการเติมข้อมูลลงใน AcroForm จากไฟล์ Forms Data Format (FDF) บนดิสก์ นี่คือการดำเนินการที่อยู่เบื้องหลังการควบคุมโหลดข้อมูลตัวอย่างใหม่ (Reload sample data) หรือรีเซ็ตเป็นค่าเริ่มต้น (Reset to defaults) โดยไฟล์ FDF จะถูกส่งไปพร้อมกับ PDF และเก็บค่าฟิลด์ที่เป็นมาตรฐาน การเรียกใช้ฟังก์ชันจะเหมือนกับการดำเนินการอื่นๆ โดยรับพื้นที่สี่เหลี่ยมฮอตสปอต เส้นทางไปยัง FDF และบิตแมสก์ของรูปลักษณ์: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1) ไฟล์ไม่จำเป็นต้องมีอยู่เมื่อสร้าง PDF แต่ต้องมีอยู่เมื่อผู้ใช้คลิก และเครื่องหมายแบ็กสแลชใดๆ ในเส้นทางจะถูกแปลงเป็นรูปแบบสแลชมาตรฐานของ PDF ให้โดยอัตโนมัติ

ข้อจำกัดอย่างหนึ่งที่ควรระบุให้ชัดเจนเนื่องจากมักจะสร้างความแปลกใจบ่อยครั้ง การดำเนินการนำเข้าข้อมูลจะชี้ไปยังไฟล์ภายนอก ดังนั้นจึงไม่ได้รับอนุญาตในมาตรฐาน 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 จะสร้างฟิลด์บนหน้าปัจจุบันและคืนค่าดัชนีของฟิลด์นั้น ประเภทจำนวนเต็มจะเลือกชนิดของฟิลด์ โดย 1 คือ Text, 2 คือ Pushbutton, 3 คือ Checkbox, 4 คือ Radiobutton, 5 คือ Choice, 6 คือ Signature และ 7 คือ Parent ที่มีฟิลด์ย่อยแต่ไม่มีการวาดองค์ประกอบใดๆ บนหน้ากระดาษ ชื่อเรื่องที่คุณส่งไปจะต้องไม่มีเครื่องหมายจุด เนื่องจากจุดเป็นตัวคั่นในชื่อแบบเต็มที่การดำเนินการใช้เพื่ออ้างอิงถึงฟิลด์ย่อย

กลุ่มวิทยุ (Radio groups) และฟอร์มแบบลำดับชั้นถูกสร้างขึ้นโดยการกำหนดฟิลด์ย่อยให้กับฟิลด์หลัก NewChildFormField จะเพิ่มฟิลด์ย่อยภายใต้ฟิลด์หลักที่ระบุชื่อ และสำหรับกรณีวิทยุและตัวเลือก AddFormFieldSub จะเพิ่มตัวเลือกแต่ละตัวและส่งคืนดัชนีชั่วคราวที่คุณใช้เพื่อกำหนดตำแหน่งของแต่ละตัวเลือก เมื่อขั้นตอนการโต้ตอบสิ้นสุดลงและคุณต้องการตรึงฟิลด์เพื่อให้รูปลักษณ์ปัจจุบันกลายเป็นเนื้อหาหน้าถาวร FlattenFormField จะวาดฟิลด์ลงบนหน้าและลบออกจากฟอร์ม หลังจากแบนราบแล้ว ดัชนีของฟิลด์ถัดๆ ไปจะเลื่อนลงไปหนึ่งตำแหน่ง ซึ่งเป็นสิ่งสำคัญอย่างหนึ่งที่ต้องจำไว้หากคุณแบนราบหลายฟิลด์ในลูป

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 จะเปลี่ยนสถานะการมองเห็นและต้องระบุชื่อเรื่องของฟิลด์ โดยการเข้ารหัสแบบสตริงหรืออาร์เรย์จะถูกจัดการให้คุณโดยอัตโนมัติ การดำเนินการนำเข้าข้อมูลจะเข้าถึงไฟล์บนดิสก์และไม่ได้รับอนุญาตใน PDF/A ส่วนการดำเนินการ JavaScript จะรันตรรกะใดๆ และควรแบ่งระหว่างแพ็กเกจฟังก์ชันระดับโกลบอลกับการเรียกใช้งานขนาดเล็กต่อการดำเนินการ เลือกตัวเลือกที่ง่ายที่สุดที่สามารถทำงานได้: การดำเนินการ Hide พกพาง่ายกว่าสคริปต์ที่ตั้งค่าแฟล็กซ่อน และการดำเนินการตามชื่อมีความทนทานกว่าปลายทางของหน้าเว็บที่เก็บไว้ เนื่องจากไม่มีหมายเลขหน้าให้ต้องคอยปรับแต่งดูแล

จากจุดนี้ หัวข้อใกล้เคียงสองหัวข้อจะช่วยเติมเต็มภาพรวม หากฟอร์มเป็นส่วนหนึ่งของเอกสารที่เข้าถึงได้ โครงสร้างแผนผังที่โปรแกรมอ่านหน้าจอใช้จะอธิบายไว้ในบทความของเราเกี่ยวกับ tagged PDF และโครงสร้างการเข้าถึง เมื่อฟอร์มที่กรอกเสร็จสมบูรณ์ต้องถูกล็อกและลงลายมือชื่อ ขั้นตอนการทำงานจะอธิบายไว้ในคู่มือสำหรับเวิร์กเบนช์การปฏิบัติตามข้อกำหนดและการลงชื่อ ทั้งสามตัวสร้างขึ้นบนเอ็นจินเดียวกันซึ่งมาพร้อมกับคลัง PDF สำหรับ Delphi พร้อมกับ API การสร้าง ฟอร์ม และลายเซ็นที่ครอบคลุมอยู่ในบล็อกนี้