Bài viết Kỹ thuật

Thêm các trường AcroForm vào PDF đã tải trong Delphi

Bạn có một mẫu hóa đơn từ bên thứ ba, hoặc một hợp đồng lưu trữ mà ai đó đã tạo từ nhiều năm trước bằng phần mềm giờ không còn ai tìm được nữa, và yêu cầu là biến nó thành tài liệu tương tác: đặt một ô chữ ký ở góc, thêm vài trường văn bản, biến một danh sách kiểm tra phẳng thành các checkbox thực sự. Vấn đề là bạn không đang tạo PDF này từ đầu. Nó đã tồn tại, đã có trang, content stream và font mà bạn không kiểm soát, và bạn cần gắn các widget AcroForm lên object graph đó mà không dựng lại tài liệu. Đó là một bài toán khác hẳn so với tạo form trên một tài liệu mới, và phần dễ làm người ta vấp là điều bạn không nhìn thấy cho đến khi mở kết quả trong trình xem và các trường vừa ghi không xuất hiện ở đâu trên trang

HotPDF là một thành phần PDF VCL gốc cho Delphi và C++Builder, và từ v2.247.0 nó cung cấp một nhóm phương thức chuyên dụng cho đúng việc này: tạo cả sáu kiểu trường tiêu chuẩn trực tiếp trên một tài liệu đã tải bằng LoadFromFile. Bài viết này sẽ đi qua những gì các phương thức đó làm, dictionary ISO 32000-1 mà chúng tạo ra, và lá cờ duy nhất nếu thiếu thì toàn bộ thao tác sẽ âm thầm tạo ra một tệp trông như rỗng

Vì sao việc tạo trường trên tài liệu đã tải lại là một nhánh riêng

Khi bạn dựng PDF từ đầu, HotPDF sở hữu toàn bộ object model. Mỗi trang là một wrapper THPDFPage có thể ghi, và việc thêm một trường văn bản qua AddTextField sẽ nối widget mới vào annotation object của trang, page object của trang và danh sách trường của form, rồi tạo appearance stream từ tài nguyên font của tài liệu. Appearance stream là bề mặt hiển thị của widget, gồm khung, viền và mọi văn bản mặc định, được vẽ bằng các toán tử PDF drawing mà trình xem sẽ hiển thị nguyên trạng

Tài liệu đã tải không mang lại cho bạn bất kỳ lớp chống đỡ nào như vậy. Các trang đi vào dưới dạng dictionary thô; không có wrapper THPDFPage có thể ghi để treo widget lên, và quan trọng hơn là không có pipeline tài nguyên font sẵn sàng để vẽ appearance stream. Vì thế nhánh loaded đi theo một đường khác. Nó ghi các dictionary của trường trực tiếp lên object graph đã được parse và tham chiếu trang bằng chỉ số bắt đầu từ 0 thay vì bằng page object. Kiểu trường và các bit cờ vẫn khớp chính xác với nhánh tạo từ đầu, nên trường Text vẫn là trường Text theo cả hai cách; điều thay đổi là phần plumbing bên dưới và, quan trọng nhất, cách bề mặt của widget được vẽ

Cờ /NeedAppearances không phải là tùy chọn ở đây

Đây là sự thật duy nhất quyết định công việc của bạn có xuất hiện hay không. Vì nhánh loaded không tạo appearance stream, một widget vừa thêm sẽ tới tay trình xem mà không có mục /AP: tức là một field không có bề mặt được mô tả. Nhiều trình xem, khi được yêu cầu hiển thị một widget không có appearance và cũng không có chỉ dẫn để tự tạo một cái, sẽ không vẽ gì cả. Field vẫn có trong tệp, hợp lệ về cấu trúc, có thể được một công cụ điền form truy cập, nhưng lại hoàn toàn vô hình với con người

