Technical Article

การแลกเปลี่ยนข้อมูลฟอร์ม FDF, XFDF และ XFA ใน Delphi

losLab PDF Library เคลื่อนย้ายข้อมูลฟอร์มเข้าและออกจาก PDF ได้สามวิธีที่แตกต่างกัน ได้แก่ FDF และ XFDF สำหรับค่าฟิลด์ AcroForm, รูปแบบเดียวกันทั้งสองนี้สำหรับความคิดเห็นของคำอธิบายประกอบ (annotation comments) และแพ็กเก็ต XFA XDP ทั้งหมดที่เขียนลงในฟอร์มโดยตรง โดยมี ExportFormDataToXFDF, ImportFormDataFromXFDF, ImportAnnotationsFromFDF, ExportAnnotationsToXFDF และ SetXFAFromString เป็นจุดเริ่มต้นการทำงาน และแต่ละฟังก์ชันมีทั้งเวอร์ชันที่ระบุไฟล์และเวอร์ชันที่ระบุเป็นสตริง

สาเหตุที่มีเมธอดจำนวนมากเนื่องจากข้อมูลฟอร์ม PDF ไม่ใช่สิ่งเดียวกันทั้งหมด AcroForm ที่กรอกแล้วจะมีค่าฟิลด์ และอาจมีความคิดเห็นในการตรวจทานรวมอยู่ด้วย ส่วนฟอร์ม XFA แบบเก่านั้นจะฝังคำอธิบายแอปพลิเคชัน XML ทั้งหมดซึ่งไม่มีความเกี่ยวข้องกับฟอร์มแบบอื่นเลย losLab PDF Library จงใจแยกการทำงานทั้งสามส่วนนี้ออกจากกันในแต่ละ API เนื่องจากหากรวมเข้าด้วยกันจะทำให้ต้องฝืนใช้แบบจำลองข้อมูล (data model) ที่ไม่ถูกต้องกับเมธอดอย่างน้อยสองส่วน การตัดสินใจเลือกใช้ประเภทใดจึงเป็นขั้นตอนการออกแบบขั้นแรกสุด ซึ่งปกติจะถูกกำหนดโดยระบบรับข้อมูลฝั่งตรงข้ามที่คุณต้องการส่งข้อมูลไปให้

ความแตกต่างระหว่างข้อมูลฟอร์ม FDF, XFDF และ XFA คืออะไร?

FDF และ XFDF เก็บข้อมูลเดียวกันในรูปแบบไวยากรณ์สองแบบที่ต่างกัน ส่วน XFA เป็นอีกระบบหนึ่งต่างหาก FDF คือเอกสารไวยากรณ์ PDF ขนาดเล็ก (ISO 32000-2 §12.7.8): เป็นอาร์เรย์ /Fields ของพจนานุกรม << /T (name) /V (value) >> โดยมีการใช้เครื่องหมายหลีก (escape) สตริงแบบเดียวกับที่ใช้ในสตริงลิเทอรัลภายใน PDF สำหรับ XFDF เป็นรูปแบบ XML ของข้อมูลเดียวกัน (ISO 19444) ซึ่งเป็นโครงสร้างต้นไม้ <fields> ที่ Acrobat และระบบหลังบ้านของฟอร์มส่วนใหญ่อ่านและเขียนได้โดยตรง ส่วน XFA ไม่ใช่ทั้งสองแบบข้างต้น แต่เป็นเทมเพลต XML Forms Architecture และข้อมูลของมัน ซึ่งจัดเก็บในรูปแบบ XML Data Package (XDP) ที่ PDF อ้างอิงจาก /AcroForm/XFA เลือก FDF หรือ XFDF เมื่อคุณต้องการทำงานร่วมกันในส่วนของค่าฟิลด์ และเลือกใช้ XFA เฉพาะเมื่อคุณต้องบำรุงรักษาเอกสารที่เขียนขึ้นในรูปแบบฟอร์ม XFA ตั้งแต่แรกเท่านั้น

