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

เซฟ XFA ใน PDFium Component: newline, emoji กับ restoreState

PDFium Component เซฟค่าฟอร์ม XFA ที่ถูกแก้ไขไว้ได้เป๊ะ ตลอดการเซฟกับเปิดซ้ำ เมื่อมันรัน runtime Windows V8 คือ pdfium.v8.dll ที่แถมมาใน v3.125.2 ขึ้นไป runtime รุ่นเก่าเคยเติม line feed เข้าค่าของ field, ตัด emoji ให้เหลือเป็นอักขระ BMP ที่ไม่มีส่วนเกี่ยวข้อง, เมินการเซฟ XFA แบบ single stream ไปอย่างเงียบ ๆ และอาจกลืนการเขียนรอบสุดท้ายที่ล้มเหลวทิ้งไปได้ อาการหนึ่งตอนเปิดซ้ำไม่ใช่ของเสียจากไลบรารีเลยแม้แต่น้อย: ฟอร์ม dynamic ที่ root subform ขาด restoreState="auto" จะสร้าง layout ของมันใหม่จากเทมเพลต

รายงานบั๊กของเรื่องนี้หน้าตาเหมือนกันหมด ลูกค้ากรอกฟอร์มเคลมแบบ XFA ใน viewer Delphi เซฟ เปิดใหม่ แล้วบางอย่างเพี้ยนไปนิดหน่อย กล่อง comment ว่าง ๆ ตอนนี้มีบรรทัดว่างหนึ่งบรรทัด เซฟรอบสองกลายเป็นสองบรรทัด ชื่อที่พิมพ์พร้อม emoji กลับมาพร้อม glyph ในพื้นที่ private use ไม่มีใครได้ error เลย ซึ่งนั่นแหละคือสิ่งที่ทำให้บั๊กกลุ่มนี้แพง: ความเพี้ยนปรากฏอีกทีหลังผ่านไปหลายสัปดาห์ ในไฟล์ export ของใครสักคนอื่น

เซฟฟอร์ม XFA แล้วเปิดซ้ำ อะไรพังได้บ้าง

ข้อบกพร่องสี่ตัวแยกกันในเส้นทางเซฟ XFA ระดับ native เป็นต้นเหตุของความเพี้ยนของค่า และแต่ละตัวซ่อนตัวหลังการเซฟที่ดูสำเร็จสองตัวมาจาก serialization หนึ่งตัวมาจาก layout การเก็บแบบ single-stream และอีกหนึ่งตัวมาจากตัวเขียน PDF เอง ตารางแมปอาการแต่ละอย่างเข้ากับสาเหตุ กับรีลีสที่ PDFium Component แก้มัน

อาการหลังเปิดซ้ำสาเหตุแก้แล้วใน
field ว่างถือ line feed หนึ่งตัว ค่าเติม newline เพิ่มทีละหนึ่งต่อการเซฟตัวเขียน XFA ทั้งสองตัวแทรก newline เพื่อจัดรูปหลัง start tagv3.125.2, pdfium.v8.dll
U+1F642 กลับมาเป็น U+F642 หรือ emoji หายจากแพ็กเกตของฟอร์มการตัดของ wchar_t 16 บิตในการถอดรหัส กับการกรอง surrogate ใน serializer ของฟอร์มv3.125.2, pdfium.v8.dll
การแก้ไขในเอกสาร XFA แบบ single stream หายวับไปเลยการเซฟระดับ native ปฏิเสธ layout แบบ stream แต่ค่าที่คืนถูกเมินv3.125.2 คอมเมนต์กับ processing instruction ถูกเก็บไว้ตั้งแต่ v3.126.0
ไฟล์ถูกตัดสั้นทั้งที่การเซฟรายงานว่าสำเร็จการเขียนแบบบัฟเฟอร์รอบสุดท้ายล้มเหลวหลัง writer รายงานความสำเร็จไปแล้วv3.125.2 V8 runtime; v3.125.3 pdfium.dll ธรรมดา
ฟอร์ม dynamic สามหน้าเปิดซ้ำมาเหลือสองหน้าroot subform ไม่ได้ขอ restoreState="auto"เรื่องการเขียนฟอร์ม ไม่ใช่ของเสียจากไลบรารี

