บทความเทคนิค

นิพจน์เส้นทาง XFA SOM ใน Delphi: ข้อมูลฟอร์มแบบไดนามิก

losLab PDF Library อ่านและเขียนโหนดข้อมูลที่ซ้ำกันในฟอร์ม XFA แบบไดนามิกผ่านนิพจน์เส้นทาง XFA 3.3 SOM: ฟังก์ชัน GetXFAFormFieldValue และ SetXFAFormFieldValue รองรับรูท $data, $record และ !data, ไวลด์การ์ดระบุรายการซ้ำ [*], ตัวเลือกโหนดลูกหลาน (descendant) และตัวเลือกโหนดลูกไวลด์การ์ด (child-wildcard), คุณสมบัติ parent, ตัวเลือกคลาส #dataGroup/#dataValue และตัวกรอง (predicates) รูปแบบ FormCalc อย่างง่าย เพื่อให้โปรแกรม Delphi อัปเดตรายการใบแจ้งหนี้ทุกบรรทัดในฟอร์มภาษีของรัฐบาลได้ในการเรียกครั้งเดียว บทความนี้เป็นเอกสารอ้างอิงสำหรับการระบุตำแหน่งข้อมูล สำหรับการย้ายชุดข้อมูลทั้งหมดระหว่างไฟล์ โปรดดูที่บทความคู่กันเกี่ยวกับการแลกเปลี่ยนข้อมูลฟอร์ม FDF, XFDF และ XFA

ปัญญานี้จะเกิดขึ้นทันทีที่ฟอร์มไม่ได้เป็นโครงสร้างแบบแบนราบอีกต่อไป แบบฟอร์มภาษีมูลค่าเพิ่มหรือการสำแดงของศุลกากรที่สร้างขึ้นเป็นฟอร์ม XFA แบบไดนามิกจะไม่มีฟิลด์ 12 ฟิลด์ที่ใช้ชื่อตั้งแต่ Total_Price_1 ถึง Total_Price_12 แต่มันจะมีซับฟอร์ม Detail เพียงหนึ่งอันที่ประกาศไว้ใน Template DOM และจะถูกสร้างขึ้นซ้ำตามความต้องการของข้อมูล ค่าเหล่านี้จะอยู่ในทรีที่สอง คือ Data DOM ภายในแพ็กเก็ตชุดข้อมูล โดยแต่ละรายการแถวข้อมูลจะเป็นองค์ประกอบ <Detail> ที่เกิดขึ้นซ้ำภายใต้ <Receipt> ชื่อฟิลด์แบนๆ ไม่สามารถระบุถึง "ราคาในบรรทัดที่สาม" หรือ "ทุกบรรทัดที่มีมูลค่าเกิน 200" ได้ ซึ่งนั่นคือบทบาทหน้าที่ที่บท Scripting Object Model ของข้อกำหนด Adobe XFA 3.3 มอบให้กับนิพจน์ SOM และเป็นไวยากรณ์ที่ XFA field-value APIs ของ losLab PDF Library ใช้งาน

PDF Library for Delphi: แผนภาพนี้เปรียบเทียบ XFA template DOM ที่ประกาศ subform Detail ครั้งเดียว กับ Data DOM ที่ทำซ้ำมันสำหรับเรกคอร์ดข้อมูลทุกชุดที่มาถึง
ฟอร์ม XFA แบบไดนามิกประกาศ subform แต่ละตัวเพียงครั้งเดียวใน template และทำซ้ำในแพ็กเกจ datasets ขณะข้อมูลมาถึง

จะระบุตำแหน่งโหนดข้อมูล XFA จาก Delphi ได้อย่างไร?