ภายใน FDF และ XFDF นั้น losLab PDF Library ได้แยกความแตกต่างอีกประการหนึ่งคือ: ค่าฟิลด์ กับ ความคิดเห็นของคำอธิบายประกอบ โดยกลุ่มเมธอดของข้อมูลฟอร์ม (ExportFormDataToFDF, ImportFormDataFromFDF, ExportFormDataToXFDF, ImportFormDataFromXFDF) จะอ่านและเขียนเฉพาะทรีย่อย (subtree) /Fields เท่านั้น ส่วนกลุ่มเมธอดของคำอธิบายประกอบ (ExportAnnotationsToFDF, ImportAnnotationsFromFDF, ExportAnnotationsToXFDF, ImportAnnotationsFromXFDF) จะอ่านและเขียนทรีย่อย /Annots แทน ซึ่งเทียบเท่ากับฟังก์ชัน Export Comments ใน Acrobat โดยที่สองกลุ่มนี้ไม่มีการทำงานทับซ้อนกัน ดังนั้นการส่งออกค่าฟิลด์จะไม่ดึงเอาบันทึกการตรวจทานอื่นๆ ติดไปด้วย และการนำเข้าความคิดเห็นจะไม่กระทบต่อค่าที่ผู้ใช้กรอกไว้แล้ว ส่วนลักษณะการทำงานของวิดเจ็ต (widget) เมื่อคลิกหรือคำนวณค่าใหม่จะเป็นอีกเรื่องหนึ่งที่กล่าวถึงในเอกสารอธิบายเรื่อง interactive form actions and JavaScript

จะป้อนข้อมูลฟอร์ม PDF จาก FDF หรือ XFDF ใน Delphi ได้อย่างไร?

โหลดเอกสาร เรียกใช้เมธอดนำเข้าหนึ่งตัว แล้วบันทึก ImportFormDataFromXFDF และ ImportFormDataFromFDF จะวิเคราะห์ข้อมูลฟอร์มที่เข้ามา จับคู่ทุกรายการเข้ากับฟิลด์ AcroForm ด้วยชื่อเต็มที่ถูกต้อง (fully qualified name) กำหนดค่า และส่งคืนจำนวนฟิลด์ที่ได้รับการอัปเดตจริง โดยทั้งสองเมธอดจะอัปเดตเฉพาะฟิลด์ที่มีอยู่แล้วใน PDF ปลายทางเท่านั้น จะไม่มีการสร้างฟิลด์ใหม่สำหรับชื่อที่ไม่มีในแบบฟอร์ม เพื่อป้องกันไม่ให้ไฟล์ข้อมูลที่แปลกปลอมหรือเป็นอันตรายเข้ามาเพิ่มฟิลด์ในฟอร์มของคุณโดยไม่รู้ตัว

var
  Lib: TPDFlib;
  FieldsSet: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('application-blank.pdf', '');
    // XFDF produced by a case-management system, already UTF-8 on disk
    FieldsSet := Lib.ImportFormDataFromXFDF('applicant-1042.xfdf');
    if FieldsSet > 0 then
      Lib.SaveToFile('application-filled.pdf');
  finally
    Lib.Free;
  end;
end;

มีรายละเอียดสองประการที่เป็นตัวกำหนดว่าข้อมูลจะยังคงความถูกต้องครบถ้วนหลังการแปลงหรือไม่ ประการแรกคือ การหลีกอักขระ (escaping): FDF จะจัดเก็บค่าต่างๆ เป็นสตริงลิเทอรัลของ PDF ดังนั้นค่าที่มีวงเล็บ, แบ็กสแลช หรือไบต์ที่พิมพ์ไม่ได้จะถูกห่อหุ้มด้วยการหลีกเลขฐานแปด (octal escaping) ตามที่กำหนดใน ISO 32000-2 §7.3.4.2 และ losLab PDF Library จะแปลงกลับเมื่อนำเข้า เพื่อให้วงเล็บและสแลชกลับมาเป็นอักขระดั้งเดิมที่ผู้ใช้กรอกไว้ ประการที่สองคือ การเข้ารหัส (encoding): เมธอดที่ทำงานกับไฟล์จะเขียนและอ่าน XFDF และ FDF เป็น UTF-8 ซึ่งเป็นสิ่งที่ข้อกำหนด XFDF ระบุไว้ และจำเป็นสำหรับค่าฟิลด์ที่ไม่ใช่ ASCII (เช่น ชื่อที่มีเครื่องหมายการออกเสียง, สัญลักษณ์สกุลเงิน หรือที่อยู่ภาษา CJK) เพื่อให้แปลงกลับไปมาได้โดยข้อมูลไม่เสียหาย หากคุณสร้าง XFDF ขึ้นมาเอง โปรดประกาศเป็น UTF-8 และเขียนด้วย UTF-8 แล้วการนำเข้าจะสามารถทำงานร่วมกันได้อย่างถูกต้อง

