PDFlibPas, thư viện PDF Delphi và C++Builder gốc, cho một tài liệu PDF hai nơi riêng biệt để treo hành vi tự động: các hành động vòng đời cấp tài liệu như WillClose, WillSave, DidSave, WillPrint và DidPrint, lưu trong từ điển /AA của Catalog, và các hành động vòng đời cấp trang — Open và Close — lưu trong từ điển /AA riêng của mỗi đối tượng Page thay vào đó. Nhầm lẫn hai container này là cách phổ biến nhất khiến một hành động vòng đời âm thầm không làm gì cả
Các trường hợp thúc đẩy điều này rất thông thường. Một đội tài chính muốn một mẫu sao kê đóng dấu thời gian in và ghi log ai đã in nó ngay khi việc in thực sự bắt đầu, không phải khi file chỉ đơn thuần mở. Một luồng công việc nặng biểu mẫu cần các giá trị trường được đẩy đến một server tự động trước khi client PDF của trình đọc được phép đóng cửa sổ, để một tab đã đóng không bao giờ có nghĩa là một lần chỉnh sửa bị mất. Một báo cáo nhiều trang muốn một banner đặc thù cho trang chỉ xuất hiện khi trang đó đang trên màn hình. PDF thực sự cung cấp một tầng thứ ba dưới tài liệu và trang cho loại hành vi này — các hành động gắn với entry /A riêng của một trường biểu mẫu hay liên kết đơn lẻ, chủ đề của một bài viết đồng hành về hành động biểu mẫu tương tác và JavaScript — nhưng bài viết này ở lại với hai tầng phía trên nó: toàn bộ tài liệu, và một trang đơn lẻ
Những trigger nào sống trên /AA của Catalog tài liệu?
Năm trigger sống trên từ điển /AA của Catalog, và mỗi cái trong số đó phát ra cho một sự kiện ảnh hưởng đến toàn bộ tài liệu, không phải một trang đơn lẻ. ISO 32000-1 §12.6.3 (Trigger Events) liệt kê các khóa cấp tài liệu là WC, WS, DS, WP và DP — các tên hai-chữ-cái chữ nghĩa được viết vào từ điển /AA — cho WillClose, WillSave, DidSave, WillPrint và DidPrint tương ứng, và PDFlibPas phản chiếu chính xác tập hợp đó trong liệt kê TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction là điểm vào duy nhất gắn bất kỳ cái nào trong năm cái, và tham số ActionKind nó nhận là một trong mười hằng số PDF_ACTION_BUILDER_* dùng chung qua mọi lệnh gọi bộ dựng hành động trong thư viện, từ một URI thuần túy đến một script đến một bước nhảy đích. Những gì một hành động GoTo, file ngoài, file nhúng, hay Launch thực sự làm một khi được kích hoạt là một câu hỏi khác với việc nó được gắn ở đâu, và đó là chủ đề của một bài viết đồng hành về hành động GoTo, file ngoài, file nhúng, và launch — bài viết này ở lại với câu hỏi container, Catalog hay Page, thay vì câu hỏi loại-hành-động
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.AddStandardFont(4);
Lib.DrawText(40, 700, 'Quarterly statement');
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save', '', 0, 0);
Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
Lib.SaveToFile('statement.pdf');
finally
Lib.Free;
end;
end;
Một trigger cấp trang khác với một trigger cấp tài liệu như thế nào?
Một trigger cấp trang chỉ phát ra cho đúng một đối tượng Page nó được gắn vào, và PDFlibPas lưu nó trong từ điển /AA riêng của trang đó thay vì của Catalog. Chỉ có hai trigger trang, Open và Close, tương ứng với các khóa O và C mà ISO 32000-1 định nghĩa cho từ điển hành động bổ sung của một trang, và PDFlibPas phơi bày chúng như patOpen và patClose thông qua SetPageAction, gắn vào bất cứ trang nào hiện đang được chọn qua SelectPage — một chi tiết quan trọng lần đầu tiên bạn lặp qua một tài liệu mong đợi một lệnh gọi áp dụng ở khắp mọi nơi, vì nó không bao giờ làm vậy. Việc gắn bất kỳ loại trigger nào cũng nâng phiên bản PDF tối thiểu của file, và hai container yêu cầu các sàn khác nhau: PDFlibPas nâng tài liệu lên ít nhất PDF 1.4 lần đầu tiên nó ghi một mục Catalog /AA, và lên ít nhất PDF 1.5 lần đầu tiên nó ghi một mục Page /AA, bất kể loại hành động nào nằm bên trong. Đó là một yêu cầu cấp container xếp lớp lên trên bất cứ thứ gì bản thân hành động cần riêng, nên một hành động URI thuần túy tự thân chỉ cần PDF 1.1 vẫn kéo toàn bộ file lên PDF 1.5 một khi nó được bọc trong một trigger mở-trang
Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
'https://example.com/analytics/page-3-closed', '', 0, 0);
Đọc và xóa các hành động vòng đời
GetDocumentActionInfo và GetPageActionInfo đều trả về một bản ghi TPDFlibActionInfo, và trường Kind trả về akNone bất cứ khi nào trigger đó không có gì gắn kèm, nên hãy kiểm tra Kind trước khi tin tưởng bất kỳ trường nào khác trên bản ghi — URI, JavaScript, FileName và phần còn lại chỉ có ý nghĩa cho đúng một loại hành động mà Kind thực sự báo cáo, vì cùng hình dạng bản ghi được tái sử dụng qua mọi loại hành động bộ dựng có thể tạo ra. RemoveDocumentAction và RemovePageAction mỗi cái xóa một trigger duy nhất và báo cáo 1 khi chúng tìm thấy thứ gì đó để xóa, 0 khi trigger đã trống sẵn; khi mục đã xóa là mục cuối cùng còn lại trong từ điển /AA, PDFlibPas xóa chính /AA giờ đã trống thay vì để lại một container lơ lửng, vô nghĩa trên Catalog hay trang
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetDocumentActionInfo(datWillSave);
if Info.Kind = akURI then
WriteLn('WillSave calls out to: ', string(Info.URI));
if Lib.RemoveDocumentAction(datWillSave) = 1 then
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save-v2', '', 0, 0);
end;
PDF/A có hoàn toàn cho phép hành động vòng đời không?
Không. Tuân thủ PDF/A từ chối toàn bộ container hành động bổ sung, không chỉ các loại hành động nghe có vẻ rủi ro, vì ISO 19005 giới hạn mô hình hành động tương tác của PDF dựa trên giả định rằng một file lưu trữ phải render giống hệt hàng chục năm sau, không phụ thuộc vào một engine script hay một kết nối mạng có thể không còn tồn tại vào lúc đó. SetLifecycleAction, bộ dựng dùng chung đứng sau cả SetDocumentAction lẫn SetPageAction, kiểm tra PDFAMode trước cả khi nó nhìn vào ActionKind, nên một hành động URI chỉ mở một trang web công ty hay một hành động Named chỉ có nghĩa là đi đến trang tiếp theo bị bắt trong cùng tấm lưới với một hành động nguy hiểm — không có gì mà một người review bảo mật bình thường sẽ gắn cờ, dù sao cũng bị chặn, vì giới hạn mang tính cấu trúc thay vì theo-từng-trường-hợp. Nguy hiểm thực tế là sự từ chối này âm thầm: cả SetDocumentAction lẫn SetPageAction đều trả về 0 mà không ném ra ngoại lệ, nên một điểm gọi không bao giờ kiểm tra giá trị trả về phát hành một tài liệu âm thầm thiếu trigger nó lẽ ra phải mang
Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
'', '', 0, 0) = 0 then
// rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
WriteLn('lifecycle action not attached');
Có một sự bất đối xứng đáng ghi nhớ. RemoveDocumentAction và RemovePageAction không bao giờ kiểm tra PDFAMode, nên việc nạp một file đã mang các hành động vòng đời không tuân thủ chuẩn và loại bỏ chúng trên đường đến một lượt lưu tuân thủ PDF/A hoạt động đúng như mong đợi — chỉ đường ghi, gắn một trigger mới, mới bị cổng theo chế độ tuân thủ
In-khi-mở khớp vào đâu mà không có trigger WillOpen?
Từ điển /AA của Catalog hoàn toàn không có mục WillOpen nào, theo thiết kế — /AA cấp tài liệu trong ISO 32000-1 định nghĩa đúng năm khóa, WillClose, WillSave, DidSave, WillPrint và DidPrint, và không gì trong danh sách đó phát ra thuần túy vì một file đã được mở. Hook thời-điểm-mở sống trong một mục Catalog riêng biệt, /OpenAction, mà PDFlibPas phơi bày thông qua họ lệnh gọi riêng của nó, SetOpenActionJavaScript, SetOpenActionDestination và SetOpenActionNamedDestination trong số đó, không cái nào đụng đến từ điển /AA hay liệt kê TPDFlibDocumentActionTrigger cả. Tuy nhiên, hai cơ chế này kết hợp được, và đó thường là những gì một mẫu in-khi-mở thực sự cần: xây dựng mẫu để /OpenAction của nó khởi động job in, thường là một hành động JavaScript gọi lệnh in riêng của trình xem, và chính việc in là điều cho WillPrint và DidPrint thứ gì đó để chạy trên đó — một dấu thời gian đóng vào trước khi các trang spool, một mục kiểm toán viết ra một khi chúng hoàn tất
Các trigger này đáng tin cậy đến đâu qua các trình xem PDF?
Không phải mọi trình xem đều chạy chúng, kể cả ngoài PDF/A, nên hãy coi một hành động vòng đời như một yêu cầu chứ không phải một cam kết. Acrobat và hầu hết các trình đọc desktop đầy đủ thực thi toàn bộ tập hợp trung thực, nhưng một phần lớn việc tiêu thụ PDF thực tế hoàn toàn không bao giờ đụng đến một từ điển hành động bổ sung: các trình xem nhúng trong trình duyệt, hầu hết trình đọc di động, và gần như mọi pipeline render phía server hay trích xuất văn bản hoặc hoàn toàn bỏ qua /AA hoặc chỉ tôn trọng một lát mỏng của nó, với WillPrint và DidPrint thường hoạt động tệ nhất vì chuyển đổi headless không có thao tác in nào cho chúng móc vào. Nếu một hành động submit-form WillClose là đường duy nhất bắt được dữ liệu biểu mẫu, đó không phải một đường đáng tin cậy — ghép nó với một nút submit tường minh, và coi trigger tự động như một tiện ích cho các trình đọc tình cờ hỗ trợ nó
Các trigger tài liệu, trang, và trường là ba tầng của cùng cỗ máy từ điển hành động bên dưới, và một khi container đã rõ ràng, phần còn lại là chọn đúng hằng số ActionKind và kiểm tra mã trả về. Các trigger vòng đời này, cùng với API bộ dựng hành động rộng hơn mà bài viết này chạm tới, đi kèm sẵn trong thư viện PDF Delphi PDFlibPas tiêu chuẩn, với tài liệu tham khảo trigger và loại-hành-động đầy đủ trong tài liệu sản phẩm