งานเขียนก่อนหน้านี้เคยสรุปว่าการแก้ field ของ XFA ไม่สามารถถูกเก็บถาวรด้วย PDFium ได้เลย ซึ่งถูกต้องสำหรับ runtime รุ่นนั้น ๆ runtime V8 ตัวใหม่เซฟค่าของ XFA ได้ระดับ native การแก้ที่ทำในฟอร์มที่มีชีวิตจึงไปถึงแพ็กเกต datasets ที่ถูกเซฟโดยไม่ต้องผ่าตัดแพ็กเกตเอง

runtime ของ PDFium ตัวไหนเซฟค่า XFA ได้

ความเป๊ะของการเซฟ XFA ขึ้นกับ DLL ระดับ native ไม่ใช่ wrapper ฝั่ง Delphi เช็กแรกเลยคือ process ของคุณโหลด runtime ตัวไหนอยู่จริง PDFium Component แถมบิลด์ Windows สองตัวต่อสถาปัตยกรรม คือ pdfium.dll ธรรมดาที่บิลด์โดยไม่มี V8 กับ XFA และ pdfium.v8.dll ที่แบกทั้ง JavaScript engine กับ runtime ของฟอร์ม XFA มีแต่ pdfium.v8.dll เท่านั้นที่รันฟอร์ม XFA ได้ การแก้เรื่อง XFA ทุกเรื่องที่เล่าไว้ในนี้จึงอยู่ที่ตัวนั้น เริ่มจากไลบรารี V8 รุ่น Win32 กับ Win64 ที่บิลด์ใหม่ใน v3.125.2

การแก้เรื่องการเขียนรอบสุดท้ายเป็นโค้ดทั่วไปของตัวเขียน PDF จึงสำคัญกับเอกสารธรรมดาด้วย v3.125.3 บิลด์ไลบรารี pdfium.dll ธรรมดาใหม่เพื่อแบกการซ่อมเดียวกันไปด้วย ซอร์สเดียวกันไม่ใช่หลักฐานว่าพฤติกรรมเดียวกัน ไบนารียังไม่ถูกบิลด์ใหม่ DLL ตัวเก่าก็ยังถือบั๊กเก่าอยู่

กับดักที่สองนั่งอยู่ในตัวโหลด ก่อน v3.125.2 การเซ็ต EnableV8Engine เป็น True ทำให้ตัว binding หยิบชื่อ pdfium.v8.dll ค่าเริ่มต้นแล้วเมิน path เต็ม ๆ ที่ระบุใน LibraryName แอปที่ชี้ไปยัง runtime ที่ deploy ใหม่จึงยังคงโหลดสำเนาเก่าจากโฟลเดอร์อื่นได้ ตั้งแต่ v3.125.2 LibraryName ที่มีไดเรกทอรีอยู่ในนั้นจะเลือกไฟล์นั้นเป๊ะในทั้งสองโหมดเอนจิน และ path ที่หายไปจะล้มเหลวแทนที่จะหลุดไปใช้ไลบรารีที่แถมมาอีกตัว

uses
  System.SysUtils, PDFium;

procedure SelectXfaRuntime;
begin
  // การมีไดเรกทอรีใน LibraryName ตรึงไฟล์นี้เป๊ะ (v3.125.2 ขึ้นไป);
  // ถ้าไฟล์หาย การโหลดจะ raise แทนที่จะหลุดไปใช้ตัวอื่น
{$IFDEF WIN64}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
  PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
  PDFium.EnableV8Engine := True;
  PDFium.LoadLibrary;  // ให้พังตอนสตาร์ท ไม่ใช่ตอนเซฟครั้งแรก
end;

หลังเปิดเอกสาร TPdf.XFA บอกว่าไฟล์มี XFA และ TPdf.XfaRuntimeAvailable บอกว่า DLL ที่โหลดไว้รันมันได้จริง ถ้าคุณยังต้องแยกฟอร์ม static กับ dynamic TPdf.FormType คืน ftXfaFull หรือ ftXfaForeground บทความเรื่องการตรวจจับฟอร์ม XFA กับการสกัดแพ็กเกต XFA ใน Delphi เล่าการแอะพวกนี้ละเอียด

ทำไม field XFA ที่เซฟไว้ถึงได้ line feed เกินมา

