HotPDF Delphi Component coi /FT, /Ff, /V và /DV trên một trường AcroForm đã nạp là các thuộc tính thừa kế, được phân giải bằng cách đi theo chuỗi /Parent. Kể từ v2.754.3 và v2.754.4, một trường con có tên mà kiểu của nó đến từ cha vẫn truy cập được riêng lẻ, RemoveFormField không đụng tới các anh chị em của nó, còn ResetLoadedFormField copy default thừa kế với đúng kiểu object PDF gốc. Trước đó, một số lượng đáng ngạc nhiên các biểu mẫu thường ngày đã bị đọc sai
Cái biểu mẫu bộc lộ tất cả những điều này chẳng hề lạ lùng. Một công cụ soạn thảo dựng node nhóm group mang /FT /Ch, các cờ trường và danh sách option một lần duy nhất, rồi treo hai trường con có tên a và b bên dưới, mỗi cái là một dictionary gộp trường-cộng-widget chẳng có gì ngoài /T, /Parent, /Rect và /V của riêng nó. Đó là một cách chia sẻ thuộc tính hoàn toàn hợp lệ, và cũng chính là ca mà phần Limits của bài đặt giá trị trường biểu mẫu trong một PDF đã nạp bằng Delphi đánh dấu là chưa xử lý: phần hòa giải nút bấm chỉ nhìn /FT cục bộ. Bài này nối tiếp đúng chỗ bài đó dừng lại, nói về cách cây trường được phân loại, cách giá trị thừa kế được đọc, và một reset một trường được phép ghi gì
Trường AcroForm có thể thừa kế entry nào từ cha của nó?
ISO 32000-1 §12.7.3.1, Bảng 220, đánh dấu /FT, /Ff, /V và /DV là thừa kế được, và Bảng 229 trong §12.7.4.3 làm điều tương tự với /MaxLen của trường văn bản, nên bất kỳ bộ đọc nào chỉ nhìn dictionary cục bộ sẽ báo sai kiểu, sai cờ và giá trị rỗng cho một trường con hoàn toàn hợp lệ. HotPDF dồn mọi phép đọc này qua một resolver nội bộ duy nhất, HPDFLoadedInheritedFieldObject, kiểm tra dictionary xem có khóa không, phân giải tham chiếu indirect nếu tìm thấy, nếu không thì đi theo /Parent tối đa 128 tầng, vì các tệp dị dạng có thể dựng chu trình /Parent chẳng liên quan gì tới /Kids. Các getter công khai ngồi trên nó: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue cùng các helper option GetLoadedFormFieldOptionCount và GetLoadedFormFieldOptions, thứ còn bắt được cả mảng /Opt nằm trên cha. Một luật trong resolver dễ làm sai: phép đi dừng ở dictionary đầu tiên chứa khóa, kể cả khi giá trị ở đó là chuỗi rỗng. Một /V () cục bộ là một phép ghi đè có chủ ý che khuất cha, chứ không phải một khoảng hổng cần lấp từ trên cao hơn
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' mang /FT /Ch, /Ff 131078 và /Opt; trường con
// 'group.b' chỉ mang /T, /Parent, /Rect và /V của riêng nó
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 cục bộ
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
Vì sao một /FT cục bộ là phép thử sai cho trường cuối?
Vì một cha có thể cung cấp kiểu mà vẫn sở hữu các trường con có tên, nên sự hiện diện của /FT chẳng nói lên điều gì về nơi cây trường kết thúc. Phép duyệt cũ tuyên bố một node là cuối bất cứ khi nào nó có /FT riêng hoặc không có /Kids. Trong biểu mẫu trên, group có cả /FT /Ch lẫn /Kids, nên nó được đăng ký thành một trường tên group với hai widget, và các tên đầy đủ group.a, group.b đơn giản bốc hơi. GetFormFieldCount trả về 1, tra theo tên con thất bại, còn SetFormFieldValue chỉ ghi được cha dùng chung. Phép thử thay thế, HPDFLoadedFieldHasChildFields, nhìn vào các kid thay vì cha: một kid là trường con nếu nó có /T riêng, có /Kids riêng, hoặc chẳng phải dictionary /Subtype /Widget ngay từ đầu. Chỉ khi chẳng kid nào thỏa thì node mới là cuối, với các kid được coi là widget annotation của nó
Hai ca biên định hình luật đó đều đến từ dictionary gộp, thứ §12.7.3.1 cho phép khi một trường có đúng một widget. Một dictionary gộp có tên mang /Subtype /Widget mà vẫn là trường con, nên riêng subtype không thể đẩy nó vào danh sách widget vô danh của cha; /T thắng. Chiều ngược lại cũng xảy ra: một số producer lặp /FT của cha trên mọi widget vô danh, nên /FT cũng không thể dùng làm bằng chứng rằng một widget mở đầu một trường mới. Phân loại này được relationship cache, FormFieldExists và RemoveFormField dùng chung, và mỗi phép đi trong số đó giờ ghi lại các dictionary đã ghé và dừng sau 128 tầng. Một tệp hồi quy mà nhóm liệt kê chính nó hai lần, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], vẫn báo đúng hai trường thay vì đệ quy vô hạn hay đếm trùng cùng một node
RemoveFormField tránh xóa các trường anh em ra sao?
RemoveFormField giờ chỉ xóa đúng trường con bạn gọi tên, vì phát hiện và xóa cuối cùng đã thống nhất về trường cuối là gì. Sự thống nhất ấy quan trọng hơn vẻ ngoài của nó. Overload theo tên phân giải một chỉ số qua relationship cache rồi đếm các trường cuối trong một lần đi thứ hai trên /AcroForm /Fields. Khi cache đã sửa để thấy group.a và group.b, một phép đi xóa chưa sửa vẫn coi group là một trường cuối duy nhất, và chỉ số 0 sẽ gỡ bỏ cha cùng mọi anh chị em và toàn bộ widget của chúng. Phép đi xóa giờ dùng cùng phép thử HPDFLoadedFieldHasChildFields và cùng tập đã ghé, thu thập chỉ widget annotation của trường con bị xóa, gỡ chúng khỏi /Annots của từng trang, và chỉ gỡ cha khi mảng /Kids của nó kết thúc rỗng. Bản hồi quy kiểm tra cả ba nơi một sai sót sẽ lộ diện: /Kids của cha, /Annots của trang, và giá trị cùng appearance của anh chị em sống sót, cả sau một lần ghi lại trọn vẹn lẫn sau một cập nhật tăng dần
// Gỡ một trường con có tên; anh chị em và cha dùng chung sống sót
Pdf.RemoveFormField('group.a');
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Kiểu, cờ và option vẫn được phân giải qua cha
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
ResetLoadedFormField ghi gì khi default là thừa kế?
ResetLoadedFormField ghi một /V cục bộ là bản copy tươi của /DV thừa kế với cùng kiểu object PDF, và nó xác thực toàn bộ default trước khi đụng vào trường. Kiểu object quan trọng vì các getter scalar ép mọi thứ về văn bản. Default của một checkbox là một name như /Yes, default của một list box multi-select là một mảng chuỗi, còn default văn bản có thể là chuỗi UTF-16 thập lục phân; copy bất kỳ thứ nào trong số đó qua GetLoadedFormFieldDefaultValue sẽ biến name thành chuỗi, mảng thành chuỗi rỗng và chuỗi hex thành các chữ số của nó. Vì thế phép reset rẽ nhánh theo kiểu thừa kế: trường văn bản và choice nhận một object chuỗi mới giữ cờ IsHexadecimal, trường choice có default mảng nhận một mảng mới gồm các chuỗi mới, còn nút không phải pushbutton nhận một object name mới. Copy chứ không trỏ vào các object của cha là có chủ đích: một /V dùng chung mảng /DV hay số object của cha sẽ đổi default ngay lần sau ai đó sửa giá trị. Default sai kiểu, hay một mảng choice chứa thứ gì ngoài chuỗi, sẽ ném exception và để /V với /I nguyên như cũ. Pushbutton, vốn không có giá trị (Bảng 226, bit 17), và trường chữ ký rơi về đường cũ chỉ-hỗ-trợ-chuỗi
Khi chẳng có /DV nào trên chuỗi, phương thức giữ đúng hợp đồng xóa của mình bằng cách ghi một chuỗi rỗng cục bộ, hay /Off cho checkbox hoặc radio. Xóa /V cục bộ trông sẽ gọn hơn và sẽ sai: cha có thể đang giữ một giá trị hiện hành, và gỡ phép ghi đè của con sẽ âm thầm đem giá trị đó trở lại. Đây cũng là lý do một reset một trường không phải hành động ResetForm của §12.7.5.3, thứ trình xem chạy trên một tập trường khi người dùng nhấp nút, như bài dựng trường và hành động AcroForm với HotPDF đã nói. ResetLoadedFormField là một thao tác chỉnh sửa trên một trường đã nạp, với luật riêng cho ca không có default, và nó ghi nhận trường qua NoteLoadedFormFieldDirty để việc tính lại tăng dần nhìn thấy thay đổi
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Cha giữ /DV [(b) (r)] trên một list box MultiSelect: group.a nhận
// /V [(b) (r)] và một /I [0 2] tươi; cha không bị đụng tới
Pdf.ResetLoadedFormField(Field.Index);
// Getter scalar không thể biểu diễn default mảng
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // rỗng
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Giữ /V, /I và /AS nhất quán
Một reset chỉ đúng khi chỉ số chọn và trạng thái appearance theo kịp giá trị, nên ResetLoadedFormField kết thúc bằng cùng hai bộ hòa giải với SetFormFieldValue. HPDFReconcileChoiceSelection giờ nhận giá trị mảng: nó xóa /I cục bộ mà không biến đổi nó, khớp từng giá trị với nửa export của mỗi entry /Opt, và ghi một /I mới đã sắp xếp, nên reset về [(b) (r)] với các option b, g, r cho ra /I [0 2]. ReconcileLoadedButtonAppearanceStates giờ xin kiểu thừa kế, nên một checkbox con mà /FT /Btn nằm trên cha cuối cùng cũng được đặt /AS. Ở phía ghi, SetFormFieldValue và SetLoadedFormFieldDefaultValue lưu một object name cho nút không phải pushbutton thừa kế kể cả khi con không có entry cục bộ nào để copy kiểu. Còn khi EnsureLoadedFieldAppearanceStream dựng lại appearance của nút, nó ghi /AS /Off trừ khi giá trị khớp trạng thái bật, và cho mỗi state stream một /Type /XObject, /Subtype /Form cùng /BBox đàng hoàng; trước v2.754.4, việc tái tạo appearance sau reset có thể tích lại hộp kiểm trước khi tệp được lưu
Những giới hạn đáng biết trước khi bạn xây trên nền này
Các getter scalar vẫn là scalar. GetFormFieldValue và GetLoadedFormFieldDefaultValue trả về chuỗi rỗng cho giá trị mảng, stringify số và boolean thành 42 hay true, và báo một chuỗi mã hóa hex theo chính chính tả thập lục phân của nó. Một chu trình /Parent kết thúc phép đi không kèm exception, nên một trường bị mất kiểu trong chu trình báo lfftUnknown và cờ 0 thay vì thất bại. SetFormFieldValue và ResetLoadedFormField luôn ghi đúng trường con bạn gọi và chưa bao giờ thăng giá trị lên cha dùng chung, điều đúng với các con độc lập nhưng nghĩa là nhóm radio nên được gọi thông qua trường sở hữu lựa chọn. Và mỗi lệnh gọi tự cam kết một trường; chẳng gì ở đây biến một loạt reset thành giao dịch
Phân giải thuộc tính thừa kế, phân loại cây trường thống nhất và reset có kiểu mô tả trong bài đều là một phần của loaded-form API trong HotPDF Delphi Component cho Delphi và C++Builder, cùng với việc tạo trường được trình bày trong thêm trường AcroForm vào một PDF đã nạp bằng Delphi