Checkbox và radio button flatten thành trạng thái chưa chọn vì trạng thái hiển thị /AS chưa bao giờ được đồng bộ với giá trị field /V. PDFium Component, thành phần VCL và LCL dựa trên PDFium cho Delphi, C++Builder, và Lazarus, giờ đọc giá trị đó bằng FPDFAnnot_GetFormFieldValue, hàm này giải quyết dictionary field cha thay vì annotation widget
Báo cáo lỗi dẫn tới đây thuộc loại mà thoạt đầu bạn không tin. Một khách hàng flatten một biểu mẫu đồng ý đã ký, mở kết quả ra, và mọi checkbox đều trống. Mở tệp nguồn trong Acrobat và các ô đó hiển thị rõ ràng đã được tích. Đọc lại tệp nguồn qua cùng thành phần và các giá trị field đều đúng. Chỉ có đầu ra đã flatten mới mất chúng, và chỉ với checkbox và radio button: các trường văn bản trên cùng trang thì vẫn ra đúng
Vì sao checkbox lại chưa chọn sau khi flatten?
Vì flatten không bao giờ nhìn vào /V. FPDFPage_Flatten nướng luồn hiển thị (appearance stream) của widget vào nội dung trang, và hiển thị nó chọn là hiển thị được nêu tên bởi /AS. Nếu /AS vẫn nói /Off trong khi giá trị field nói ô đó đang bật, flatten trung thành nướng luôn hiển thị tắt đó vào. Giá trị chưa bao giờ bị mất; nó chưa bao giờ được tham khảo tới
ISO 32000-1 §12.5.5 định nghĩa dictionary hiển thị /AP với ba mục có thể có, /N, /R, và /D. Đối với một checkbox hay radio button, mục /N không phải là một stream mà là một sub-dictionary với các khóa là tên trạng thái hiển thị, và §12.5.2 khiến /AS trở thành bộ chọn bắt buộc khi /N là một sub-dictionary. Vì vậy một checkbox mang hai hiển thị đã dựng sẵn và một con trỏ. Sai con trỏ đó và việc render sẽ sai theo cách mà không lượng /V đúng nào có thể sửa được. Đây cũng là lý do chế độ thất bại khác với trường văn bản, vốn hoàn toàn không có hiển thị dựng sẵn nào để chọn: một /N của trường văn bản là một stream đơn phải được tái sinh từ đầu sau khi giá trị thay đổi, nên GenerateFormAppearances xử lý hai trường hợp này qua hai đường mã hoàn toàn tách biệt và chỉ đường xử lý nút (button) mới bị hỏng
Giá trị checkbox thực sự sống ở đâu?
Trên dictionary field, không phải trên widget. ISO 32000-1 §12.7.5.2 mô tả checkbox và radio button là các trường nút mà /V của chúng là một đối tượng tên nêu tên trạng thái hiển thị hiện hành, và §12.7.3.1 đặt /V trong số các mục chung cho mọi dictionary field. Annotation widget được định nghĩa trong §12.5.6.19 đóng góp /AS và /AP. Không có gì trong đặc tả bắt buộc một widget phải mang /V
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue không hề bị lỗi. Hợp đồng của nó đúng như tên nó nói: lấy một mục chuỗi từ dictionary annotation mà bạn đưa cho nó. Hỏi nó về /V trên đối tượng 13 trả về rỗng vì đối tượng 13 thực sự không có /V. Khiếm khuyết nằm ở bên gọi, thứ đã giả định một mô hình đối tượng phẳng mà ISO 32000-1 chưa bao giờ hứa hẹn
Khi nào field và widget chia sẻ chung một dictionary?
Bất cứ khi nào một field chỉ có đúng một widget. §12.5.6.19 cho phép dictionary field và annotation widget đơn của nó được gộp thành một đối tượng, và hầu hết công cụ soạn thảo dùng lối tắt đó. Trong một đối tượng đã gộp, /FT, /T, /V, /AS, và /AP đều nằm cạnh nhau, nên một lượt đọc /V ở mức widget thành công và toàn bộ lỗi này vẫn vô hình
Ngay khi một field sở hữu từ hai widget trở lên, việc gộp là bất khả thi, và §12.7.3.1 yêu cầu các widget trở thành /Kids của một dictionary field riêng biệt. Mọi radio group đều ở hình dạng này theo cấu trúc. Các checkbox đồng ý lặp lại ở header và footer cũng vậy, cũng như bất kỳ field nào mà một công cụ soạn thảo đã sao chép sang trang thứ hai. Đó chính là toàn bộ lời giải thích cho lý do khiếm khuyết này sống sót qua một bộ test hồi quy: kho test đầy các biểu mẫu một-widget còn tệp của khách hàng thì không. Nếu bạn tự duyệt widget thay vì dựa vào thành phần này, cùng sự bất đối xứng đó xuất hiện trong thứ tự liệt kê, và ghi chú về điều hướng trường form PDF với PDFium Component trình bày cách một lượt duyệt annotation ở mức trang liên hệ với cây field ở mức tài liệu
Đọc giá trị theo đúng cách PDFium hướng tới
FPDFAnnot_GetFormFieldValue là API đúng, và nó đã được liên kết trong thành phần này một thời gian mà không được đường xử lý checkbox sử dụng. Nó nhận cả handle form lẫn annotation, đây chính là tín hiệu quan trọng: với môi trường điền form khả dụng, PDFium giải quyết annotation về control form của nó và đọc giá trị từ đối tượng field, nên nó trả về câu trả lời đúng cho cả bố cục đã gộp lẫn đã tách
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
Hai chi tiết trong đoạn mã đó dễ bị làm sai. Độ dài trả về là số byte cho văn bản UTF-16 bao gồm cả ký tự kết thúc, nên số ký tự là buflen div 2 - 1 và một giá trị bằng 2 nghĩa là một chuỗi rỗng. Vì vậy phép bảo vệ buflen >= 4 nghĩa là có ít nhất một ký tự thật, đây chính là điều giữ cho một field hoàn toàn không có /V không bị ghi đè /AS của nó bằng một tên rỗng
/AS và /AP /N thực sự đồng thuận với nhau ở điều gì
Chúng đồng thuận về một cái tên, và cái tên đó do bất kỳ ai tạo ra tệp lựa chọn. §12.7.5.2 yêu cầu trạng thái tắt phải được gọi là /Off, và hoàn toàn để mặc trạng thái bật cho bên tạo. /Yes là một quy ước, không phải một quy tắc. Acrobat ghi /Yes, nhưng rất nhiều bộ tạo ghi /On, /1, /Choice1, hay một từ đã bản địa hóa, và một radio group thông thường cho mỗi thành viên (kid) của nó một tên trạng thái-bật riêng biệt để nhóm có thể diễn đạt nút nào đang được chọn. Đây chính xác là lý do vì sao sao chép /V nguyên văn vào /AS là thao tác đúng chứ không phải một mẹo vặt: đối với một control đã chọn, PDFium báo cáo tên trạng thái-bật mà chính tệp đó định nghĩa, và đối với một control chưa chọn, nó báo cáo Off, nên giá trị bạn ghi vào /AS được bảo đảm là một khóa tồn tại trong sub-dictionary /AP /N của widget đó. Gắn cứng /Yes sẽ hoạt động trên đầu ra của Acrobat và âm thầm hỏng ở mọi nơi khác
Thứ tự thao tác, và nơi vẫn cần thận trọng
Chuỗi thao tác này là cố định và không khoan nhượng: bật điền form, gán giá trị, tái sinh hiển thị, flatten, rồi mới lưu. Bỏ qua bước tái sinh và FPDFPage_Flatten tìm thấy các luồng hiển thị rỗng hoặc lỗi thời và nướng chúng vào mà không một lời phàn nàn, đây là một sự mất dữ liệu âm thầm chứ không phải một lỗi trả về
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
Còn hai giới hạn trung thực. Thứ nhất, việc đồng bộ ghi giá trị field vào /AS của mọi widget của field đó, điều này đúng với checkbox nhưng chỉ là xấp xỉ đối với các radio group mà mỗi thành viên tự định nghĩa tên trạng thái-bật riêng của nó; một thành viên có /AP /N không có mục nào khớp với /AS đã ghi thì không có hiển thị nào để chọn theo §12.5.5, nên một nút chưa được chọn có thể flatten thành trống rỗng thay vì một vòng tròn trống. Kiểm tra một radio group bằng FPDFAnnot_GetFormControlIndex trước khi flatten đáng bỏ ra vài dòng mã. Thứ hai, không điều nào trong số này áp dụng cho XFA, nơi giá trị sống trong một gói dữ liệu XML thay vì trong các dictionary AcroForm, một sự phân tách được trình bày trong ghi chú về chỉnh sửa trường XFA không được lưu lại. Bài học tổng quát đáng giữ lại vượt xa bản sửa lỗi này: bất cứ khi nào một API nhận cả handle form lẫn annotation, nó đang nói với bạn rằng nó sẽ giải quyết cây field thay cho bạn, và bất cứ khi nào nó chỉ nhận annotation, nó sẽ đọc đúng chính xác đối tượng bạn đã truyền vào. Sự phân biệt đó cũng chi phối việc trao đổi dữ liệu, vì xuất và nhập dữ liệu form XFDF hoạt động theo tên field đủ điều kiện, không bao giờ theo vị trí widget
Flatten form là một trong những tính năng trông có vẻ chỉ là một lệnh gọi API đơn lẻ nhưng hóa ra lại là một hợp đồng giữa ba dictionary. Nếu bạn muốn làm việc với một thành phần đã mã hóa sẵn hợp đồng đó, PDFium Component cho Delphi và C++Builder cung cấp việc tái sinh hiển thị, flatten, và truy cập trường form được mô tả ở đây dưới dạng các property và method thông thường