Technical Article

ฟอร์ม PDF คำนวณอัตโนมัติใน Delphi ด้วย HotPDF

ใบแจ้งหนี้หรือเอกสาร PDF แสดงค่าใช้จ่ายที่สามารถรวมยอดเงินในแต่ละรายการและจัดรูปแบบวันที่ได้ด้วยตนเองนั้น จำเป็นต้องมีการเชื่อมต่อสิ่งสำคัญสามประการเข้ากับ AcroForm และ HotPDF สามารถเพิ่มทั้งสามสิ่งนี้เข้าไปในไฟล์ที่โหลดใน Delphi ได้แก่: ApplyLoadedFieldCalculation สำหรับการคำนวณผลรวม AFSimple_Calculate, ApplyLoadedFieldValidation สำหรับจัดรูปแบบ AF ของ Acrobat และเมธอดตัวช่วยตรวจสอบความถูกต้อง และ EnsureLoadedFieldAppearanceStream เพื่อบันทึกค่าที่โปรแกรมเปิดอ่าน PDF จะใช้วาดจริง

HotPDF เป็นส่วนประกอบ VCL PDF ดั้งเดิมสำหรับ Delphi และ C++Builder โดยเมธอดทั้งสามนี้จะทำงานกับเอกสารที่คุณเปิดขึ้นด้วย LoadFromFile แทนที่จะสร้างขึ้นใหม่ทั้งหมด เมธอดแต่ละตัวจะรับค่าดัชนีฟิลด์แบบเริ่มต้นที่ศูนย์ (zero-based index) ในอาร์เรย์ AcroForm /Fields — ซึ่งเรียงลำดับแบบเดียวกับที่ส่งคืนจาก GetLoadedFormFieldNames — และเขียนข้อมูลลงในพจนานุกรมฟิลด์โดยตรง ดังนั้นขั้นตอนการทำงานทั้งหมดคือการโหลด, ค้นหาดัชนี, แนบสคริปต์ และ SaveLoadedDocument สำหรับการสร้างชุดฟิลด์ขึ้นมาเองนั้นจะเป็นอีกหัวข้อหนึ่งที่อธิบายไว้ใน adding AcroForm fields to a loaded PDF in Delphi ส่วนหน้านี้จะเน้นเรื่องการกำหนดกระบวนการทางคณิตศาสตร์และการจัดรูปแบบให้กับชุดฟิลด์ที่มีอยู่แล้ว

เหตุใดฟอร์ม PDF จึงต้องใช้สคริปต์เพื่อคำนวณยอดรวมในฟิลด์ของตัวเอง?

เนื่องจาก PDF ไม่มีเลเยอร์ตารางคำนวณ (spreadsheet layer) ฟิลด์ข้อความจะเก็บค่าเป็นสตริง ไม่ใช่สูตรคำนวณ และไม่มีสิ่งใดในไฟล์ที่จะคำนวณผลรวมใหม่เมื่อรายการเปลี่ยนไป เว้นแต่จะมีการแนบสคริปต์เพื่อดำเนินการดังกล่าว กลไกการทำงานนี้คือพจนานุกรมการดำเนินการเพิ่มเติมของฟิลด์ /AA (ISO 32000-1 §12.7.5) ซึ่งจะจับคู่เหตุการณ์ทริกเกอร์กับการดำเนินการต่างๆ เช่น ทริกเกอร์การกดแป้นพิมพ์ (K) เมื่อมีการป้อนตัวอักษร, ทริกเกอร์รูปแบบสำหรับการแสดงผล, ทริกเกอร์การตรวจสอบความถูกต้อง (V) เมื่อยืนยันข้อมูล และทริกเกอร์การคำนวณ (C) ที่จะทำงานเมื่อฟิลด์ใดๆ ในฟอร์มเกิดการเปลี่ยนแปลง การดำเนินการแต่ละตัวคือการดำเนินการ JavaScript /S /JavaScript (ISO 32000-1 §12.6.4.14) และสคริปต์ที่รันอยู่นี้คือสิ่งที่จะทำหน้าที่บวกเลขหรือจัดรูปแบบใหม่จริง ตัวโปรแกรม Acrobat จะมีไลบรารีตัวช่วยสำเร็จรูปมาให้ — เช่น AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx และอื่นๆ — ดังนั้นสคริปต์ที่แนบไปจึงมักเป็นการเรียกใช้คำสั่งบรรทัดเดียวไปยังไลบรารีนั้น แทนที่จะเขียนโค้ดขึ้นมาใหม่เองทั้งหมด