ฟิลด์แบบลำดับชั้น, ตัวเลือกหลายค่า และ Rich Text

ชื่อฟิลด์แบบลำดับชั้นเป็นจุดแรกที่ระบบส่งออกที่ไม่ได้ออกแบบมารองรับมักจะเกิดการทำงานผิดพลาด AcroForm จะอ้างอิงฟิลด์ที่ซ้อนอยู่ด้วยชื่อเต็มที่มีจุดคั่น เช่น Applicant.FullName แต่ XFDF จะไม่ใส่สตริงที่มีจุดคั่นนั้นลงในแอตทริบิวต์ชื่อตัวเดียว โดย ISO 19444 จะกำหนดโครงสร้างแบบซ้อนเป็น <field name="Applicant"><field name="FullName"> ซึ่ง losLab PDF Library จะแบ่งแยกชื่อเต็มที่มีจุดคั่นออกเป็นองค์ประกอบ <field> ซ้อนกันเมื่อส่งออก และประกอบองค์ประกอบเหล่านั้นกลับมาเป็นชื่อเต็มเมื่อนำเข้า เพื่อให้ทั้งสองทิศทางทำงานอย่างสมมาตร นอกจากนี้ในขั้นตอนการส่งออก ระบบจะข้ามฟิลด์ที่เป็นโหนดพาเรนต์ (parent fields) ที่ไม่ใช่ปลายทาง เนื่องจากโหนดพาเรนต์ใน AcroForm มีบทบาทเพียงแค่การกำหนดระดับชื่อและไม่มีค่าของตัวเอง การส่งออกโหนดดังกล่าวจะทำให้เกิดค่า <value></value> ที่ว่างเปล่าซึ่งไม่ตรงกับฟอร์มจริง ช่องรายการที่เลือกได้หลายค่า (multi-select list boxes) คือจุดควรระวังจุดที่สอง: ฟิลด์ประเภทตัวเลือกสามารถเก็บค่าที่เลือกได้หลายค่าพร้อมกัน ซึ่ง XFDF จะแสดงออกในรูปแบบของแท็ก <value> ที่ซ้ำกัน และ FDF จะแสดงเป็นอาร์เรย์ /V โดย losLab PDF Library จะแยกฟิลด์ออกเป็นหลายค่าเฉพาะเมื่อเป็นตัวเลือกแบบ multi-select จริงๆ เท่านั้น ทำให้ฟิลด์ข้อความธรรมดาหลายบรรทัดยังคงรักษาการขึ้นบรรทัดใหม่เอาไว้ได้ แทนที่จะแตกออกเป็นค่าที่ไม่ถูกต้อง

Rich Text เป็นจุดที่สาม และมักเป็นจุดที่พบความผิดพลาดบ่อยที่สุด ฟิลด์ที่มีการจัดรูปแบบจะบันทึกมาร์กอัปไว้ในรายการ RV ในลักษณะของทรีย่อย XHTML และ XFDF จะเก็บไว้ใน <value-richtext> โดย losLab PDF Library จะเขียนทรีย่อยนั้นเป็น XML fragment จริงแทนที่จะใช้เครื่องหมายหลีก เพื่อให้เครื่องมือในขั้นตอนถัดไปสามารถอ่านเนื้อหา Rich Text จริงได้แทนที่จะเห็นเป็นสตริงของแท็กมาร์กอัป และในการนำเข้า ระบบจะรักษาทรีย่อย RV ดั้งเดิมเอาไว้พร้อมทั้งยังคงนำค่าธรรมเนียมของ <value> ไปใช้กับ V ในกรณีที่ฟิลด์มีทั้งสองรูปแบบ ค่าธรรมดาจะมีผลเหนือกว่าสำหรับ V และ Rich Text จะแนบไปด้วยใน RV ซึ่งเป็นกฎความเข้ากันได้ที่จะช่วยป้องกันไม่ให้การนำเข้า Rich Text ไปเขียนทับค่าที่โปรแกรมอื่นกำหนดไว้แล้วโดยไม่ได้ตั้งใจ หาก Rich Text ของคุณมีขึ้นเพื่อนำเสนอโครงสร้างเอกสารแก่เทคโนโลยีสิ่งอำนวยความสะดวก โปรดจัดการในลักษณะเดียวกับลำดับการอ่านที่อธิบายในเอกสารเรื่อง tagged PDF and accessibility structure

var
  Lib: TPDFlib;
