HotPDF Delphi Component ถือว่า /FT, /Ff, /V กับ /DV บนฟิลด์ AcroForm ที่โหลดมาเป็นแอตทริบิวต์ที่สืบทอดได้ โดย resolve ด้วยการเดินตามห่วงโซ่ /Parent ตั้งแต่ v2.754.3 กับ v2.754.4 ฟิลด์ลูกที่มีชื่อซึ่งชนิดมาจาก parent ยังเรียกถึงทีละตัวได้ RemoveFormField ไม่แตะพี่น้องของมัน และ ResetLoadedFormField คัดลอกค่า default ที่สืบทอดมาพร้อมชนิด PDF object เดิม ก่อนหน้านี้ฟอร์มธรรมดา ๆ จำนวนไม่น้อยถูกอ่านผิดมาแล้ว
ฟอร์มที่เปิดเผยทุกอย่างนี้ไม่ใช่ของแปลก เครื่องมือเขียนฟอร์มสร้าง node กลุ่มชื่อ group ที่แบก /FT /Ch, ธงฟิลด์และรายการ option ครั้งเดียว แล้วแขวนฟิลด์ลูกที่มีชื่อสองตัวคือ a กับ b ไว้ใต้มัน แต่ละตัวเป็น dictionary ที่หลอมฟิลด์กับ widget เดียวกัน มีอะไรบ้างนอกจาก /T, /Parent, /Rect และ /V ของตัวเอง นี่คือวิธีแชร์แอตทริบิวต์ที่ถูกกฎหมายสนิท และเป็นเคสพอดีที่ส่วน Limits ของการตั้งค่าฟิลด์ฟอร์มใน PDF ที่โหลดมาด้วย Delphi ติดป้ายว่ายังไม่รองรับ: การ reconcile ปุ่มมองแค่ /FT ท้องถิ่น บทความนี้รับต่อจากจุดที่บทความนั้นหยุด ครอบคลุมว่า tree ฟิลด์ถูกจำแนกอย่างไร ค่าที่สืบทอดถูกอ่านอย่างไร และ reset หนึ่งฟิลด์ได้รับอนุญาตให้เขียนอะไร
entry ไหนของ AcroForm ที่ฟิลด์สืบทอดจาก parent ได้
ISO 32000-1 §12.7.3.1 Table 220 กำกับว่า /FT, /Ff, /V กับ /DV สืบทอดได้ และ Table 229 ใน §12.7.4.3 ก็ทำแบบเดียวกับ /MaxLen ของฟิลด์ text ใครที่อ่านแค่ dictionary ท้องถิ่นจึงรายงานชนิดผิด ธงผิด และค่าว่างให้ฟิลด์ลูกที่ถูกต้องสมบูรณ์ HotPDF รวบการอ่านพวกนี้ทั้งหมดผ่าน resolver ภายในตัวเดียวคือ HPDFLoadedInheritedFieldObject ที่เช็กคีย์ใน dictionary, resolve reference แบบ indirect ถ้าเจอ และไม่งั้นก็ตาม /Parent ไม่เกิน 128 ระดับ เพราะไฟล์พัก ๆ สร้างวงจร /Parent ที่ไม่เกี่ยวอะไรกับ /Kids ได้ getter สาธารณะวางอยู่บนมัน: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue และ helper กลุ่ม option GetLoadedFormFieldOptionCount กับ GetLoadedFormFieldOptions ซึ่งยังเก็บ array /Opt ที่เก็บไว้บน parent ด้วย กฎหนึ่งข้อใน resolver พลาดง่าย: การเดินหยุดที่ dictionary แรกที่มีคีย์นั้น แม้ค่าที่นั่นจะเป็นสตริงว่าง /V () ท้องถิ่นคือการ override โดยเจตนาที่บัง parent ไม่ใช่ช่องว่างที่ต้องเติมจากด้านบนต่อ ๆ ขึ้นไป
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' แบก /FT /Ch, /Ff 131078 และ /Opt; ฟิลด์ลูก
// 'group.b' ถือแค่ /T, /Parent, /Rect และ /V ของตัวเอง
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // ค่า /V ท้องถิ่น
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
ทำไม /FT ท้องถิ่นจึงเป็นการทดสอบที่ผิดสำหรับฟิลด์ปลายทาง
เพราะ parent ให้ชนิดได้แล้วยังเป็นเจ้าของฟิลด์ลูกที่มีชื่ออยู่ การมี /FT จึงไม่ได้บอกอะไรเลยว่า tree ฟิลด์จบตรงไหน การเดินของรุ่นเก่าประกาศว่า node เป็นปลายทางทุกครั้งที่มี /FT ของตัวเองหรือไม่มี /Kids ในฟอร์มข้างบน group มีทั้ง /FT /Ch และ /Kids มันจึงถูกลงทะเบียนเป็นฟิลด์เดียวชื่อ group ที่มีสอง widget และชื่อเต็ม group.a กับ group.b ก็หายไปเฉย ๆ GetFormFieldCount คืน 1, การหาด้วยชื่อฟิลด์ลูกล้มเหลว และ SetFormFieldValue เขียนได้แค่ parent ที่ใช้ร่วมกัน การทดสอบที่เข้ามาแทนคือ HPDFLoadedFieldHasChildFields ซึ่งมองที่ kid แทน parent: kid เป็นฟิลด์ลูกถ้ามันมี /T ของตัวเอง มี /Kids ของตัวเอง หรือไม่ใช่ dictionary /Subtype /Widget เลย เมื่อไม่มี kid ตัวไหนเข้าเกณฑ์จึงเป็น node ปลายทาง โดย kid ถูกถือเป็น widget annotation ของมัน
edge case สองตัวที่ขึ้นรูปกฎนี้มาจาก dictionary ที่ถูกหลอมรวมทั้งคู่ ซึ่ง §12.7.3.1 ยอมให้เมื่อฟิลด์มี widget เดียว dictionary ที่หลอมรวมซึ่งมีชื่อแบก /Subtype /Widget แล้วก็ยังเป็นฟิลด์ลูก subtype เพียงอย่างเดียวจึงส่งมันไป list widget ไม่มีชื่อของ parent ไม่ได้ ฝ่าย /T ชนะ ทางกลับกันก็เกิดขึ้น: producer บางตัวเขียน /FT ของ parent ซ้ำบน widget ไม่มีชื่อทุกตัว /FT จึงใช้เป็นหลักฐานว่า widget ใดเริ่มฟิลด์ใหม่ไม่ได้เช่นกัน การจำแนกนี้ถูกใช้ร่วมกันระหว่าง relationship cache, FormFieldExists กับ RemoveFormField และการเดินทั้งหมดตอนนี้จด dictionary ที่ตัวเองเยี่ยมแล้วและหยุดเลย 128 ระดับ ไฟล์ regression ที่กลุ่มลิสต์ตัวเองซ้ำสองครั้ง /Kids [5 0 R 5 0 R 6 0 R 7 0 R] ยังรายงานสองฟิลด์พอดี แทนที่จะเรียกตัวเองไม่รู้จบหรือนับ node เดียวสองครั้ง
RemoveFormField หลีกเลี่ยงการลบฟิลด์พี่น้องอย่างไร
RemoveFormField ตอนนี้ลบแค่ฟิลด์ลูกที่คุณระบุชื่อ เพราะการค้นพบกับการลบในที่สุดเห็นตรงกันว่าฟิลด์ปลายทางคืออะไร ความเห็นตรงกันนี้สำคัญกว่าที่หน้าตา overload ที่อ้างด้วยชื่อ resolve index ผ่าน relationship cache แล้วนับฟิลด์ปลายทางในการเดินรอบที่สองบน /AcroForm /Fields เมื่อ cache ถูกแก้ให้เห็น group.a กับ group.b แล้ว ถ้าการเดินลบยังไม่ถูกแก้ มันจะยังถือ group เป็นฟิลด์ปลายทางตัวเดียว และ index 0 จะลบ parent ไปพร้อมพี่น้องทุกตัวและ widget ของพวกมันทั้งหมด การเดินลบตอนนี้ใช้ test HPDFLoadedFieldHasChildFields เดียวกันและชุด visited เดียวกัน เก็บ widget annotation ของฟิลด์ลูกที่ถูกลบเท่านั้น เอาพวกมันออกจาก /Annots ของแต่ละหน้า และลบ parent เมื่อ array /Kids ของมันกลายเป็นว่างเท่านั้น regression เช็กสามจุดที่ความผิดพลาดจะโผล่ทั้งหมด: /Kids ของ parent, /Annots ของหน้า และค่ากับ appearance ของฟิลด์พี่น้องที่รอดชีวิต ทั้งหลังเขียนใหม่เต็มรูปแบบและหลัง incremental update
// ลบฟิลด์ลูกที่มีชื่อหนึ่งตัว; พี่น้องและ parent ที่ใช้ร่วมกันรอด
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// ชนิด ธง และ option ยังถูก resolve ผ่าน parent
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
ResetLoadedFormField เขียนอะไรเมื่อค่า default มาจากการสืบทอด
ResetLoadedFormField เขียน /V ท้องถิ่นที่เป็นสำเนาใหม่ของ /DV ที่สืบทอดมาด้วยชนิด PDF object เดิม และมัน validate ค่า default ทั้งก้อนก่อนแตะฟิลด์ ชนิด object สำคัญเพราะ getter แบบสเกลาร์ปรับทุกอย่างเป็นข้อความ default ของ checkbox คือ name อย่าง /Yes, default ของ list box แบบเลือกหลายรายการคือ array ของสตริง และ default ของ text อาจเป็นสตริง UTF-16 แบบ hex การคัดลอกอะไรพวกนี้ผ่าน GetLoadedFormFieldDefaultValue จะเปลี่ยน name เป็น string, array เป็นสตริงว่าง และสตริง hex เป็นเลขตามตัวอักษร การ reset จึงแตกทางตามชนิดที่สืบทอดมา: ฟิลด์ text กับ choice ได้ string object ใหม่ที่คงธง IsHexadecimal ไว้ ฟิลด์ choice ที่ default เป็น array ได้ array ใหม่ของสตริงใหม่ และปุ่มที่ไม่ใช่ pushbutton ได้ name object ใหม่ การคัดลอกแทนการชี้ไปที่ object ของ parent ตั้งใจมา: /V ที่ใช้ร่วม array /DV ของ parent หรือเลข object ของมันจะเปลี่ยนค่า default ทุกครั้งที่มีคนแก้ค่า default ที่ผิดชนิด หรือ choice array ที่มีอะไรนอกจากสตริงจะโยน exception แล้วปล่อย /V กับ /I ให้อยู่เหมือนเดิมทุกอย่าง ส่วน pushbutton ซึ่งไม่มีค่า (Table 226, bit 17) และฟิลด์ signature ย้อนกลับไปใช้เส้นทางเดิมที่รับแต่สตริง
เมื่อไม่มี /DV อยู่ที่ไหนเลยขึ้นไปตามห่วงโซ่ method คงสัญญาเรื่องการล้างโดยเขียนสตริงว่างท้องถิ่น หรือ /Off สำหรับฟิลด์ checkbox หรือ radio การลบ /V ท้องถิ่นทิ้งจะดูเรียบร้อยกว่าแต่ผิด: parent อาจถือค่าปัจจุบันอยู่ และการเอา override ของฟิลด์ลูกออกจะพาค่านั้นกลับมาอย่างเงียบ ๆ นี่ยังเป็นเหตุผลที่ reset หนึ่งฟิลด์ไม่ใช่ action ResetForm ของ §12.7.5.3 ซึ่ง viewer รันบนชุดฟิลด์เมื่อผู้ใช้กดปุ่ม ตามที่เล่าไว้ในการสร้างฟิลด์และ action ของ AcroForm ด้วย HotPDF ResetLoadedFormField เป็น operation แก้ไขบนฟิลด์ที่โหลดมาหนึ่งตัว มีกฎของตัวเองสำหรับเคสไม่มี default และมันจดฟิลด์ผ่าน NoteLoadedFormFieldDirty เพื่อให้การคำนวณใหม่แบบ incremental เห็นการเปลี่ยน
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// parent ถือ /DV [(b) (r)] บน list box แบบ MultiSelect: group.a ได้
// /V [(b) (r)] ของตัวเองและ /I [0 2] ใหม่; parent ไม่ถูกแตะ
Pdf.ResetLoadedFormField(Field.Index);
// getter แบบสเกลาร์แสดง default แบบ array ไม่ได้
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // ว่าง
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
รักษา /V, /I และ /AS ให้ตรงกัน
การ reset ถูกต้องก็ต่อเมื่อ index การเลือกกับสถานะ appearance ตามค่าไปด้วย ResetLoadedFormField จึงปิดท้ายด้วย reconciler สองตัวเดียวกับ SetFormFieldValue HPDFReconcileChoiceSelection ตอนนี้รับค่าแบบ array: มันลบ /I ท้องถิ่นโดยไม่ mutate มัน เทียบทุกค่ากับครึ่ง export ของแต่ละ entry /Opt แล้วเขียน /I ใหม่ที่เรียงแล้วหนึ่งตัว การ reset เป็น [(b) (r)] เทียบกับ option b, g, r จึงได้ /I [0 2] ReconcileLoadedButtonAppearanceStates ตอนนี้ขอชนิดที่สืบทอดมาด้วย checkbox ลูกที่ /FT /Btn อยู่บน parent จึงได้ /AS ถูกตั้งในที่สุด ฝั่งเขียน SetFormFieldValue กับ SetLoadedFormFieldDefaultValue เก็บ name object ให้ปุ่มแบบ non-pushbutton ที่สืบทอดชนิดมา แม้ฟิลด์ลูกไม่มี entry ท้องถิ่นให้คัดลอกชนิด และเมื่อ EnsureLoadedFieldAppearanceStream สร้าง appearance ของปุ่มใหม่ มันเขียน /AS /Off เว้นแต่ค่าจะตรงกับ on state และให้ state stream แต่ละตัวมี /Type /XObject, /Subtype /Form กับ /BBox ที่ถูกต้อง ก่อน v2.754.4 การสร้าง appearance ใหม่หลัง reset อาจติ๊กกลับซะก่อนไฟล์ถูกเซฟ
ขีดจำกัดที่ควรรู้ก่อนเอาไปต่อยอด
getter แบบสเกลาร์ก็ยังเป็นสเกลาร์ GetFormFieldValue กับ GetLoadedFormFieldDefaultValue คืนสตริงว่างกับค่าแบบ array เปลี่ยนตัวเลขกับ boolean เป็น 42 หรือ true และรายงานสตริงที่เข้ารหัสแบบ hex ด้วยตัวสะกด hex ของมัน วงจร /Parent จบการเดินโดยไม่โยน exception ฟิลด์ที่ชนิดหลงอยู่ในวงจรจึงรายงาน lfftUnknown กับธงเป็น 0 แทนที่จะพัง SetFormFieldValue กับ ResetLoadedFormField เขียนฟิลด์ลูกที่คุณอ้างถึงเสมอและไม่เคยยกค่าขึ้นไปให้ parent ที่ใช้ร่วมกัน ซึ่งถูกสำหรับฟิลด์ลูกอิสระ แต่แปลว่ากลุ่ม radio ควรถูกอ้างผ่านฟิลด์ที่เป็นเจ้าของการเลือก และแต่ละ call กระทำเพียงฟิลด์เดียวของมันเอง ไม่มีอะไรที่นี่ทำให้ชุด reset เป็น transaction ได้
การ resolve แอตทริบิวต์ที่สืบทอด การจำแนก tree ฟิลด์แบบรวมเบ็ดเสร็จ และ reset ตามชนิดที่เล่าไว้ทั้งหมดเป็นส่วนหนึ่งของ API ฟอร์มที่โหลดมาในHotPDF Delphi Component สำหรับ Delphi และ C++Builder ควบคู่กับการสร้างฟิลด์ที่ครอบไว้ในการเพิ่มฟิลด์ AcroForm ให้ PDF ที่โหลดมาใน Delphi