field XFA ที่เซฟไว้ได้ line feed เพิ่ม เพราะตัวเขียน XFA ระดับ native ทั้งสองตัว ทั้งตัวเขียน element XML ทั่วไปและ serializer ของแพ็กเกตฟอร์ม พิมพ์ output ให้สวยงามด้วย newline หลัง start tag ใน XML ทั่วไปช่องว่างพวกนี้เป็นเรื่องประดับ แต่ในข้อมูล XFA มันไม่ใช่ เมื่อแพ็กเกต datasets ถูก parse ใหม่ ข้อความระหว่าง <Comments> กับ </Comments> คือค่าของ field newline รวมอยู่ด้วย field ว่างจึงเปิดซ้ำมาพร้อม LF หนึ่งตัวในตัว และทุกรอบเซฟกับเปิดซ้ำถัดไปเติมมันได้อีก

แผนภาพรอบการเซฟ XFA ของ PDFium Component ที่ writer เติม newline หลัง start tag ตัว parse ตอนเปิดซ้ำอ่าน LF ระหว่างแท็ก Comments ไปเป็นค่าของ field และทุกรอบเซฟถัดไปต่อท้าย line feed อีกหนึ่งตัว จนกว่า v3.125.2 จะตัดเฉพาะช่องว่างที่ serializer สังเคราะห์ขึ้นเอง
รอบเซฟกับเปิดซ้ำหนึ่งรอบปลูก line feed แรก และทุกรอบถัดไปเติมอีกหนึ่งตัว ความเพี้ยนจึงโชว์รูปทรงเต็มของมันเฉพาะในรุ่นลูกที่สอง

การซ่อมแบบที่เห็นได้ทันที คือตัดขอบค่าตอนโหลด แต่มันผิด ผู้ใช้พิมพ์ช่องว่างนำหน้า ช่องว่างท้าย กับข้อความหลายบรรทัดตั้งใจไว้ใน field ของ XFA บล็อกที่อยู่หรือรหัสความกว้างคงที่ต้องรอดมาเป็นไบต์เดิมทุกตัว การแก้ใน v3.125.2 จึงตัดเฉพาะช่องว่างที่ serializer เองสังเคราะห์ขึ้นรอบแท็ก ค่าของผู้ใช้ text node ที่มีอยู่เดิม กับส่วน CDATA ผ่านไปไม่ถูกแตะ " indented" จึงยังมีช่องว่างนำหน้าและ field ที่ตั้งใจให้ว่างก็ยังว่าง

ทำไม emoji กลับมาเป็นอักขระคนละตัว

emoji กลับมาผิดเพราะ wchar_t บน Windows กว้าง 16 บิต และสองเส้นทางการถอดรหัสเก็บ Unicode scalar value เต็มตัวลงใน wchar_t ตัวเดียว ทั้งตัวถอดรหัส stream UTF-8 และ parser ของ numeric character reference อย่าง &#x1F642; ทำแบบนี้ U+1F642 ใบหน้ายิ้มน้อย ๆ ไม่ลง 16 บิต บิตสูงจึงหล่นหายแล้ว U+F642 โผล่มาแทน ซึ่งเป็น code point ใน Private Use Area ที่ฟอนต์ส่วนใหญ่เรนเดอร์เป็นกล่องหรือไม่แสดงอะไรเลย

serializer ของฟอร์มมีปัญหาฝั่งตรงข้าม มันกรองอักขระทีละ wchar_t เห็น code unit แบบ surrogate สองตัวที่เดี่ยว ๆ ไม่ถูกต้อง แล้วทิ้งทั้งคู่ emoji จึงหายจากแพ็กเกตฟอร์มไปทั้งตัว ใน v3.125.2 ตัวถอดรหัสกิน scalar value แต่ละตัวให้จบและปล่อย surrogate pair ที่ถูกต้องออกมา เมื่อช่อง output เหลือเพียงช่องเดียว มันจะถือ low surrogate ค้างไว้และไม่รายงานจบ stream ขณะที่ unit นั้นยังอยู่ในบัฟเฟอร์ ลำดับ UTF-8 ที่ถูกตัดคร่อมบล็อกอ่านจะถูกแบกต่อไปยังรอบอ่านถัดไปแทนที่จะถูกทิ้ง ตัว export ฟอร์มตอนนี้เก็บ surrogate pair ที่ถูกต้องไว้ด้วยกัน และ numeric character reference ก็ผลิต pair ที่ถูกต้องได้เช่นกัน