มีรายละเอียดข้อหนึ่งที่เป็นตัวตัดสินว่าฟอร์มที่มีหลายขั้นตอนจะแสดงคำตอบที่ถูกต้องหรือไม่: ลำดับการคำนวณ Acrobat จะรันทุกฟิลด์ที่มีการดำเนินการคำนวณ และจะรันตามลำดับที่แสดงในอาร์เรย์ AcroForm /CO ไม่ใช่เรียงตามลำดับหน้า เมธอด ApplyLoadedFieldCalculation จะแนบการดำเนินการคำนวณเข้ากับฟิลด์ที่คุณระบุ เมื่อฟิลด์คำนวณหนึ่งส่งต่อค่าไปยังอีกฟิลด์หนึ่ง — เช่น ยอดรวมย่อยที่บรรทัดภาษีนำไปอ่านค่า แล้วยอดรวมสุทธิอ่านค่าจากทั้งสองส่วน — คุณต้องตรวจสอบให้แน่ใจว่าลำดับ /CO ได้แสดงรายการอินพุตแต่ละตัวไว้ก่อนหน้าฟิลด์ที่นำค่าไปประมวลผล มิฉะนั้นยอดรวมสุทธิจะอ่านค่ายอดรวมย่อยที่ล้าสมัยและแสดงผลล่าช้าไปหนึ่งขั้นตอน

จะทำให้ฟอร์ม PDF คำนวณยอดรวมโดยอัตโนมัติได้อย่างไร?

ApplyLoadedFieldCalculation จะรับค่าดัชนีฟิลด์ปลายทาง, ประเภทการดำเนินการ และชื่อของฟิลด์ต้นทางที่ต้องการนำมารวมกัน HotPDF จะเขียน AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) ลงในการดำเนินการคำนวณของฟิลด์ปลายทาง และใช้ Enum THPDFFieldCalcOp เพื่อเลือกฟังก์ชัน: โดย fccSum, fccAverage, fccProduct, fccMinimum และ fccMaximum จะตรงกับคำสั่ง SUM, AVG, PRD, MIN และ MAX ของ Acrobat ส่วนชื่อต้นทางคือชื่อฟิลด์ AcroForm ซึ่งเป็นสตริงชุดเดียวกับที่ GetLoadedFormFieldNames ส่งกลับมา ดังนั้นการคำนวณยอดรวมของรายการสินค้าสามรายการและค่าจัดส่งจึงสามารถดำเนินการได้ในการเรียกคำสั่งเพียงครั้งเดียว

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

การเรียกเมธอด EnsureLoadedFieldAppearanceStream สำหรับผลรวมไม่ใช่เรื่องของการแสดงผลให้สวยงามเท่านั้น การคำนวณจะเกิดขึ้นภายในโปรแกรมเปิดอ่าน PDF ดังนั้นจนกว่าจะมีโปรแกรมที่รองรับการรันสคริปต์มาเปิดไฟล์ ฟิลด์ผลรวมจะไม่มีเนื้อหาที่เรนเดอร์ในตัวเอง การบันทึกสตรีมการแสดงผล (appearance stream) จะช่วยให้ฟิลด์มีตัวเลขแสดงในระหว่างนั้น ซึ่งงานพิมพ์หรือโปรแกรมสร้างรูปภาพตัวอย่าง (thumbnail generator) จะสามารถดึงข้อมูลส่วนนี้ไปใช้ได้ทันทีโดยไม่ต้องรันสคริปต์

การจัดรูปแบบและการตรวจสอบความถูกต้องของฟิลด์ด้วยชุดตัวช่วย AF