ทุกเส้นทางชุดข้อมูลเริ่มต้นจากรูท และ losLab PDF Library ยอมรับการสะกดมาตรฐานสลับกันได้: $data เป็นรูปแบบย่อของ xfa.datasets.data ในข้อกำหนด XFA 3.3, !data เป็นรูปแบบย่อที่มีรูทอยู่ที่ xfa.datasets และในแพ็กเก็ตที่ไม่ได้ใช้การประมวลผลเรกคอร์ด $record จะดึงข้อมูลไปยังเรกคอร์ดข้อมูลชั้นนอกสุด ซึ่งเป็นองค์ประกอบแรกภายใต้โหนดข้อมูล ส่วนฝั่งเทมเพลต APIs เช่น SetXFAFormFieldAccess จะยอมรับ $template และ xfa.template ในลักษณะเดียวกัน เส้นทางย่อยอาจคั่นด้วยเครื่องหมายจุดหรือเครื่องหมายทับ (เช่น $data.Receipt.Tax หรือ $data/Receipt/Tax) ซึ่งมีความสำคัญเนื่องจาก GetXFAFormFieldNames จะแจกแจงชื่อฟิลด์เป็นเส้นทางคั่นด้วยเครื่องหมายทับซึ่งสามารถนำไปใช้ใน APIs จัดการค่าและเทมเพลตได้ทันที ดัชนีระบุรายการซ้ำจะเริ่มจากศูนย์ตามข้อกำหนด ดังนั้น Detail[0] คือแถวแรก กฎการหลีกเลี่ยง (escaping) สองข้อจะช่วยให้ตั้งชื่อพิเศษได้: ในเส้นทางที่คั่นด้วยจุด \. หมายถึงเครื่องหมายจุดปกติ (เช่น Line\.Item) ขณะที่เส้นทางที่คั่นด้วยเครื่องหมายทับจะถือว่าจุดเป็นอักขระทั่วไป ข้อมูลธุรกิจที่มีคำนำหน้าเนมสเปซ XML (XML namespace prefix) ของตัวเองจะถูกค้นหาด้วยชื่อท้องถิ่น ดังนั้น <m:Receipt> จะยังคงถูกพบด้วยคำว่า Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Equivalent addresses for the same data node
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Slash form with a zero-based occurrence index
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

จะกรอกแถวที่ซ้ำกันในฟอร์ม XFA ผ่านการเขียนโปรแกรมได้อย่างไร?

ไวลด์การ์ด [*] สำหรับรายการซ้ำคือเครื่องมือสำหรับการจัดการแบบชุด ในขณะที่ดัชนีตัวเลขจะเลือกเพียงโหนดเดียว [*] จะเลือกโหนดพี่น้องที่มีชื่อเดียวกันทั้งหมด ดังนั้น $data.Receipt.Detail[*].Total_Price จะระบุตำแหน่งของฟิลด์ราคาในทุกบรรทัดรายการพร้อมกัน ในการอ่าน GetXFAFormFieldValue จะส่งกลับค่าของโหนดที่ตรงกันทั้งหมดโดยเชื่อมด้วยตัวคั่น |; ในการเขียน SetXFAFormFieldValue จะอัปเดตทุกโหนดที่ตรงกันด้วยค่าเดียวกันและส่งกลับค่า 1 เมื่อสำเร็จ เส้นทางที่ไม่ตรงกับสิ่งใดเลยจะอ่านค่ากลับมาเป็นสตริงว่าง ซึ่งเป็นวิธีง่ายๆ ในการตรวจสอบว่าสาขาย่อยมีอยู่หรือไม่ก่อนที่จะเขียนข้อมูลลงไป

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // All Detail rows at once: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Reset one column across every repeated row in a single call
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

ตัวเลือกโหนดลูกหลาน, ตัวเลือกโหนดลูกไวลด์การ์ด และตัวเลือกโหนดแม่