แผนภาพการจัดการ surrogate ของ PDFium Component ที่ U+1F642 มาถึงเป็นคู่ UTF-16 คือ D83D DE42 แล้วสองเส้นทางที่มีข้อผิดพลาดทำลายมัน ตัวถอดรหัส wchar_t 16 บิตตัด scalar เหลือ U+F642 ในพื้นที่ private use ขณะที่ serializer ของฟอร์มกรอง surrogate เดี่ยวแล้วทิ้ง emoji ไปทั้งตัว
wchar_t บน Windows กว้าง 16 บิต scalar ที่ต้องการ surrogate pair จึงเสียครึ่งบิตสูงไปหรือหายจากแพ็กเกตไปเลย จนกว่าทั้งสองเส้นทางจะเรียนรู้ที่จะเก็บคู่ไว้ด้วยกัน

ข้อมูลทดสอบ Latin-1 ไม่มีทางโชว์สิ่งเหล่านี้เลย เทสต์ round-trip ของ XFA ทุกชุดจึงต้องมีอักขระใน supplementary plane อย่างน้อยหนึ่งตัว

XFA แบบ single stream กับความล้มเหลวของการเซฟที่ไม่มีใครเห็น

เอกสาร XFA แบบ single stream สูญเสียการแก้ไข เพราะตัวช่วยเซฟระดับ native ปฏิเสธ layout การเก็บแบบนั้น และผู้เรียกก็เมินความล้มเหลวนั้นไป ISO 32000-1 §12.7.8 อนุญาตให้ entry /XFA ของ interactive form dictionary เป็นได้ทั้ง array ของชื่อแพ็กเกตกับ stream หรือเป็น stream เดียวที่ถือเอกสาร XDP ทั้งฉบับ array ของแพ็กเกตเป็นกรณีที่พบบ่อย แต่ stream เดี่ยวก็ถูกต้องตามกฎหมายเต็มร้อย และการเซฟ PDF ก็จบไปราวกับไม่มีอะไรเกิดขึ้น ทั้งที่ข้อมูลฟอร์มยังนั่งอยู่ที่ค่าเดิม

ตั้งแต่ v3.125.2 runtime V8 รองรับส่วนย่อยแบบ single-stream ที่รองรับได้ มัน export ทั้งสองแพ็กเกตที่มีชีวิต คือ datasets กับ form ออกไปยังพื้นที่ staging ก่อน ตรวจความถูกต้อง แล้วจึงแทนที่แพ็กเกตที่ตรงกันใน XDP ต้นฉบับ แพ็กเกตอื่นกับการประกาศ namespace ที่ root ถูกเก็บไว้ ถ้า staging ล้ม แต่ stream XFA ที่เก็บถาวรจะไม่ถูกแตะเลย และเอกสารก็คงเครื่องหมายการแก้ไขไว้

คอมเมนต์ XML กับ processing instruction ต้องการความเอาใจใส่เพิ่ม เพราะ XML DOM ภายในทิ้งพวกมันไป ใน v3.125.2 การมีอยู่ของพวกมันทำให้การเซฟล้มตรง ๆ แทนที่จะสูญเสียเนื้อหาอย่างเงียบ ๆ v3.126.0 เก็บพวกมันไว้ได้ ก่อน parse คอมเมนต์หรือ processing instruction แต่ละอันจะถูกสลับออกเป็น marker ที่สร้างจาก prefix ซึ่งไม่เกิดขึ้นที่ไหนในข้อความต้นฉบับ หลังแพ็กเกตที่มีชีวิตถูกแทนที่ marker ทุกตัวต้องปรากฏพอดีหนึ่งครั้ง ก่อนที่ token ต้นฉบับจะถูกคืนและ stream ถูกเขียนลง token นอกแพ็กเกตที่ถูกแทนที่จึงคงข้อความกับลำดับเดิม รวม token ใน prolog เทมเพลต กับแพ็กเกตอื่น