ApplyLoadedFieldValidation จะแนบสคริปต์จัดรูปแบบ, การกดปุ่ม และการตรวจสอบความถูกต้องในการเรียกใช้งานครั้งเดียว และ THPDFFieldValidationKind ที่คุณส่งผ่านจะเป็นตัวกำหนดว่าต้องเชื่อมต่อกับตัวช่วยตัวใดของ Acrobat หากส่งค่า fvkCurrency ตัว HotPDF จะเขียน AFNumber_Format(2,0,0,0,"$ ",true) เป็นสคริปต์จัดรูปแบบคู่กับ AFNumber_Keystroke ส่วน fvkNumber จะใช้ "." เป็นอาร์กิวเมนต์ตัวคั่น, fvkPercentage จะส่งออก AFPercent_Format และ fvkDate จะส่งออก AFDate_FormatEx("dd/mm/yyyy") เว้นแต่คุณจะระบุหน้ากาก (mask) ของคุณเองในอาร์กิวเมนต์ DateFormat ส่วนรูปแบบพิเศษคือ — fvkZipCode, fvkSSN และ fvkPhone — จะจับคู่กับ AFSpecial_Format พร้อมรหัสที่ Acrobat จองไว้สำหรับแต่ละแบบ และ fvkArbitraryMask จะรับสตริงหน้ากากผ่านอาร์กิวเมนต์ Mask สำหรับรูปแบบอื่นที่การตั้งค่าล่วงหน้าไม่ครอบคลุม นอกจากนี้ในประเภทตัวเลข HotPDF จะแนบสคริปต์ AFRange_Validate ไปด้วยเพื่อให้ปฏิเสธข้อมูลที่อยู่นอกช่วงที่กำหนดเมื่อมีการยืนยันข้อมูล

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

ตัวช่วย AF ครอบคลุมรูปแบบการทำงานทั่วไปส่วนใหญ่แล้ว คุณจึงไม่จำเป็นต้องเขียน JavaScript ขึ้นมาเอง เมื่อฟิลด์ต้องการตรรกะที่การตั้งค่าล่วงหน้าไม่มี — เช่น การตรวจสอบค่าควบคุม (checksum), กฎข้ามฟิลด์ หรือ regex แบบกำหนดเอง — ให้แนบสคริปต์ดิบด้วยตนเองโดยใช้เมธอดการดำเนินการระดับฟิลด์ที่อธิบายใน building AcroForm fields and actions with HotPDF ซึ่งจะรับสตริง JavaScript โดยตรง ทั้งสองแนวทางนี้จะเขียนข้อมูลลงในพจนานุกรม /AA เดียวกัน ดังนั้นคุณจึงสามารถกำหนดรูปแบบสกุลเงินสำเร็จรูปให้กับฟิลด์หนึ่ง และเขียนสคริปต์ตรวจสอบความถูกต้องเองกับอีกฟิลด์หนึ่งในเอกสารเดียวกันได้

เหตุใดคุณจึงต้องสร้างสตรีมการแสดงผลใหม่?

เนื่องจากค่าสตริงข้อมูลและพิกเซลภาพที่โปรแกรมเปิดอ่าน PDF วาดแสดงผลนั้นเป็นคนละส่วนกันในระบบ PDF และการเปลี่ยนค่าส่วนหนึ่งไม่ได้ทำให้เกิดการเปลี่ยนแปลงในอีกส่วนหนึ่งโดยอัตโนมัติ ฟิลด์เก็บค่าข้อมูลไว้ใน /V แต่สิ่งที่โปรแกรมเปิดอ่านเรนเดอร์แสดงออกมาจะมาจากสตรีมการแสดงผลของฟิลด์คือ /AP /N หากแก้ไข /V ผ่านโครงสร้างออบเจกต์ โปรแกรมเปิดอ่านที่ทำงานอย่างเข้มงวดจะยังคงแสดงผลภาพแบบเดิมจนกว่าจะมีกระบวนการสร้างรูปขึ้นใหม่ พจนานุกรมฟอร์มจะมีแฟล็ก /NeedAppearances เพื่อขอให้โปรแกรมเปิดอ่านสร้างรูปแบบการแสดงผลใหม่ทั้งหมดเมื่อเปิดไฟล์ ซึ่งโปรแกรม Acrobat จะปฏิบัติตามคำขอนี้ — ทว่าโปรแกรมเรนเดอร์ที่ฝังในเบราว์เซอร์, โปรแกรมเปิดอ่านในมือถือ, เซิร์ฟเวอร์การพิมพ์ และโปรแกรมสร้างภาพตัวอย่าง มักจะละเลยแฟล็กนี้และวาดภาพตามสตรีมใดๆ ที่มีอยู่แล้ว หรือไม่แสดงอะไรเลย เมธอด EnsureLoadedFieldAppearanceStream จะเข้ามาช่วยแก้ปัญหานี้โดยทำหน้าที่สร้าง /AP /N Form XObject ขึ้นมาเองจากข้อมูลฟิลด์ /DA, /V และ /Rect เพื่อให้ภาพที่เรนเดอร์แล้วแนบไปกับไฟล์ แทนที่จะต้องพึ่งพาโปรแกรมเปิดอ่านวาดภาพให้ใหม่

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

