Technical Article

Biểu mẫu PDF tương tác trong Delphi: Action và JavaScript

Bản thân một trường biểu mẫu PDF chỉ là một hộp chứa một giá trị. Điều làm cho biểu mẫu hoạt động giống như một ứng dụng nhỏ là action được đính kèm với nó: một cú nhấp chuột ẩn đi một phần, kéo các giá trị đã lưu trở lại từ một tệp, nhảy đến trang cuối cùng, hoặc chạy một kịch bản tính tổng một cột. Không có điều nào trong số đó nằm trong trường. Nó nằm trong một từ điển action (action dictionary), và tiêu chuẩn ISO 32000-1 sắp xếp toàn bộ họ này trong mục §12.6. Bài viết này sẽ đi qua các action mà chương trình Delphi thường dùng nhất và chỉ ra cách PDFlibPas kết nối từng action với một trường hoặc một liên kết.

Mô hình tư duy cần ghi nhớ là trường và action là các đối tượng riêng biệt được liên kết bằng một tham chiếu. Một widget annotation hoặc một link annotation mang một action trong mục nhập /A của nó. Action gọi tên trường mà nó tác động bằng tiêu đề (title), không phải bằng chỉ mục (index), vì vậy tiêu đề bạn đặt cho một trường là handle mà mọi action sau đó sử dụng để tìm thấy nó. Khi sự phân chia đó đã rõ ràng, API sẽ không còn giống như một tập hợp các cuộc gọi lộn xộn nữa và bắt đầu giống như một mẫu duy nhất được áp dụng cho bốn loại động từ.

Named action: điều hướng không cần số trang

Các action đơn giản nhất không mang theo bất kỳ tham số nào. Tiêu chuẩn ISO 32000-1 §12.6.4.11, Bảng 194, định nghĩa các named action: trình xem sẽ thông dịch một tên tượng trưng tại thời điểm chạy thay vì đi theo một đích đến được lưu trữ. Bốn tên được hỗ trợ phổ biến, và chúng chính xác là những gì người đọc mong đợi từ một thanh công cụ: NextPage, PrevPage, FirstPage, và LastPage. Bởi vì đích đến là tương đối so với bất kỳ trang nào mà trình xem hiện đang hiển thị, một nút Next được xây dựng theo cách này hoạt động trên mọi trang mà bạn không cần phải tính toán mục tiêu.

Trong PDFlibPas, một named action được đính kèm vào một hình chữ nhật vùng nóng (hotspot) trên trang hiện tại. Các đối số nguyên thứ tư và thứ năm sẽ chọn động từ và giao diện.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // jump to last page

Không có đích đến nào cần giữ đồng bộ, đó là toàn bộ mấu chốt. Một named action vẫn tồn tại khi chèn và xóa trang vì nó không bao giờ đặt tên cho một trang ngay từ đầu. Hãy so sánh điều đó với một liên kết chuyển đến trực tiếp (explicit go-to link), vốn lưu trữ một chỉ mục trang mục tiêu mà bạn phải đánh số lại ngay khi tài liệu phình to.

Action Hide và cạm bẫy mảng

Action Hide, ISO 32000-1 §12.6.4.10, Bảng 196, chuyển đổi khả năng hiển thị của một hoặc nhiều trường. Đó là cách sạch sẽ nhất để xây dựng hành vi hiển thị và ẩn mà không cần lập trình, và đó là những gì bạn muốn cho một liên kết Hiển thị chi tiết hoặc cho hai bảng loại trừ lẫn nhau mà việc tiết lộ bảng này sẽ che giấu bảng kia. Action mang theo một mục tiêu trong mục nhập /T của nó và một giá trị boolean /H quyết định hướng: ẩn khi true, hiển thị khi false.

