HotPDF round-trip các giá trị list box multi-select qua FDF và XFDF bằng cách giữ giá trị trường dưới dạng mảng từ đầu tới cuối. Kể từ bản 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF và ExportLoadedFormToXFDF ghi mỗi option được chọn thành một chuỗi FDF hay một phần tử <value> XFDF riêng, và các phương thức import tương ứng đối chiếu từng giá trị với các option của trường rồi dựng lại các chỉ số chọn /I trước khi đụng vào bất cứ thứ gì. Chẳng gì bị dán vào một chuỗi duy nhất dọc đường
Thất bại mà bản này sửa thì dễ tái hiện. Lấy một biểu mẫu đặt hàng với một list box multi-select các option sản phẩm, cho người dùng chọn hai cái, xuất dữ liệu biểu mẫu cho một hệ thống back-office, rồi nhập tệp đã sửa trở lại vào PDF. Trước thay đổi này, list box quay về rỗng hay sai. Nguyên nhân là một trong các giá trị export chứa một dấu xuống dòng, và đường cũ đã làm phẳng các lựa chọn thành một chuỗi đơn phân tách bằng dòng. Lấy nhiều lựa chọn ra khỏi chuỗi ấy chưa bao giờ đáng tin, và với một giá trị export mà tự nó chứa dấu xuống dòng thì căn bản không thể chạy
Vì sao nối các giá trị multi-select bằng dấu xuống dòng lại hỏng round trip?
Nối các lựa chọn thành một chuỗi là vứt bỏ ranh giới giữa các giá trị, mà một giá trị lại có thể chứa chính dấu phân tách, nên chẳng importer nào tách chuỗi về đúng được. ISO 32000-1 §12.7.4.4 cho phép entry /V của một choice field là một chuỗi văn bản đơn hoặc một mảng các chuỗi văn bản, và một list box mang cờ MultiSelect (bit 22 của /Ff) dùng dạng mảng một khi nhiều hơn một option được chọn. Cùng mục đó định nghĩa /I là một mảng chỉ số option đếm từ 0 theo thứ tự tăng dần, thứ mà trình xem dùng để phân biệt hai option tình cờ chung một giá trị export. Trong HotPDF, getter scalar GetFormFieldValue chỉ đọc dạng chuỗi, nên đẩy một mảng qua nó làm mức export thoái hóa thành chuỗi rỗng, còn import XFDF cũ nối các phần tử <value> lặp lại bằng LF. Hãy hình dung một option được export là Deep, xuống dòng, Blue: sau khi nối, Deep\nBlue\nRed có thể là hai lựa chọn hoặc ba, và tệp chẳng cho cách nào biết là cái nào. Bản sửa là bỏ hẳn việc dùng scalar ở giữa round trip
Tệp FDF và XFDF đã export chứa gì?
HotPDF ghi một giá trị multi-select thành một mảng có kiểu trong FDF và một phần tử <value> mỗi lựa chọn trong XFDF, nên các ranh giới vẫn nhìn thấy được trên đĩa. Trong FDF, mỗi mục giữ nguyên chính tả như trong PDF nguồn: chuỗi thập lục phân đi ra dưới dạng hex, còn literal string được escape bởi một helper duy nhất biến CR và LF thành \r và \n. Trong XFDF, gốc mang xml:space="preserve" như ISO 19444-1 yêu cầu, nghĩa là bất kỳ khoảng trắng nào bên trong một phần tử văn bản đều tính là dữ liệu. Vì thế HotPDF ghi thẻ mở, văn bản đã escape và thẻ đóng của mỗi <value> liền một mạch, giữ phần thụt đầu dòng ở ngoài phần tử, và mã hóa CR, LF, TAB thành character reference để một XML parser áp dụng chuẩn hóa cuối dòng không thể đổi các byte gốc
<!-- FDF: một mảng có kiểu mỗi trường -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: một <value> mỗi lựa chọn -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Hai ca biên của export đáng biết trước khi bạn viết code gọi. Thứ nhất, ExportLoadedFormToFDF dựng trọn body FDF trong bộ nhớ trước khi tạo tệp đích (đã sửa trong 2.755.1), nên một giá trị không thể export, ví dụ một mảng chứa thứ gì đó ngoài chuỗi, sẽ ném lỗi mà không cắt cụt tệp đang có. Thứ hai, một lựa chọn rỗng trên list box mà lại có kèm một giá trị export chuỗi rỗng là mơ hồ trong XFDF, vì <value/> có thể nghĩa là chẳng chọn gì hoặc là option rỗng đang được chọn. ExportLoadedFormToXFDF ném lỗi trong ca đó thay vì đoán, và nó ném trước khi tệp đích được mở. FDF không có sự mơ hồ ấy, vì /V [] và /V [()] là hai thứ khác nhau. Cả hai bộ export FDF còn bỏ qua các terminal chỉ-widget không có tên /T, khớp với bộ export XFDF, vì chẳng importer nào có thể ghép những entry đó ngược về một trường
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// List box multi-select được ghi thành /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Lựa chọn rỗng cộng một option export rỗng: XFDF không phân biệt
// nổi hai thứ, và tệp .xfdf hiện có được giữ nguyên
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
HotPDF xác thực một giá trị multi-select lúc import thế nào?
HotPDF chỉ chấp nhận một mảng import khi đích là một choice field có cờ MultiSelect và mọi giá trị trong mảng khớp với một giá trị export trong mảng /Opt của trường. Mỗi slot option chỉ được dùng một lần, nên một danh sách với hai option chung giá trị export b chấp nhận [<62> <62>] là hai lựa chọn khác nhau và từ chối b thứ ba. /I dựng lại theo thứ tự /Opt chứ không theo thứ tự các giá trị đến, vì §12.7.4.4 đòi chỉ số tăng dần. HotPDF dựng /V và /I mới như các object tách rời và chỉ gán sau khi mọi giá trị đã qua xác thực, nên một giá trị bị từ chối chưa bao giờ để lại nửa mảng hay chỉ số cũ kỹ. Bản copy được ghi vào trường đang import chứ không vào một mảng tổ tiên dùng chung, chính tả hex đến từ FDF giữ nguyên hex xuyên qua lần lưu, và các trường có phép tính phụ thuộc list box bị đánh dấu để tính lại. Nếu bạn chỉ cần đặt một giá trị đơn, đặt một giá trị trường biểu mẫu trong PDF đã nạp đi qua đường scalar, vốn theo thiết kế không xử lý lựa chọn nhiều
Một số công cụ khác ghi giá trị export ASCII thường thành hex string không kèm byte order mark, ví dụ <416272>, rồi export XFDF bằng cách viết các chữ số hex đó ra dưới dạng văn bản. Phép so sánh literal nghiêm ngặt trên đường về sẽ thất bại, và import dừng. Bản 2.755.1 thêm một lượt retry: khi một giá trị không khớp option nào, HPDFHexSpellingText giải mã văn bản như một payload hex và so lại kết quả. Lượt retry chỉ áp dụng cho input mà lẽ ra đã ném lỗi, nên nó chẳng bao giờ đổi một giá trị đã khớp. Cùng bản phát hành đó còn khiến đường scalar và đường mảng dùng chung một bộ giải mã Unicode, thứ hiểu PDFDocEncoding, UTF-16 với byte order mark theo một trong hai chiều và UTF-8. Trước đó, một giá trị logic có thể khớp trên đường này mà gục trên đường kia trong những tài liệu trộn các bảng mã
Vì sao một tệp FDF hợp lệ vẫn có thể mất trường khi parse?
Một scanner FDF không theo dõi chuỗi thập lục phân có thể cắt đôi một dictionary trường khi một giá trị hex kết thúc ngay sát ký tự kết thúc dictionary. Trong << /T (region) /V <416273>>>, dấu > đầu đóng chuỗi hex, nhưng một scanner ngây thơ đọc nó cùng dấu > kế tiếp thành điểm kết thúc dictionary và lặng lẽ đánh rơi trường. Bộ import FDF cấp tệp đã theo dõi việc mình có đang bên trong một chuỗi hex hay không từ trước, và trong 2.755.1, các scanner mảng và dictionary đằng sau ImportLoadedInterchangeFromFDF làm y như vậy. Vấn đề thứ hai liên quan tới tham chiếu indirect. Một tệp FDF là một tài liệu cú pháp PDF nhỏ với cách đánh số object riêng (ISO 32000-1 §12.7.7), nên một giá trị như /V [11 0 R] ám chỉ object 11 của tệp FDF, chứ không phải object 11 của PDF bạn đang điền. Bộ parser FDF đơn giản hóa của HotPDF không phân giải tham chiếu bên trong tệp, nên nó từ chối một mảng như thế thay vì đọc đại object 11 tình cờ là gì trong tài liệu đích
Import tệp, stream và XFDF báo lỗi khác nhau
Ba đường import xác thực cùng một cách nhưng báo thất bại khác nhau, và đáng để chọn một cách có chủ đích. ImportLoadedFormFromFDF bỏ qua mọi trường trượt xác thực và trả về số trường nó đã áp dụng, nên một con số thấp hơn mong đợi là dấu hiệu duy nhất của vấn đề. ImportLoadedInterchangeFromFDF và ImportLoadedFormFromXFDF ném lỗi ngay trường đầu tiên bị từ chối. Mỗi trường được cam kết riêng, nên các trường xử lý trước exception giữ nguyên giá trị mới của chúng. Đừng coi cái nào trong số này là một giao dịch trên toàn bộ tệp trao đổi: nếu bạn cần hành vi tất-cả-hoặc-không-cả, hãy vứt bỏ tài liệu đã nạp khi exception xảy ra thay vì lưu nó
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// Chỉ các trường; một giá trị ngoài /Opt hay một đích không multi-select sẽ ném lỗi
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Mở rộng các callback XFDF mà không gãy các caller hiện có
Phần hỗ trợ mảng trong unit XFDF tầng thấp nằm trong một record riêng, THPDFXFDFArrayAccess, và trong các overload mới của HPDFXFDFExportFields với HPDFXFDFImportFields, chứ không phải trong các trường thêm vào cuối record THPDFXFDFAccess hiện có. Lý do là tính tương thích nhị phân. Code đổ THPDFXFDFAccess như một biến cục bộ thường chỉ đặt những slot nó biết và chẳng bao giờ dọn phần còn lại, nên một con trỏ hàm mới thêm vào record đó sẽ chứa rác từ stack, và thư viện sẽ nhầm nó thành một callback thật. Với một record riêng, các caller cũ giữ nguyên layout cũ và các overload cũ, còn những overload đó truyền một record mảng toàn-nil nội bộ. Overload import scalar gốc vẫn nối các giá trị lặp bằng LF để giữ tương thích, và chỉ overload hiểu-mảng mới tách chúng ra. Khi bạn gắn kho dữ liệu của riêng mình, hãy bắt đầu từ Default(THPDFXFDFArrayAccess). Trả về True từ GetFormFieldValueArray với bất kỳ trường nào có giá trị danh sách, kể cả trường chẳng chọn gì, và False để rơi về callback scalar
uses HPDFXFDF;
// Con trỏ hàm thường, không phải "of object": Context mang kho của riêng bạn
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // các binding scalar hiện có của bạn
ArrayAccess := Default(THPDFXFDFArrayAccess); // mọi slot không dùng đều là nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Trao đổi multi-select chạy trên các list box đã tồn tại và có bit MultiSelect đặt trong /Ff. Còn các choice field cùng bit cờ của chúng được tạo ra thế nào ngay từ đầu, xem thêm ListBox và các trường AcroForm khác vào một PDF đã nạp. Còn markup chú thích đi qua cây <annots> của XFDF, xem import và export annotation XFDF trong HotPDF. Tài liệu tham chiếu API đầy đủ và bản tải thử nằm ở trang HotPDF Delphi PDF component