Lối thoát được định nghĩa trong ISO 32000-1 §12.7.3: AcroForm dictionary mang một boolean /NeedAppearances, và khi nó là true thì một trình đọc tuân thủ phải tự dựng các appearance stream còn thiếu từ chuỗi /DA (default appearance) và giá trị của từng field. HotPDF đặt sẵn điều này cho bạn. Lần đầu tiên bạn thêm bất kỳ field nào vào tài liệu đã tải, EnsureLoadedAcroForm sẽ chạy: nếu catalog chưa có /AcroForm thì nó tạo một cái, nếu chưa có mảng /Fields thì nó tạo luôn, và nó ép /NeedAppearances true. Bạn không gọi nó trực tiếp, nhưng biết nó tồn tại sẽ giải thích được hành vi. Nó cũng làm rõ một lưu ý triển khai đáng nói thẳng: một số trình xem tối giản hoặc không tuân thủ sẽ bỏ qua /NeedAppearances và vẫn không vẽ gì. Với các trình đọc phổ biến thì cờ này làm đúng việc của nó, nhưng nếu đối tượng của bạn dùng một renderer nhúng khác thường, hãy thử trên đó trước khi hứa hẹn bất kỳ điều gì

Thêm sáu kiểu trường

Mỗi phương thức đều theo cùng một khuôn. Bạn truyền vào chỉ số trang bắt đầu từ 0, bốn góc của hình chữ nhật widget theo tọa độ không gian người dùng PDF, tên trường, và mọi đối số bổ sung mà kiểu đó cần. Hình chữ nhật là X1, Y1, X2, Y2 với gốc PDF ở góc trái dưới của trang, nên các giá trị Y lớn hơn sẽ nằm cao hơn; đây là quy ước tọa độ của định dạng tệp, không phải quy ước màn hình từ góc trái trên, và nhầm chỗ này là lỗi phổ biến thứ hai sau khi quên cờ. Mỗi lần gọi trả về chỉ số bắt đầu từ 0 của trường mới, hoặc -1 nếu chỉ số trang ngoài phạm vi hay không thể phân giải được page object

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Tham số chuỗi thứ ba và thứ tư của trường văn bản lần lượt là tên trường và giá trị /V ban đầu; số nguyên là /MaxLen, chỉ được ghi khi lớn hơn 0. HotPDF gán cho mọi trường có thể chỉnh sửa một chuỗi appearance mặc định là /Helv 12 Tf 0 0 0 rg, và đó là thứ một trình xem tôn trọng /NeedAppearances sẽ dùng để quyết định font và màu mà nó vẽ giá trị lên. Checkbox nhận một export value, tức chuỗi form sẽ gửi khi ô được tích, cộng với một giá trị boolean cho trạng thái ban đầu; bên trong, nó ghi các name entry /V, /AS/DV tương ứng để trạng thái on/off nhất quán ngay khi tệp được mở. Nếu export value để trống, mặc định sẽ là Yes, tên on quen thuộc của checkbox

Các trường lựa chọn và các bit cờ /Ff

ComboBox và ListBox đều là trường lựa chọn, kiểu trường /Ch trong ISO 32000-1 §12.7.4. Khác biệt giữa một dropdown và một danh sách cuộn chỉ là một bit trong số nguyên cờ trường /Ff: bit 18, Combo flag, giá trị $40000. HotPDF đặt bit đó cho AddLoadedComboBox và để nó tắt cho AddLoadedListBox; ngoài ra hai cái giống hệt nhau, và cả hai đều nhận các lựa chọn dưới dạng mảng mở các chuỗi được ghi vào mục /Opt

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Hai lưu ý về danh sách lựa chọn. HotPDF ghi từng mục /Opt dưới dạng một chuỗi đơn, trong đó export value và nhãn hiển thị là cùng một văn bản. ISO 32000-1 §12.7.4.4 cũng cho phép dạng hai phần tử [export display] khi bạn cần giá trị được gửi đi khác với thứ người dùng nhìn thấy; các phương thức tạo trên tài liệu đã tải dùng dạng chuỗi đơn đơn giản hơn, nên nếu bạn cần export value và display value khác nhau thì bạn sẽ tự đặt chúng trên dictionary kết quả. Và giá trị bạn truyền làm lựa chọn hiện tại của trường nên là một trong các option đã cung cấp, vì trình xem sẽ đối chiếu nó với danh sách