Sự tinh tế hoàn toàn nằm ở cách mã hóa mục tiêu đó, và đó là loại chi tiết tạo ra một biểu mẫu hoạt động trên máy của bạn nhưng lại thất bại trên máy của khách hàng. Khi action đặt tên cho một trường duy nhất, /T được viết dưới dạng một chuỗi văn bản. Khi nó đặt tên cho nhiều trường, /T được viết dưới dạng một mảng các chuỗi văn bản. Các trình xem cũ hơn không xử lý một mảng một phần tử theo cách giống như một chuỗi trần, vì vậy việc mã hóa phải phân nhánh dựa trên số lượng: một tên duy nhất phải được xuất ra dưới dạng một chuỗi, không phải là một mảng có độ dài bằng một, để nhiều trình đọc nhất có thể tuân thủ nó. PDFlibPas đưa ra quyết định đó cho bạn. Bạn truyền các tên trường cách nhau bằng dấu phẩy, dấu chấm phẩy, hoặc dấu xuống dòng, và trình viết sẽ xuất ra một chuỗi duy nhất cho một tên và một mảng cho hai tên trở lên.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Bởi vì action không tham chiếu đến tài nguyên bên ngoài nào, nó vẫn tương thích với PDF/A. Các tên bạn truyền là tiêu đề trường đủ điều kiện (fully qualified field titles), đó là lý do tại sao một trường con bên trong một nhóm phải được định địa chỉ thông qua đường dẫn chấm đầy đủ của nó thay vì tên lá trần của nó.

ImportData: điền trước từ FDF

Trong khi action Hide sắp xếp lại những gì đã có trên trang, action import-data mang các giá trị từ bên ngoài vào. Tiêu chuẩn ISO 32000-1 §12.6.4.8, Bảng 198, định nghĩa nó là một action điền dữ liệu vào AcroForm từ một tệp Forms Data Format trên đĩa. Đây là action đằng sau tính năng Reload sample data hoặc Reset to defaults control, nơi một tệp FDF đi kèm bên cạnh tệp PDF và giữ các giá trị trường chuẩn tắc. Lệnh gọi phản chiếu những lệnh gọi khác, lấy hình chữ nhật vùng nóng, đường dẫn đến FDF, và một bitmask giao diện: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Tệp không cần phải tồn tại khi PDF được xây dựng, nhưng nó phải có mặt khi người dùng nhấp chuột, và bất kỳ dấu gạch chéo ngược nào trong đường dẫn đều được viết lại thành dạng gạch chéo chuẩn PDF cho bạn.

Một hạn chế đáng để nêu rõ vì nó thường gây ngạc nhiên. Một action import-data trỏ đến một tệp bên ngoài, vì vậy nó không được cho phép trong PDF/A. Khi tài liệu ở chế độ PDF/A, lệnh gọi trả về số không và không thêm gì cả thay vì tạo ra một tệp không vượt qua kiểm tra tính tương thích. Nếu luồng xử lý của bạn hướng tới đầu ra lưu trữ, việc điền trước phải xảy ra tại thời điểm tạo bằng cách ghi trực tiếp các giá trị trường, chứ không phải hoãn chúng cho đến khi có cú nhấp chuột.

JavaScript: gói toàn cục và script theo từng action

Đối với logic vượt quá việc hiển thị, ẩn và nhập, họ action tìm đến JavaScript cấp tài liệu. Có hai nơi riêng biệt mà một script có thể tồn tại, và sự khác biệt này rất quan trọng. Một gói JavaScript cấp tài liệu được lưu trữ một lần cho toàn bộ tệp và chạy khi tài liệu mở ra, khiến nó trở thành ngôi nhà thích hợp cho các định nghĩa hàm và trạng thái dùng chung. Một script theo từng action được gắn vào một liên kết hoặc trường và chỉ chạy khi đối tượng đó được kích hoạt, khiến nó trở thành ngôi nhà thích hợp cho dòng mã duy nhất gọi một hàm mà gói đã định nghĩa.

PDFlibPas hiển thị cả hai. AddGlobalJavaScript lưu trữ một gói được đặt tên ở cấp tài liệu; việc sử dụng lại một tên sẽ thay thế bất cứ thứ gì được lưu trữ dưới tên đó. AddLinkToJavaScript gắn một script vào một vùng nóng để cú nhấp chuột thực thi nó.

// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Giữ hàm trong gói toàn cục và cuộc gọi trong liên kết không phải là một sở thích phong cách viết mã. Nó tránh việc sao chép cùng một thân hàm trên mọi điều khiển cần nó, và điều đó có nghĩa là một trình xem đã vô hiệu hóa tính năng lập trình sẽ đơn giản không làm gì khi nhấp chuột thay vì bị nghẹn bởi một khối dữ liệu nội tuyến bị lỗi. Nó cũng giữ cho các mục nhập theo từng action nhỏ gọn, giúp tệp dễ đọc khi bạn kiểm tra nó sau này.

Trường, trường con, và đóng băng kết quả

Các action cần các trường để tác động, vì vậy sẽ hữu ích khi xem cách một trường được hình thành. NewFormField tạo một trường trên trang hiện tại và trả về chỉ mục của nó; kiểu số nguyên chọn loại trường, trong đó 1 là Text, 2 là Pushbutton, 3 là Checkbox, 4 là Radiobutton, 5 là Choice, 6 là Signature, và 7 là Parent sở hữu các trường con nhưng bản thân không vẽ gì. Tiêu đề bạn truyền không được chứa dấu chấm, vì dấu chấm là dấu phân cách trong các tên đủ điều kiện mà các action sử dụng để định vị các trường con.

Các nhóm nút radio và biểu mẫu phân cấp được xây dựng bằng cách cung cấp cho một trường cha các trường con. NewChildFormField thêm một trường con dưới một trường cha được đặt tên, và đối với các trường hợp radio và choice, AddFormFieldSub thêm các tùy chọn riêng lẻ và trả về một chỉ mục tạm thời mà bạn sử dụng để định vị từng tùy chọn. Khi giai đoạn tương tác kết thúc và bạn muốn đóng băng một trường để giao diện hiện tại của nó trở thành nội dung trang vĩnh viễn, FlattenFormField sẽ vẽ trường đó lên trang và loại bỏ nó khỏi biểu mẫu. Sau khi làm phẳng (flatten), chỉ mục của các trường sau đó sẽ dịch chuyển xuống một đơn vị, đây là điều duy nhất cần nhớ nếu bạn làm phẳng nhiều trường trong một vòng lặp.

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Lệnh gọi flatten được ghi chú lại có mục đích. Bỏ qua nó và tài liệu sẽ được gửi đi dưới dạng một biểu mẫu sống động có các action kích hoạt trong trình đọc. Bật nó lên và trường sẽ được kết xuất thành các đánh dấu tĩnh, đó là những gì bạn muốn khi biểu mẫu đã hoàn thành và kết quả cần truyền đi dưới dạng một bản ghi cố định. Cùng một trường, cùng một mã, hai tài liệu rất khác nhau tùy thuộc vào việc bạn có đóng băng nó hay không.

Lựa chọn động từ phù hợp

Bốn action phân chia rõ ràng theo những gì chúng tác động. Một named action di chuyển khung nhìn và không cần trường nào. Một action Hide thay đổi khả năng hiển thị và cần tiêu đề trường, với việc mã hóa chuỗi so với mảng được xử lý tự động cho bạn. Một action import-data tiếp cận một tệp trên đĩa và do đó bị cấm trong PDF/A. Một action JavaScript chạy logic tùy ý và tốt nhất nên được chia tách giữa một gói hàm toàn cục và các lệnh gọi nhỏ theo từng action. Hãy tìm đến cách đơn giản nhất để hoàn thành công việc: một action Hide dễ di chuyển hơn là một script đặt cờ ẩn, và một named action bền vững hơn là một đích đến trang được lưu trữ vì không có số trang nào cần duy trì.

Từ đây, hai chủ đề lân cận sẽ hoàn thiện bức tranh. Nếu biểu mẫu là một phần của tài liệu có thể truy cập được, cây cấu trúc mà trình đọc màn hình duyệt qua được đề cập trong our article on tagged PDF and accessibility structure. Khi biểu mẫu đã điền xong cần được khóa và ký, quy trình làm việc được mô tả trong the compliance and signing workbench walkthrough. Cả ba đều được xây dựng trên cùng một công cụ, được cung cấp dưới dạng PDF library for Delphi cùng với các API tạo, biểu mẫu và chữ ký được đề cập ở những nơi khác trên blog này.