您有一個第三方的發票範本,或是某人多年前用現在大家都找不到的軟體所產生的合約封存檔,而需求是要讓它具備互動性:在角落放上一個簽章方塊,加入幾個文字欄位,並將扁平的檢查清單變成真正的核取方塊。這裡的難處在於,您並非從頭開始創作這份 PDF。它已經存在,它已經擁有您無法控制的頁面、內容串流與字型,而您需要將 AcroForm 小工具 (widget) 嫁接到該物件圖上,且不能重新建置它。這與在一份全新文件上建立表單是不同的問題,而讓人跌跤的部分在您於檢視器中打開結果之前是看不見的,那時您會發現您剛寫入的欄位在頁面上根本找不到
HotPDF 是一個適用於 Delphi 與 C++Builder 的原生 VCL PDF 元件,從 v2.247.0 開始,它公開了一個專用的方法家族,正是為了此事而生:直接在透過 LoadFromFile 載入的文件上建置所有六種標準欄位類型。本文將帶您了解這些方法的作用、它們所建構的 ISO 32000-1 字典,以及那個要是沒有它,整個操作就會無聲無息地產出一個看起來是空白檔案的旗標
為何載入文件的欄位建立有其專屬的程式碼路徑
當您從無到有建置一份 PDF 時,HotPDF 擁有整個物件模型。每個頁面都是一個可寫入的 THPDFPage 包裝器 (wrapper),而透過 AddTextField 加入一個文字欄位,會將新的小工具連接到頁面的註解物件、頁面物件以及表單的欄位集合中,然後從文件的字型資源產生一個外觀串流 (appearance stream)。外觀串流是小工具可見的表面,包含方塊、邊框以及任何預設文字,以 PDF 繪圖運算子的形式繪製,檢視器會原封不動地渲染它
已載入的文件不會給您那些鷹架。頁面是以原始字典的形式進來的;沒有可寫入的 THPDFPage 包裝器可以掛載小工具,更重要的是,沒有字型資源管線在一旁待命來繪製外觀串流。因此,已載入的路徑採取了不同的路線。它將欄位字典直接寫入已解析的物件圖中,並以從零開始的索引 (zero-based index) 而不是透過頁面物件來定址頁面。欄位類型和旗標位元與從頭建立的路徑完全吻合,所以不管是哪種方式,文字欄位終究是文字欄位;改變的是底層的水管配置,而最關鍵的,是小工具的表面是如何被畫出來的
在這裡 /NeedAppearances 旗標不是選用的
這是決定您的心血是否會顯示出來的單一事實。因為已載入的路徑不會產生外觀串流,所以一個剛加入的小工具抵達檢視器時是沒有 /AP 條目的:這是一個沒有描述表面的欄位。許多檢視器在被要求渲染一個沒有外觀也沒有指令去建立外觀的小工具時,會什麼都不畫。該欄位存在於檔案中,結構上是有效的,能被表單填寫工具定址,但在人類眼中卻完全隱形
逃生門定義在 ISO 32000-1 §12.7.3:AcroForm 字典帶有一個 /NeedAppearances 布林值,而當它為 true 時,一個符合規範的閱讀器便被要求要自己從每個欄位的 /DA(預設外觀)字串與數值中,建構出缺失的外觀串流。HotPDF 會為您設定這個值。當您第一次將任何欄位加入到已載入的文件時,EnsureLoadedAcroForm 就會執行:如果目錄沒有 /AcroForm,它會建立一個;如果沒有 /Fields 陣列,它會建立它;並且它會強制設定 /NeedAppearances true。您不會直接呼叫它,但知道它的存在解釋了這種行為。這也解釋了一個值得直說的部署注意事項:少數極簡或不合規的檢視器會忽略 /NeedAppearances 並且仍然什麼都不渲染。對於主流的閱讀器來說,這個旗標能發揮它的作用,但如果您的受眾執行的是不尋常的嵌入式渲染器,請在您做出任何承諾之前先在那裡進行測試
加入六種欄位類型
每個方法都遵循相同的形狀。您傳入從零開始的頁面索引、小工具矩形在 PDF 使用者空間座標中的四個角落、欄位名稱,以及該類型需要的任何額外引數。該矩形是 X1, Y1, X2, Y2,且 PDF 原點位於頁面的左下角,因此較大的 Y 值會坐落在較高的位置;這是來自檔案格式的座標慣例,而不是左上角的螢幕慣例,把它弄反是僅次於忘記旗標之後最常見的錯誤。每次呼叫都會傳回新欄位從零開始的索引,如果頁面索引超出範圍或無法解析頁面物件,則傳回 -1
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;
文字欄位的第三和第四個字串引數是欄位名稱及其初始的 /V 值;整數是 /MaxLen,只有在大於零時才會寫入。HotPDF 給予每個可編輯欄位一個預設外觀字串 /Helv 12 Tf 0 0 0 rg,這正是尊重 /NeedAppearances 的檢視器用來決定它繪製數值時的字型和顏色的依據。核取方塊接受一個匯出值 (export value),即勾選該方塊時表單所送出的字串,加上一個代表初始狀態的布林值;在內部它會寫入相符的 /V、/AS 和 /DV 名稱條目,使得開/關狀態在檔案開啟的那一刻就是一致的。空匯出值預設為 Yes,這是核取方塊傳統的「開啟」名稱
選擇欄位與 /Ff 位元旗標
下拉式方塊 (ComboBox) 和清單方塊 (ListBox) 都是選擇欄位,在 ISO 32000-1 §12.7.4 中的欄位類型為 /Ch。下拉式選單與捲動清單之間的差異,在於欄位旗標整數 /Ff 中的一個位元:位元 18,也就是 Combo 旗標,值為 $40000。HotPDF 會為 AddLoadedComboBox 設定該位元,並為 AddLoadedListBox 保持清除狀態;除此以外,兩者是相同的,且都會將其選項作為一個字串的開放陣列 (open array) 寫入到 /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');
關於選項清單有兩點注意事項。HotPDF 將每個 /Opt 條目寫成純字串,其中匯出值與顯示的標籤是相同的文字。當您需要送出的值與使用者閱讀的值不同時,ISO 32000-1 §12.7.4.4 也允許使用雙元素 [export display] 的形式;已載入的建立方法使用較簡單的單一字串形式,所以如果您需要匯出值與顯示值分歧,您必須自己去設定產生出來的字典。而且您作為欄位目前選擇所傳入的值,應該要是您所提供的選項之一,因為檢視器會將其與清單進行比對
按鈕 (Push button) 是另一個由旗標驅動的案例:欄位類型為 /Btn 加上位元 17,即 PushButton 旗標,值為 $10000。正是這個位元將可點擊的按鈕與核取方塊區分開來,後者也是 /Btn 欄位但沒有該位元。您傳入的標題會被寫入外觀特徵字典 /MK 中,作為一般標題 /CA。在這裡誠實說明範圍是值得的:按鈕被建立出來時帶有它的標籤和矩形,但已載入的建立方法並未附加動作,所以就它本身而言,它是一個看起來正確,但點擊時毫無反應的按鈕。串接送出、重設或 JavaScript 動作是另一個獨立的關注點;至於從頭開始的創作端,欄位加動作的工作流程在在 Delphi 中建置 AcroForm 欄位與動作中有所涵蓋,這是了解已載入路徑刻意遺漏了什麼的正確比較點
每個欄位共用的字典
在這六個方法底層是一個共用的建構器,它負責建構小工具註解並將其註冊在兩個地方。它寫入 /Type /Annot 和 /Subtype /Widget,由您四個座標組成的 /Rect 陣列,設定了 Print 位元好讓欄位能出現在紙本以及螢幕上的註解旗標 /F 4,欄位名稱 /T,欄位類型 /FT,旗標 /Ff,以及一個指向頁面物件的向後參照 /P。接著它將新欄位附加到 AcroForm 的 /Fields 陣列以及該頁面的 /Annots 陣列中,在此過程中解析間接參照,所以它是延伸真正的陣列,而不是讓小工具成為孤兒
這種雙重註冊之所以重要,是因為一個只存在於這兩個清單其中之一的小工具,會在一種微妙的方面被破壞。一個存在於 /Fields 中卻在頁面的 /Annots 裡缺席的欄位,表單認識它但永遠不會被畫出來;反過來則是畫出來了,但表單邏輯卻不認識它。HotPDF 在每次加入時都會讓兩者保持同步,像這種簿記工作,否則您就得對著規格書小心翼翼地手動完成
一些誠實的限制
在您基於此建置工作流程之前,請先設定好期望。展平與重新產生 (flatten-and-regenerate) 的行為取決於檢視器是否尊重 /NeedAppearances,這涵蓋了 Acrobat、現代瀏覽器的 PDF 引擎以及常見的桌面閱讀器,但這並非對野外每一個渲染器的硬性保證。如果您必須產生一份其欄位在任何地方渲染出來都完全一樣的檔案,包括在那些會忽略該旗標的檢視器中,那麼您就進入了外觀串流的領域,而那個會為您繪製 /AP、從頭開始創作的路徑將是更合適的選擇。同樣地,簽章欄位被建立出來時只是一個準備好被簽署的空簽章小工具;放置該欄位與套用密碼學簽章並不相同
對於更改已經存在的東西,而不是在上面做加法,相關的操作是表單展平 (form flattening),您會將互動式欄位烘焙回靜態頁面內容中,讓數值變成永久且不可編輯;這個來回的過程,包括如何處理帶有 XFA 的表單,在在 Delphi 中展平 XFA 與 AcroForm 欄位中有探討。加入欄位和展平欄位是同一個生命週期的兩端:本文是關於您如何讓一份原本缺乏互動性的文件獲得它,而展平則是關於當表單達成其目的後,您如何把它拿掉
這裡展示之用於已載入文件的表單 API,是作為適用於 Delphi 和 C++Builder 之標準 HotPDF 元件的一部分提供,與欄位旗標、外觀處理以及 AcroForm 模型的其餘完整參考資料一併隨附