Nút bấm là trường hợp thứ hai do cờ chi phối: kiểu trường /Btn với bit 17, PushButton flag, giá trị $10000. Bit đó phân biệt một nút có thể bấm với một checkbox, vốn cũng là trường /Btn nhưng không có bit này. Caption bạn truyền sẽ được ghi vào appearance characteristics dictionary /MK dưới dạng normal caption /CA. Cần nói rõ phạm vi ở đây: nút được tạo với nhãn và hình chữ nhật, nhưng phương thức tạo trên tài liệu đã tải không gắn action, nên tự thân nó chỉ là một nút nhìn đúng nhưng bấm vào thì không làm gì. Việc gắn submit, reset hoặc JavaScript action là một vấn đề riêng; ở phía tạo từ đầu, quy trình trường cộng action đã được trình bày trong xây dựng field và action AcroForm trong Delphi, đó là điểm so sánh đúng cho những gì nhánh loaded cố ý không làm

Dictionary mà mọi trường đều chia sẻ

Bên dưới cả sáu phương thức là một builder chung dựng widget annotation và đăng nó ở hai nơi. Nó ghi /Type /Annot/Subtype /Widget, mảng /Rect từ bốn tọa độ của bạn, annotation flags /F 4 để bật bit Print cho trường xuất hiện cả khi in lẫn trên màn hình, tên trường /T, field type /FT, các cờ /Ff, và một tham chiếu ngược /P tới page object. Sau đó nó thêm trường mới vào mảng /Fields của AcroForm và vào mảng /Annots của trang đó, đồng thời phân giải các indirect reference trên đường đi để nó mở rộng đúng các mảng thật thay vì bỏ widget lại không có chủ

Việc đăng ký kép này quan trọng vì một widget chỉ sống trong một trong hai danh sách sẽ hỏng theo cách rất tinh vi. Một trường có trong /Fields nhưng thiếu trong /Annots của trang thì form biết tới nhưng không bao giờ được vẽ; ngược lại thì được vẽ nhưng logic form không biết tới. HotPDF giữ hai phía đồng bộ trong mỗi lần thêm, và đó là kiểu bookkeeping mà nếu không có sẵn bạn sẽ phải tự làm chính xác từng chi tiết theo spec

Vài giới hạn cần nói thẳng

Hãy đặt kỳ vọng trước khi xây một workflow dựa trên điều này. Hành vi flatten rồi tái sinh phụ thuộc vào việc trình xem có tôn trọng /NeedAppearances hay không, điều này bao phủ Acrobat, các engine PDF của trình duyệt hiện đại và những trình đọc desktop phổ biến, nhưng không phải là bảo đảm cứng với mọi renderer ngoài kia. Nếu bạn buộc phải tạo ra một tệp mà các trường hiển thị giống hệt ở mọi nơi, kể cả trong các trình xem bỏ qua cờ này, bạn đang ở vùng appearance stream và nhánh tạo từ đầu có thể tự vẽ /AP cho bạn sẽ là lựa chọn phù hợp hơn. Tương tự, signature field được tạo như một widget chữ ký trống sẵn sàng để ký, còn việc đặt trường không đồng nghĩa với việc áp dụng chữ ký mật mã

Nếu mục tiêu là thay đổi cái đã có thay vì thêm mới, thao tác liên quan là flatten form, tức nhúng các trường tương tác trở lại nội dung trang tĩnh để giá trị trở thành cố định và không thể sửa; toàn bộ vòng đi-về đó, bao gồm cách xử lý các form có XFA, được bàn trong làm phẳng XFA và các trường AcroForm trong Delphi. Thêm trường và làm phẳng trường là hai đầu của cùng vòng đời: bài viết này là cách đưa tính tương tác lên một tài liệu vốn chưa có, còn flatten là cách gỡ nó xuống sau khi form hoàn thành nhiệm vụ

API form trên tài liệu đã tải được trình bày ở đây đi kèm trong gói HotPDF Component tiêu chuẩn cho Delphi và C++Builder, cùng tài liệu tham chiếu đầy đủ cho field flag, xử lý appearance và phần còn lại của mô hình AcroForm