input บางแบบยังถูกปฏิเสธตั้งใจไว้ และการปฏิเสธแต่ละครั้งคือความล้มเหลวของการเซฟอย่างชัดเจน:

  • คอมเมนต์หรือ processing instruction ที่อยู่ข้างในแพ็กเกต datasets หรือ form ที่มีชีวิต เพราะตำแหน่งเดิมของพวกมันแมปเข้าเนื้อหาที่ export ใหม่ไม่ได้
  • การประกาศ DTD กับลายเซ็น XMLDSig เพราะการเขียน XDP ใหม่ไม่มีทางรักษา XML signature ให้ยังใช้ได้
  • การเข้ารหัส UTF-8 หรือ UTF-16 ที่ไม่ถูกต้อง แท็กที่ไม่สมบูรณ์, character reference ที่ผิด, entity ที่ไม่รู้จัก กับ processing instruction ที่เขียนพัง ซึ่งถูกปฏิเสธแทนที่จะถูกซ่อมแบบเงียบ ๆ
แผนภาพ pipeline การเซฟ XFA แบบ single stream ของ PDFium Component ที่แพ็กเกต datasets กับ form ที่มีชีวิตถูก export ไป staging ตรวจสอบแล้วจึงแทนที่ข้างใน XDP ต้นฉบับโดยเก็บคอมเมนต์ผ่าน marker ขณะที่ความล้มเหลวของ staging และ input อย่าง DTD หรือ XMLDSig ปฏิเสธการเซฟอย่างชัดเจน
export ที่วางบน staging ถูกตรวจก่อนที่อะไรจะถูกแทนที่ การเซฟที่ล้มจึงทิ้ง stream XFA ถาวรไว้ไม่ถูกแตะ และเอกสารก็คงเครื่องหมายการแก้ไขไว้

ผลลัพธ์แบบ single-stream เป็น UTF-8 และคงโมเดลเนื้อหา XML ไว้ ไม่ใช่ layout ไบต์เดิมหรือการประกาศการเข้ารหัสเดิม

ข้อบกพร่องสุดท้ายนั่งอยู่ใต้ XFA ลงไป ตัวเขียนไฟล์ระดับ native บัฟเฟอร์ output เป็นบล็อก 32 KB แล้ว flush บล็อกสุดท้ายที่ไม่เต็มใน destructor เท่านั้น หลังตัวเขียนเอกสารรายงานความสำเร็จไปแล้ว disk full หรือ I/O error บนบล็อกสุดท้ายจึงมองไม่เห็นจากผู้เรียก ตั้งแต่ v3.125.2 ใน runtime V8 และ v3.125.3 ใน runtime ธรรมดา flush รอบสุดท้ายนั้นอยู่ในผลการเซฟแล้ว และเครื่องหมายการแก้ไขของ XFA ถูกเคลียร์หลังความสำเร็จแบบจริงเท่านั้น ฝั่ง Delphi TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean เขียนลงไฟล์ชั่วคราวข้าง ๆ ไฟล์เป้าหมาย แล้วย้ายมันเข้าที่เมื่อการเซฟคืน True เท่านั้น การเซฟที่ล้มจึงทิ้งไฟล์เดิมไว้สมบูรณ์

ทำไมฟอร์ม dynamic XFA ถึงเปิดซ้ำมาหน้าน้อยกว่าเดิม

ฟอร์ม dynamic XFA เปิดซ้ำมาหน้าน้อยลงเมื่อ root subform ของมันไม่ประกาศ restoreState="auto" ซึ่งเป็นการตัดสินใจตอนเขียนฟอร์ม ไม่ใช่ของเสียจาก PDFium Component ใน XFA 3.3 restoreState บน root subform มีค่าเริ่มต้นเป็น manual ภายใต้ manual ตัวประมวลผล XFA คืน state ที่จำกัดจากแพ็กเกตฟอร์มที่เซฟไว้เท่านั้น ส่วนที่เหลือยกให้สคริปต์ของผู้เขียน ค่าของ field ที่เซฟไว้กับจำนวน instance ของ subform แบบทำซ้ำยังกลับมา แต่ property เชิงเรขาคณิตที่เซ็ตตอน run time ไม่กลับมา