ตัวเลือกโครงสร้างสามแบบจะครอบคลุมกรณีที่คุณทราบชื่อฟิลด์แต่ไม่ทราบระดับความลึกที่แน่นอน ตัวเลือกโหนดลูกหลาน .. จะจับคู่ที่ระดับความลึกใดก็ได้ใต้โหนดปัจจุบัน: $data..Total_Price จะค้นหา Total_Price แรกในทุกระดับใต้รูทข้อมูล และการเขียนผ่านเส้นทางลูกหลานจะอัปเดตโหนดแรกที่จับคู่ได้นี้ ข้อสังเกตขอบเขต: ดัชนีระบุรายการซ้ำที่อยู่หลังการจับคู่ลูกหลานจะไม่ข้ามไปยังสาขาพี่น้อง ดังนั้นหาก $data..Total_Price[0] สิ้นสุดที่แถว Detail แรก ค่าของ $data..Total_Price[1] จะส่งกลับค่าว่างแทนที่จะข้ามไปยังแถวถัดไป; ให้ใช้ Detail[*] เมื่อคุณต้องการทั้งหมด ไวลด์การ์ดของโหนดลูก .* จะจับคู่กับโหนดลูกโดยตรงทุกโหนดและปล่อยให้เซกเมนต์ที่เหลือกรองข้อมูล: $data.Receipt.*.Total_Price จะเข้าถึง Total_Price ภายในแต่ละสาขาลูกของ Receipt โดยไม่ไปเลือกฟิลด์สรุปที่มีชื่อเดียวกันซึ่งอยู่บน Receipt โดยตรง สุดท้าย คุณสมบัติ parent จะปีนขึ้นไปหนึ่งระดับ ซึ่งข้อกำหนด XFA 3.3 แยกความแตกต่างจาก .. อย่างชัดเจน: $data.Receipt.Detail[1].parent.Tax จะเริ่มที่รายการที่สอง ปีนขึ้นไปที่ Receipt และไปสิ้นสุดที่ค่า Tax ที่เป็นพี่น้องของมัน

ตัวเลือกคลาสและตัวกรอง: #dataGroup, #dataValue, .[expression]

ไวยากรณ์ #class จะระบุตำแหน่งโหนด Data DOM ตามคลาสของวัตถุแทนการระบุตามชื่อ ซึ่งเป็นแนวทางปฏิบัติที่มีประโยชน์เมื่อแท็ก XML มีอักขระที่สะกดยากในฐานะชื่อสคริปต์ losLab PDF Library จะกำหนดให้ #dataGroup สอดคล้องกับองค์ประกอบที่มีโหนดลูก และกำหนดให้ #dataValue สอดคล้องกับองค์ประกอบปลายทางที่ไม่มีโหนดลูก และทั้งสองแบบสามารถใช้งานร่วมกับดัชนีระบุรายการซ้ำแบบตัวเลขหรือ [*]: $data.Receipt.#dataGroup[0].#dataValue[0] จะเลือกค่าแรกภายในกลุ่มแรกภายใต้ Receipt ตัวเลือกตัวกรอง .[expression] จะกรองโหนดพี่น้องที่มีชื่อเดียวกันตามเนื้อหา ตัวกรองที่รองรับคือการเปรียบเทียบค่าของโหนดลูกอย่างง่ายเทียบกับค่าคงที่ ด้วยตัวดำเนินการเปรียบเทียบเช่น >, < และ <=; เมื่อตัวกรองตรงกับหลายแถว การอ่านจะส่งกลับค่าที่คั่นด้วย | และการเขียนจะอัปเดตทุกแถวที่ตรงกัน

PDF Library for Delphi: แผนภาพนี้แยกชิ้นส่วน SOM path expression บน XFA datasets โดยติดป้ายราก dataset, ช่วงที่คั่นด้วยตัวคั่น, ดัชนีเกิดขึ้นที่เริ่มจากศูนย์ และค่า leaf ที่เป็นเป้าหมาย
ราก ตัวคั่น และดัชนีลำดับการเกิดที่เริ่มนับจากศูนย์ประกอบกันเป็นพาธ SOM โดยมีการสะกดหลายแบบที่ชี้ไปยังโหนดเดียวกัน
// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

ฟีเจอร์ XFA SOM ใดบ้างที่ไม่รองรับ?

