Bài viết kỹ thuật

Trường PDF multi-select qua round trip FDF và XFDF (Delphi)

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

Round trip multi-select cũ của HotPDF, nơi hai option list box được chọn, một cái chứa sẵn dấu xuống dòng, bị đường scalar GetFormFieldValue làm phẳng thành chuỗi đơn Deep, xuống dòng, Blue, xuống dòng, Red, mà các trình đọc hạ nguồn có thể parse thành hai lựa chọn hoặc ba
Nối các giá trị multi-select thành một chuỗi phá hủy ranh giới giữa các giá trị, còn một giá trị export tự chứa dấu xuống dòng khiến dạng bị làm phẳng trở nên mơ hồ

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

Hình dạng export HotPDF ghi cho một list box multi-select kể từ 2.755.0: FDF mang một mảng có kiểu mỗi trường với /V [(Deep xuống dòng Blue) (Red)] và một giá trị region hex, còn XFDF mang một phần tử value mỗi lựa chọn dưới xml:space preserve nên khoảng trắng tính là dữ liệu
Ranh giới vẫn nhìn thấy được trên đĩa: FDF giữ mỗi lựa chọn thành một mục mảng riêng và XFDF ghi mỗi cái trong một phần tử value tách bạch, nên chẳng importer nào phải đoán
<!-- 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&#xA;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

Xác thực import cho giá trị multi-select của HotPDF: đích phải là choice field có MultiSelect đặt trong /Ff, mọi giá trị đến phải khớp một giá trị export /Opt với mỗi slot dùng một lần, /I được dựng lại tăng dần theo thứ tự /Opt, còn /V và /I tách rời chỉ được gán sau khi mọi giá trị qua hết
Mỗi giá trị đến được đối chiếu với các option của trường trước khi bất cứ thứ gì được ghi, nên một giá trị bị từ chối chưa bao giờ để lại nửa mảng hay các chỉ số chọn cũ kỹ

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