กรณีที่เปิดเผยเรื่องนี้คือฟอร์มสามหน้าที่สคริปต์ของมันเติม subform ให้เป็น h="450pt" แพ็กเกตฟอร์มที่เซฟไว้ถือความสูงใหม่ ค่าต่าง ๆ และจำนวน instance ครบ แต่ตอนเปิดซ้ำ layout ถูกสร้างใหม่จากความสูงในเทมเพลต ฟอร์มจึงไหลตัวมาลงสองหน้า runtime พูดถูกต้องแล้ว เทมเพลตไม่เคยขอการคืน state อัตโนมัติเลย การประกาศมันไว้บน root subform แก้เรื่องการเปิดซ้ำได้:

<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
  <subform name="form1" layout="tb" restoreState="auto">
    <pageSet>
      <pageArea name="Page1">
        <contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
        <medium stock="letter"/>
      </pageArea>
    </pageSet>
    <subform name="Details" layout="tb" w="7.5in">
      <!-- field ต่าง ๆ สคริปต์อาจเปลี่ยน h หรือเพิ่ม instance ตอน run time -->
    </subform>
  </subform>
</template>

ถ้าเทมเพลตไม่ใช่ของคุณ อย่าไปแก้ให้มันใน viewer: ฟอร์มที่พึ่งพาโหมด manual คาดหวังว่าสคริปต์ของตัวเองจะสร้าง state กลับมา การจัดหน้าใหม่ระหว่างที่ผู้ใช้พิมพ์เป็นอีกเรื่องหนึ่ง เล่าไว้ในว่า PDFium Component ติดตามจำนวนหน้าของ dynamic XFA กับ field ที่ย้ายที่อย่างไร

จะพิสูจน์การเซฟ XFA ใน Delphi อย่างไร

การเช็กการเซฟ XFA เพียงวิธีที่เชื่อถือได้คือเปิดไฟล์ที่เซฟไว้ใน TPdf instance ใหม่ แล้วอ่านข้อมูลที่เก็บไว้กลับมา TPdf.GetXfaDatasets คืนแพ็กเกต datasets ตามที่มันถูกเก็บในเอกสาร ไม่ใช่โมเดลข้อมูล XFA ที่มีชีวิต การเรียกมันก่อนเซฟจึงเห็นค่าเก่า หลังเปิดซ้ำมันโชว์สิ่งที่ถูกเขียนไปเป๊ะ ๆ เอกสารแบบ single-stream ไม่มีแพ็กเกตที่ตั้งชื่อแยก PDFium รายงาน XDP ทั้งฉบับเป็นแพ็กเกตหนึ่งตัวที่ชื่อว่างเปล่า GetXfaPacketByName('datasets') กับ GetXfaDatasets จึงคืนอะไรไม่ได้ และเส้นทางสำรองคืออ่าน stream ทั้งก้อนผ่าน GetXfaFormPackets

uses
  System.SysUtils, PDFium, FPdfXfa;

function ReadSavedXfaData(const FileName: string): string;
var
  Pdf: TPdf;
  Packets: TXfaPacketList;
  Bytes: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Bytes := Pdf.GetXfaDatasets;          // layout แบบ array ของแพ็กเกต
    if Length(Bytes) = 0 then
    begin
      Packets := Pdf.GetXfaFormPackets;   // single stream: แพ็กเกตไร้ชื่อหนึ่งตัว
      if Length(Packets) = 1 then
      begin
        SetLength(Bytes, Length(Packets[0].Content));
        if Length(Bytes) > 0 then
          Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
      end;
    end;
    Result := TEncoding.UTF8.GetString(Bytes);  // output XDP ที่เซฟแล้วเป็น UTF-8
  finally
    Pdf.Free;
  end;
end;

routine การเซฟจากนั้นจะ commit การแก้ที่ค้าง เช็กผล SaveAs แล้วเทียบค่าที่เปิดกลับมา TPdf.ClearFormFieldFocus ตัด focus ของฟอร์มทิ้ง ซึ่งเป็นจังหวะที่ PDFium commit บัฟเฟอร์การแก้ของ field ที่ถือ focus TPdf.SetFocusedFormFieldText(const Value: WString): Boolean เติม field ที่ถือ focus ด้วยโค้ดได้ แต่มันพึ่งพา focus ที่ wrapper ติดตามผ่าน FocusFormField ซึ่งเดินสำรวจ widget annotation หน้า dynamic XFA ปกติไม่มีของแบบนี้เลย ข้อความที่นั่นจึงมักเดินทางมาผ่าน input จากคีย์บอร์ดใน TPdfView และฟังก์ชันคืน False เมื่อไม่มี field ที่ถูกติดตามถือ focus อยู่