การรองรับ SOM ใน losLab PDF Library ถูกจำกัดขอบเขตไว้เฉพาะเส้นทางของฟิลด์ชุดข้อมูลและเทมเพลตเท่านั้น และมีขอบเขตที่ชัดเจนมากกว่าการพยายามทำอย่างสุดความสามารถ การรู้ขอบเขตเหล่านี้ล่วงหน้าจะช่วยประหยัดเวลาในการดีบักเส้นทางที่ส่งคืนสตริงว่างอย่างเงียบๆ

  • ตัวกรอง (Predicates) ไม่รันคำสั่ง FormCalc หรือ JavaScript ใดๆ: ไม่มีการเรียกใช้ฟังก์ชัน (ตัวกรอง contains(...) จะส่งคืนค่าว่าง), ไม่มีนิพจน์บูลีนแบบผสม, รองรับเฉพาะการเปรียบเทียบ child op literal เดี่ยวๆ เท่านั้น
  • $record ทำงานในแพ็กเก็ตที่ไม่มีการประมวลผลเรกคอร์ดเท่านั้น; ไม่มีการนำการแบ่งหน้าเรกคอร์ดของ dataWindow และการหมุนเวียนกลุ่มเรกคอร์ดมาใช้งาน
  • การดึงข้อมูลจะกระทำกับ Data DOM และ Template DOM โดยตรง; จะไม่มีการวิเคราะห์เชิงเปรียบเทียบของ Form DOM หรือมุมมองรวมที่โปรแกรมอ่านสร้างขึ้นในเวลาทำงาน (runtime)
  • จะไม่มีการนำการโหลดแอตทริบิวต์ของ Data DOM มาใช้งาน; ไลบรารีจะอ่านแพ็กเก็ตชุดข้อมูลในฐานะ XML ดิบ ดังนั้นเส้นทางจะระบุตำแหน่งไปยังองค์ประกอบต่างๆ ไม่ใช่แอตทริบิวต์ที่ถูกยกฐานะให้เป็นโหนด

ข้อจำกัดเหล่านี้ไม่ค่อยส่งผลกระทบต่องานกรอกข้อมูลแบบกลุ่ม เนื่องจากโปรแกรมกรอกข้อมูลจะระบุข้อมูลตามโครงสร้างมากกว่าที่จะใช้ตรรกะของสคริปต์ เมื่อจำเป็นต้องใช้จริงๆ ทางออกคือการทำงานในระดับแพ็กเก็ต: ให้อ่าน XML ชุดข้อมูลทั้งหมด ดำเนินการแปลงใน Delphi แล้วเขียนข้อมูลกลับลงไป

PDF Library for Delphi: แผนภาพนี้สำรวจตัวเลือก SOM เชิงโครงสร้างหกแบบสำหรับข้อมูล XFA ซ้ำ: ไวลด์การ์ดเกิดขึ้น, การค้นหาทายาท, ไวลด์การ์ดบุตร, ขั้นไปยังแม่, class selector และ predicate เนื้อหา
wildcard, ทายาท, ขั้นพ่อแม่, class selector และ predicate เชิงเนื้อหาแต่ละตัวเติมช่องว่างการอ้างแอดเดรสที่ต่างกัน

การเขียนแพ็กเก็ตทั้งหมดด้วย SetXFAFromString

SetXFAFromString คือจุดเข้าใช้งานในระดับแพ็กเก็ต: โดยจะติดตั้งสตริง XDP ที่สมบูรณ์ ทั้งเทมเพลตและชุดข้อมูลพร้อมกัน พร้อมสร้างคอนเทนเนอร์ AcroForm ในเอกสารใหม่เมื่อไม่มีคอนเทนเนอร์อยู่ และยอมรับแพ็กเก็ตที่เข้ารหัส UTF-16 ที่มีเครื่องหมายบอกลำดับไบต์ (byte-order marks) ตลอดจน UTF-8 รูปแบบการใช้งานทั่วไปคือการเก็บรักษา XDP ที่ออกโดยหน่วยงานไว้เป็นหลัก, โหลดมันด้วย SetXFAFromString จากนั้นจึงเรียกใช้ SetXFAFormFieldValue ที่ระบุตำแหน่งด้วย SOM สำหรับค่าในใบแจ้งหนี้ที่เปลี่ยนแปลงได้ก่อนบันทึก เนื่องจาก XFA เป็นหนึ่งในโมเดลฟอร์มสองแบบที่ PDF สามารถเก็บได้ ฝั่ง AcroForm แบบคลาสสิกจึงมีระบบอัตโนมัติของตัวเอง ซึ่งครอบคลุมไว้ในบทความเกี่ยวกับการทำงานของฟอร์มแบบโต้ตอบและ JavaScript

XFA field-value, enumeration และ APIs จัดการแพ็กเก็ตที่แสดงไว้ที่นี่มีจัดส่งมาใน losLab PDF Library สำหรับ Delphi, C# และ VB.NET; โดยในหน้าผลิตภัณฑ์จะมีข้อมูลอ้างอิงการจัดการฟอร์มฉบับเต็มไว้บริการ