const
  XFDF =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">' +
    '<fields>' +
    '  <field name="Applicant">' +
    '    <field name="FullName"><value>Alice Example</value></field>' +
    '  </field>' +
    '  <field name="Skills">' +
    '    <value>Delphi</value><value>PDF</value>' +
    '  </field>' +
    '  <field name="Notes">' +
    '    <value>See attachment</value>' +
    '    <value-richtext><body><p>See <b>attachment</b></p></body></value-richtext>' +
    '  </field>' +
    '</fields></xfdf>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('intake.pdf', '');
    // Applicant.FullName reassembles; Skills fills the /V + /I arrays;
    // Notes gets V from <value> and RV from <value-richtext>
    Lib.ImportFormDataFromXFDFString(XFDF);
    Lib.SaveToFile('intake-filled.pdf');
  finally
    Lib.Free;
  end;
end;

จะรับส่งข้อมูลความคิดเห็นของคำอธิบายประกอบไปกลับเป็น FDF หรือ XFDF ได้อย่างไร?

ความคิดเห็นจะเคลื่อนย้ายผ่านกลุ่มเมธอดของคำอธิบายประกอบ และชุดย่อยที่รองรับนั้นมีจำกัดด้วยความจงใจ ExportAnnotationsToXFDF และ ImportAnnotationsFromXFDF พร้อมกับคู่เทียบ FDF จะรับส่งมาร์กอัปแบบข้อความพร้อมฟิลด์ที่ส่งกลับไปมาได้สมบูรณ์ ได้แก่ ชนิดย่อยของคำอธิบายประกอบ, สี่เหลี่ยมขอบเขต (rectangle), ดัชนีหน้าเริ่มต้นที่ 0, ชื่อผู้สร้างใน T, หัวข้อใน Subj, เนื้อหาธรรมดาใน Contents และสี ซึ่งจะจับคู่ค่า RGB ของ PDF /C สลับกับแอตทริบิวต์ #RRGGBB ของ XFDF ฟังก์ชันนำเข้าจะยอมรับเฉพาะชื่อองค์ประกอบที่รู้จักและข้ามรายการใดๆ ที่ระบุหน้าไม่ถูกต้องหรือไม่มีพิกัดสี่เหลี่ยมขอบเขต เพื่อไม่ให้ไฟล์ XFDF ที่แก้ไขด้วยตนเองหรือแปลกปลอมสามารถนำเข้าคำอธิบายประกอบที่ผิดรูปแบบลงในเอกสารได้ สิ่งที่ชุดย่อยนี้ยังไม่รองรับมีรายละเอียดดังนี้คือ: เนื้อหา Rich Text (RC), ป๊อปอัป, quadpoints, ink lists, vertices และ appearance streams ที่แปลงแล้ว ซึ่งอยู่นอกขอบเขตการทำงานในขณะนี้ ดังนั้นพิกัดส่วนไฮไลต์และลักษณะของตราประทับที่กำหนดเองจะไม่สามารถส่งผ่านด้วยวิธีนี้ได้ หากต้องการตรวจสอบสิ่งที่ได้รับการป้อนเข้ามาจริง ให้ตรวจสอบคำอธิบายประกอบของหน้าหรือโครงสร้างองค์กรที่กว้างขึ้นที่กล่าวไว้ใน text search and page element enumeration

var
  Src, Dest: TPDFlib;
  Xfdf: WideString;
begin
  Src := TPDFlib.Create;
  Dest := TPDFlib.Create;
  try
    Src.LoadFromFile('reviewed.pdf', '');
    Xfdf := Src.ExportAnnotationsToXFDFString;   // <annots> subset only

    Dest.LoadFromFile('clean-copy.pdf', '');     // pages must line up by index
    Dest.ImportAnnotationsFromXFDFString(Xfdf);
    Dest.SaveToFile('clean-copy-commented.pdf');
  finally
    Dest.Free;
    Src.Free;
  end;
end;

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

