Một hành động AcroForm (AcroForm action) là một dictionary được gắn vào một widget, cho trình xem biết phải làm gì khi có điều gì đó xảy ra với widget đó. Nhấp vào một nút và trình xem đọc dictionary hành động của nó: một hành động URI mở một địa chỉ web, một hành động JavaScript chạy script, một hành động SubmitForm gửi các giá trị trường đã thu thập đến một endpoint, một hành động ResetForm xóa chúng về giá trị mặc định. Hành động là dữ liệu, không phải hành vi được đúc sẵn vào tệp. ISO 32000-1 §12.6 định nghĩa hình dạng của dictionary; trình xem cung cấp engine diễn giải nó. Sự phân tách đó quan trọng vì một hành động được viết hoàn hảo vào PDF vẫn không làm gì cả nếu trình đọc ở đầu kia không có engine cho nó, và phần lớn rắc rối với AcroForm bắt nguồn từ khoảng cách đó hơn là từ một trường bị lỗi định dạng
HotPDF ghi các dictionary đó trực tiếp từ Delphi và C++Builder, cùng với các widget trường mà chúng gắn vào. Có hai cấu trúc tham gia trong mọi biểu mẫu tương tác: widget mà người dùng nhìn thấy trên trang, và bộ máy trường cùng hành động bên dưới mang dữ liệu và hệ thống nối dây. Chúng được chỉnh sửa độc lập với nhau, và một trong hai có thể sai trong khi cái kia trông vẫn ổn. Các phần dưới đây sẽ đi qua việc đặt tên trường, bản thân các hành động của nút, JavaScript ở cấp trường, và loại lỗi vẫn qua được kiểm tra trực quan vì nó nằm hoàn toàn trong cấu trúc thứ hai
Tên trường là khóa định tuyến, không phải chú thích hiển thị
Mỗi trường AcroForm mang một tên đầy đủ (fully qualified name). ISO 32000-1 §12.7.3 quy định tên đó, chứ không phải chú thích hiển thị, là khóa mà giá trị của trường di chuyển theo khi biểu mẫu được xuất hoặc gửi đi. Các nhà phát triển đến từ thiết kế VCL có xu hướng coi tên của một control là một định danh mã nội bộ, và ở đây nó không phải vậy. Đó là định dạng truyền tải (wire format)
Điều đầu tiên rút ra từ đó là hai trường có cùng tên đầy đủ không phải là hai trường. PDF coi chúng là hai chú thích widget của một trường, chia sẻ một giá trị, nên gõ vào một trường sẽ cập nhật trường kia ngay lập tức. Đó chính xác là điều bạn muốn khi tên khách hàng phải lặp lại trên mọi trang của một hợp đồng. Đó là lỗi khi một vòng lặp tạo biểu mẫu vô tình dùng lại 'Field1' trên ba trang. Không có kiểm tra trực quan nào phát hiện trường hợp thứ hai. Mỗi trang vẫn vẽ ô riêng của nó, và mối liên kết chỉ lộ ra khi ai đó bắt đầu gõ chữ
Các tên có dấu chấm như applicant.email xây dựng một hệ thống phân cấp. Nút cha applicant nhóm các con của nó lại, và đây chính là điều cho phép một hành động đặt lại hoặc gửi chỉ nhắm vào một phần của biểu mẫu. Đặt tên trường theo cách này ngay từ đầu không tốn gì cả, và nó sẽ đem lại lợi ích ngay lần đầu tiên hệ thống nhận yêu cầu chỉ khối applicant
Nút radio có quy tắc riêng của nó. Các nút cần chuyển đổi cùng nhau phải chia sẻ một tên nhóm. Trong HotPDF, các lệnh gọi AddRadioButton truyền cùng tên nhóm sẽ gắn widget của chúng vào một trường cha duy nhất, và giá trị xuất (export value) của mỗi nút ('basic' hoặc 'full') xác định lựa chọn đã chọn. Đặt cho mỗi nút một tên riêng biệt và bạn sẽ có một hàng công tắc bật/tắt độc lập thay vì một nhóm loại trừ lẫn nhau, thứ hiển thị giống hệt nhau nhưng hoạt động sai
Tạo tập hợp trường theo từng trang
HotPDF đặt các trường thông qua các phương thức THPDFPage, nên mỗi trường thuộc về đối tượng trang đã tạo ra nó. Bẫy về trình tự cần chú ý là AddPage. Nó trỏ lại CurrentPage sang trang mới ngay khi nó trả về, nên bất kỳ lệnh gọi trường nào sau đó sẽ rơi vào trang mới ngay cả khi trường đó về mặt logic thuộc về trang bạn vừa rời khỏi. Hoàn tất từng trang, cả nội dung được vẽ lẫn các trường, trước khi bạn gọi AddPage
procedure BuildClaimForm(Pdf: THotPDF);
begin
// Trang 1: khối applicant
Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
Pdf.CurrentPage.AddComboBox('plan', 'Standard',
['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));
Pdf.AddPage; // CurrentPage giờ trỏ đến trang 2
Pdf.CurrentPage.AddListBox('riders', 'None',
['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;
Tọa độ sử dụng quy ước của PDF, với gốc tọa độ ở góc dưới bên trái của trang. Đây là gốc tọa độ giống với gốc mà TextOut dùng cho văn bản được vẽ, nên Rect(50, 100, 200, 120) nằm gần đáy của một trang khổ Letter, không phải phía trên. VCL đặt Y ở trên cùng và tăng dần xuống dưới, nên một bảng bố cục được chuyển thẳng sang sẽ bị lật ngược theo chiều dọc, mọi trường bị đảo sang đầu sai của trang. Hãy thực hiện việc chuyển đổi một lần trong một hàm hỗ trợ dùng chung thay vì tại từng điểm gọi, và một lần sửa duy nhất sẽ khắc phục toàn bộ biểu mẫu
Nối các nút với hành động URI, JavaScript và submit
Một nút nhấn (push button) không hoạt động cho đến khi có một hành động được gắn vào nó. HotPDF phơi bày các loại hành động từ ISO 32000-1 §12.6.4 thông qua kiểu liệt kê THPDFButtonAction (baURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed), và cung cấp hai phương thức tạo nút và gắn hành động của nó trong một lệnh gọi duy nhất
// Mở trang trợ giúp trong trình duyệt hệ thống
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);
// Chạy JavaScript phía trình xem
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);
// Gửi dưới dạng XFDF và giữ các trường trống trong payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
'https://api.example.com/claims', Rect(320, 620, 420, 650),
[sffXFDF, sffIncludeNoValueFields]);
Các cờ submit xứng đáng được cân nhắc kỹ hơn mức thường thấy. AddPushButtonWithSubmitAction nhận một tập THPDFSubmitFormFlags, và một tập rỗng tạo ra một bài gửi mã hóa url thuần túy (url-encoded post), đây là định dạng mà nhiều endpoint mẫu chấp nhận và nhiều endpoint sản xuất từ chối. Thêm sffXFDF sẽ chuyển payload sang XFDF. sffGetMethod thay đổi động từ HTTP. sffIncludeNoValueFields giữ các trường trống trong payload thay vì âm thầm loại bỏ chúng, điều này quan trọng ngay khi bên nhận cần phân biệt "vắng mặt" với "để trống". Tập cờ này là một phần trong hợp đồng giao diện của bạn với endpoint nhận dữ liệu, nên hãy thống nhất nó với đội ngũ phân tích cú pháp bài gửi, chứ không phải sau khi lô đầu tiên bị từ chối
JavaScript cấp trường: gõ phím, định dạng, xác thực
Nhấp nút không phải là nơi duy nhất chứa hành động. HotPDF cũng gắn JavaScript vào các sự kiện cấp trường mà các trình xem có khả năng chạy script kích hoạt trong khi người dùng đang nhập dữ liệu. Có ba loại kích hoạt, và chúng xảy ra ở các thời điểm khác nhau trong vòng đời nhập liệu. Một hành động keystroke chạy khi mỗi ký tự đến, và lại chạy khi commit. Một hành động format viết lại giá trị hiển thị sau khi một thay đổi đã được commit, thuần túy vì mục đích trình bày. Một hành động validate có tiếng nói cuối cùng, chấp nhận hoặc từ chối giá trị đã commit trước khi nó trở thành giá trị của trường
// Từ chối các giá trị đã commit không phải là địa chỉ email hợp lệ
Pdf.AttachFieldKeyStrokeAction('applicant.email',
'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');
// Hiển thị số điện thoại Mỹ theo dạng (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');
// Từ chối người đăng ký dưới 18 tuổi tại thời điểm commit
Pdf.AttachFieldValidateAction('applicant.age',
'if (parseInt(event.value) < 18) event.rc = false;');
Đặt event.rc = false bên trong một script keystroke hoặc validate báo cho trình xem từ chối dữ liệu nhập vào. Vấn đề là không có điều nào trong số này chạy trừ khi trình xem có sẵn một engine JavaScript. Acrobat và một vài sản phẩm desktop có engine đó. Hầu hết các trình đọc di động, trình kết xuất nhúng trong trình duyệt, và các pipeline in ấn thì không, và chúng âm thầm bỏ qua các script mà không báo lỗi. Vì vậy script cấp trường chỉ cải thiện chất lượng dữ liệu cho tập con người dùng có trình đọc chạy được chúng, và đó là tất cả những gì chúng làm. Chúng không phải là một ranh giới bảo mật. Mọi giá trị được gửi lên vẫn phải được xác thực trên máy chủ ngay khi nó đến, vì bạn không thể giả định rằng client đã kiểm tra bất cứ điều gì
Các lỗi vượt qua được kiểm tra trực quan
Những lỗi AcroForm khó phát hiện nhất là những lỗi nằm trong cấu trúc dữ liệu chứ không phải trong phần hiển thị, vì mở tệp lên và nhìn vào nó không cho bạn biết điều gì cả. Có bốn lỗi xuất hiện đủ thường xuyên để đáng được nêu tên, và mỗi lỗi có một phép kiểm tra máy móc phát hiện ra nó trước khi phát hành
- Trôi giá trị xuất (export value drift). Một checkbox được tạo bằng
AddCheckBox('consent', 'Yes', ...)gửi lênYes. Một bên nhận khớp theoYsẽ từ chối mọi bài gửi trong khi trang trông vẫn hoàn hảo. Điền vào biểu mẫu, xuất nó dưới dạng XFDF từ Acrobat, và so sánh các giá trị với schema mà bên nhận thực sự mong đợi - Trộn giá trị ngoài ý muốn. Hai trường chia sẻ cùng một tên đầy đủ sẽ hợp nhất thành một. Triệu chứng xuất hiện tại thời điểm nhập dữ liệu chứ không phải tại thời điểm tạo biểu mẫu, nên phép kiểm tra là gõ vào biểu mẫu, chứ không phải kết xuất nó ra rồi nhìn bằng mắt
- Giá trị combo nằm ngoài danh sách tùy chọn. Khi giá trị hiện tại truyền cho
AddComboBoxkhông phải là một trong các tùy chọn được liệt kê, các trình xem bất đồng về việc có nên hiển thị nó, để trống nó, hay đánh dấu nó. Giữ giá trị mặc định nằm trong danh sách và sự bất đồng đó sẽ biến mất - Các trường vẫn có thể chỉnh sửa sau khi quy trình đã đóng. HotPDF không có lệnh gọi làm phẳng hình thức hiển thị (appearance-flattening) cho các trường AcroForm. Cách được hỗ trợ để khóa cứng một biểu mẫu đã hoàn tất là tạo các trường với cờ
ffReadOnly, giữ cho giá trị vẫn hiển thị thông qua luồng hình thức hiển thị (appearance stream) riêng của trường trong khi từ chối chỉnh sửa. Trường vẫn là một đối tượng biểu mẫu sống, đây chính là điều mà các công cụ lắp ráp và ký tên ở hạ nguồn mong đợi tìm thấy
Có một hành vi phía trình xem đáng ghi một chú ý hồi quy dù không có thay đổi mã nào giải quyết được nó. Các triển khai Acrobat doanh nghiệp có thể vô hiệu hóa JavaScript hoặc hạn chế các đích submit theo chính sách, nên một hành động từng hoạt động qua mọi bản build phát triển có thể nằm chết trên một máy tính khách hàng bị khóa chặt. Hãy lên kế hoạch cho một phương án dự phòng hiển thị được cho trường hợp nút không làm gì cả, ngay cả khi phương án dự phòng đó chỉ là một hướng dẫn in sẵn cho người dùng biết phải làm gì thay thế
Nơi công việc biểu mẫu kết nối với phần còn lại của tài liệu
Bản thân trường chữ ký (signature field) là một loại trường AcroForm. Một biểu mẫu sẽ được chứng nhận hoặc đồng ký sau này thì tốt hơn nên dành sẵn trường đó trong quá trình tạo thay vì vá nó vào sau, và các lý do ở cấp độ byte cho điều đó nằm trong bài viết đồng hành về chữ ký số và ký PAdES với HotPDF. Dữ liệu đầu vào đến dưới dạng gói XFA thay vì AcroForm gốc là một tình huống khác: làm phẳng XFA thành các trường AcroForm là một quy trình riêng với mô hình mất mát riêng, vì hai công nghệ biểu mẫu này không thể cùng tồn tại trong một tệp
Các phương thức trường, hành động và trình kích hoạt được trình bày ở đây là một phần của API tiêu chuẩn HotPDF Delphi Component cho Delphi và C++Builder; trang sản phẩm liên kết đến tài liệu tham khảo đầy đủ, bao gồm cả các overload của cờ trường và toàn bộ enum cờ submit