function XmlText(const S: string): string;
begin
  Result := StringReplace(S, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
end;

procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
  Expected: string);
var
  Saved: string;
begin
  // เติมผ่านสคริปต์เป็นทางเลือก False แปลว่าไม่มี field ที่ถูกติดตามถือ focus
  if (Pdf.FocusedFormFieldIndex >= 0) and
     not Pdf.SetFocusedFormFieldText(Expected) then
    raise EPdfError.Create('Could not write the focused field');

  Pdf.ClearFormFieldFocus;              // commit edit buffer ที่ค้าง
  if not Pdf.SaveAs(FileName) then      // รวมการ flush รอบสุดท้าย (v3.125.2 ขึ้นไป)
    raise EPdfError.CreateFmt('Saving %s failed', [FileName]);

  Saved := ReadSavedXfaData(FileName);
  if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
    Saved) = 0 then
    raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;

ถือการทดสอบ substring เป็น smoke test เท่านั้น element ว่างอาจถูก serialize เป็น <Tag/> attribute อาจโผล่บน data element และการ escape ที่เกิน & กับ < เป็นเรื่องตัดสินใจของ serializer การเช็กระดับ production ให้โหลด XML ที่เปิดซ้ำด้วย XML parser จริงแล้วเทียบ text node ของ data element ที่ผูกไว้ รันการเช็กนี้ติดกันสองรอบด้วย เพราะข้อบกพร่องเรื่อง newline โชว์รูปทรงเต็มของมันเฉพาะรุ่นที่สอง

สรุป: เช็กลิสต์ความเป๊ะของการเซฟ XFA

  • deploy pdfium.v8.dll จาก v3.125.2 ขึ้นไปสำหรับฟอร์ม XFA และ v3.125.3 ขึ้นไปสำหรับ pdfium.dll ธรรมดา เพื่อให้การแก้ flush รอบสุดท้ายอยู่ในทั้งสองฝั่ง
  • ชี้ LibraryName ไปที่ path เต็มแล้วเซ็ต EnableV8Engine เป็น True path ที่หายจะล้มแทนที่จะไปโหลดสำเนาอื่น
  • ยืนยัน TPdf.XFA กับ TPdf.XfaRuntimeAvailable หลังเปิดเอกสาร
  • call ClearFormFieldFocus ก่อน SaveAs เพื่อให้ field ที่ถือ focus ถูก commit
  • ห้ามเมินผล Boolean ของ SaveAs ผล False ทิ้งไฟล์เดิมไว้ตรงตำแหน่ง
  • ตรวจโดยเปิดใหม่ใน TPdf ตัวใหม่แล้วอ่าน GetXfaDatasets ถ้าไม่เจอใช้ GetXfaFormPackets สำหรับ XFA แบบ single-stream
  • เทสต์ด้วยค่าว่าง ช่องว่างนำหน้า ข้อความหลายบรรทัด & และอักขระใน supplementary plane ครบสองรุ่นการเซฟ
  • คาดหวังความล้มเหลวของการเซฟอย่างชัดเจนกับ DTD, XMLDSig และคอมเมนต์ข้างในแพ็กเกตที่มีชีวิตของ XFA แบบ single-stream
  • ถ้าฟอร์ม dynamic สูญเสียเรขาคณิตตอน run time เวลาเปิดซ้ำ เช็ก root subform หา restoreState="auto" ก่อนจะสงสัยตัวไลบรารี

สำหรับโครงสร้าง callback ที่ runtime XFA คาดหวังจากแอป host ดูที่FPDF_FORMFILLINFO เวอร์ชัน 2 กับ ABI ของ XFA ใน Delphi runtime V8, wrapper ของ Delphi กับ C++Builder และ control viewer ล้วนอยู่ในPDFium Component for Delphi and C++Builder ซึ่งรวม runtime Windows ทั้งคู่สำหรับ Win32 กับ Win64