XFA เป็นกรณีพิเศษ และ SetXFAFromString คือวิธีที่คุณใช้เขียนข้อมูลทั้งหมดในครั้งเดียว เมธอดนี้จะรับ XML Data Package ที่สมบูรณ์ซึ่งเป็นเอกสาร <xdp:xdp> พร้อมกับแพ็กเก็ตเทมเพลตและชุดข้อมูล แล้วนำไปจัดเก็บในลักษณะของสตรีมที่อ้างอิงจาก /AcroForm/XFA โดย losLab PDF Library จะสร้างคอนเทนเนอร์ AcroForm ให้ทันทีหากเอกสารไม่มีโครงสร้างดังกล่าวอยู่ เพื่อที่คุณจะได้ไม่ต้องสร้างฟิลด์ชั่วคราวขึ้นมาเพื่อให้ XFA เกาะ และมันจะล้างสถานะ XFA ที่ได้รับการวิเคราะห์ก่อนหน้านี้ออกไปทั้งหมด เพื่อให้การอ่านในภายหลังได้ข้อมูลจากแพ็กเก็ตที่เพิ่งเขียนเข้าไปใหม่แทนที่จะเป็นแคชที่ล้าสมัย เนื่องจากแพ็กเก็ต XFA เป็น XML และไม่ได้จำกัดการเข้ารหัสเป็น UTF-8 เสมอไป ไลบรารีจะตรวจหาเครื่องหมายระบุลำดับไบต์ (byte-order mark) ของ UTF-16 และถอดรหัสแพ็กเก็ตอย่างถูกต้องก่อนจะทำการวิเคราะห์ ซึ่งมีความสำคัญมากสำหรับแพ็กเก็ตที่สร้างจากเครื่องมือที่ตั้งค่าเริ่มต้นเป็น UTF-16

เมื่อใส่แพ็กเก็ตเรียบร้อยแล้ว GetXFAFormFieldValue และ SetXFAFormFieldValue จะเข้าถึงแต่ละฟิลด์ผ่านพาธของ Scripting Object Model (SOM) โดย losLab PDF Library รองรับราก SOM มาตรฐานและพาธสัมพัทธ์ธรรมดา ดังนั้น form1.FullName, $data.form1.FullName และ xfa.datasets.data.form1.FullName จะชี้ไปยังโหนดข้อมูลเดียวกัน และในฝั่งเทมเพลตจะยอมรับ $template และ xfa.template ในลักษณะเดียวกัน ความยืดหยุ่นนี้มีความสำคัญมากเมื่อพาธ SOM ถูกสร้างขึ้นโดยระบบอื่นที่มักจะส่งออกชื่อเต็มเสมอ

var
  Lib: TPDFlib;
const
  Xdp =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">' +
    '  <template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">' +
    '    <subform name="form1">' +
    '      <field name="FullName"><ui><textEdit/></ui></field>' +
    '    </subform>' +
    '  </template>' +
    '  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">' +
    '    <xfa:data><form1><FullName>Alice Example</FullName></form1></xfa:data>' +
    '  </xfa:datasets>' +
    '</xdp:xdp>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetXFAFromString(Xdp, 0);      // creates the AcroForm container if none exists
    // read back through the data DOM with an SOM path
    if Lib.GetXFAFormFieldValue('form1.FullName') = 'Alice Example' then
      Lib.SetXFAFormFieldValue('form1.FullName', 'Bob Example');
    Lib.SaveToFile('xfa-packet.pdf');
  finally
    Lib.Free;
  end;
end;

โดยสรุปคือ ค่าฟิลด์สามารถรับส่งไปกลับได้อย่างสมบูรณ์ผ่าน FDF และ XFDF รวมถึงฟิลด์แบบลำดับชั้น, แบบเลือกได้หลายค่า และ Rich Text ในขณะที่ความคิดเห็นของคำอธิบายประกอบจะรับส่งได้เฉพาะบางส่วนตามข้อกำหนดของมาร์กอัปข้อความเท่านั้น และ XFA จะเขียนและอ่านในลักษณะของแพ็กเก็ตทั้งหมดโดยสามารถเข้าถึงรายฟิลด์ผ่าน SOM ได้ เลือกรูปแบบข้อมูลให้สอดคล้องกับระบบผู้ใช้งาน กำหนดค่าเข้ารหัสเป็น UTF-8 สำหรับข้อมูลที่คุณสร้างขึ้นเอง และพึงระลึกว่าเมธอดนำเข้าจะทำงานเฉพาะกับฟิลด์และคำอธิบายประกอบที่มีอยู่แล้วในเอกสารเท่านั้น เมธอดจัดการข้อมูลฟอร์ม, คำอธิบายประกอบ และ XFA ที่อธิบายไว้ในที่นี้เป็นส่วนหนึ่งของ losLab PDF Library สำหรับ Delphi และ C++Builder ซึ่งเอกสารอ้างอิงจะมีรายละเอียดพารามิเตอร์ทั้งหมดสำหรับจุดส่งออกและนำเข้าแต่ละตัว