จุดที่สคริปต์ AF จะหยุดทำงาน

สคริปต์ตระกูล AF* ทุกตัวต้องพึ่งพาโปรแกรมเปิดอ่านที่มีเอนจิน JavaScript แต่โปรแกรมเปิดอ่านส่วนใหญ่ไม่มีเอนจินดังกล่าว มีเพียง Acrobat และโปรแกรมเดสก์ท็อปบางตัวเท่านั้นที่สามารถรันตัวช่วยเหล่านี้ได้ โปรแกรมเปิดอ่าน PDF ที่ฝังใน Chrome, Edge, Firefox และแพลตฟอร์มมือถือยอดนิยม ตลอดจนกระบวนการแปลงแรสเตอร์ฝั่งเซิร์ฟเวอร์และระบบการพิมพ์ จะไม่มีการรัน JavaScript ของฟอร์มเลย ในกรณีนี้กระบวนการคำนวณจะไม่ทำงานและรูปแบบจะไม่ถูกนำมาใช้ — ฟิลด์จะเพียงแค่แสดงสตรีมการแสดงผลที่ได้รับการบันทึกไว้ล่าสุดเท่านั้น ซึ่งนี่คือเหตุผลหลักว่าทำไมการบันทึก /AP /N จึงมีความสำคัญ โปรดมองว่าเลเยอร์ AF* เป็นเพียงความสะดวกสบายที่มอบให้กับผู้ใช้กลุ่มย่อยที่ใช้โปรแกรมเปิดอ่านที่รันสคริปต์ได้เท่านั้น ไม่ใช่หลักประกันว่าการคำนวณจะทำงานได้เสมอ ผลรวมใดๆ ที่ต้องการความถูกต้องบนเซิร์ฟเวอร์ — เช่น ยอดเงินใบแจ้งหนี้เพื่อเรียกเก็บเงิน หรือตัวเลขภาษีสำหรับการยื่นเอกสาร — จะต้องถูกคำนวณใหม่บนฝั่งของคุณหลังจากได้รับฟอร์มกลับคืนมา เนื่องจากคุณไม่สามารถคาดเดาได้ว่าโปรแกรมเปิดอ่านที่กรอกข้อมูลนั้นจะรันสคริปต์ของคุณแม้แต่บรรทัดเดียว

ยังมีอีกกรณีหนึ่งที่ควรกล่าวถึง: ฟอร์มที่ส่งมาในรูปแบบ XFA ฟอร์ม XFA แบบไดนามิกจะเก็บเอนจินการคำนวณของตัวเองไว้ในโครงสร้างข้อมูล XML ซึ่งตัวช่วย AF* จะไม่รู้เรื่องด้วยเลย ดังนั้นการแนบ AFSimple_Calculate เข้ากับฟิลด์ XFA จึงไม่มีผลใดๆ คุณต้องแปลงฟอร์ม XFA ให้เป็น AcroForm ธรรมดาเสียก่อนตามที่อธิบายไว้ใน XFA to AcroForm flattening in Delphi แล้วจึงสามารถนำเมธอดทั้งสามตัวที่กล่าวถึงที่นี่ไปใช้งานกับฟิลด์ AcroForm ที่ได้ เช่นเดียวกับการใช้งานกับฟอร์มอื่นๆ ที่โหลดเข้ามา

ฟอร์ม AcroForm ที่คำนวณและจัดรูปแบบโดยอัตโนมัติสรุปได้เป็นการเรียกใช้งานสามคำสั่งบนเอกสารที่โหลดเข้ามา — แนบสคริปต์การคำนวณ, แนบสคริปต์รูปแบบและการตรวจสอบความถูกต้อง, บันทึกภาพการแสดงผล — จากนั้นเรียกใช้ SaveLoadedDocument ตัว API ฟอร์มที่โหลดซึ่งแสดงที่นี่เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder ซึ่งมีคำอธิบายโครงสร้าง Enumeration ของ THPDFFieldValidationKind และ THPDFFieldCalcOp ไว้อย่างครบถ้วนพร้อมส่วนการทำงาน AcroForm ที่เหลือ