PDF 表單欄位本身只是一個保存值的框。使表單運作起來像個小型應用程式的是附加在其上的動作:按一下以隱藏某個區段、從檔案中取回儲存的值、跳轉到最後一頁,或執行計算總和的指令碼。這些都不存在於欄位中,而是存在於動作字典中,ISO 32000-1 在 §12.6 中組織了整個系列。本文將介紹 Delphi 程式最常使用的動作,並展示 PDFlibPas 如何將每個動作連接到欄位或連結。
值得記住的心智模型是,欄位和動作是由參照連接的獨立物件。小工具註解或連結註解在它的 /A 項目中攜帶動作。動作透過標題而非索引來指定其操作的欄位,因此您為欄位指定的標題是每個後續動作在尋找它時所使用的控制代碼。一旦這種劃分清晰,API 就不再看起來像是一堆雜亂之輪的呼叫,而是開始看起來像是應用於四種動詞的一種模式。
命名動作:無需頁碼的導覽
最簡單的動作完全不帶參數。ISO 32000-1 §12.6.4.11 的表 194 定義了命名動作:檢視器在執行階段直譯符號名稱,而不是跟隨儲存的目標。有四個名稱受到普遍支援,它們正是讀者期望從工具列看到的:NextPage、PrevPage、FirstPage 和 LastPage。因為目標相對於檢視器目前顯示的任何頁面,所以以此方式建立的「下一步」按鈕可在每個頁面上運作,而無需您計算目標。
在 PDFlibPas 中,命名動作附加到目前頁面上的熱點矩形。第四和第五個整數引數選擇動詞和外觀。
// 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
沒有需要保持同步的目標,這正是關鍵所在。命名動作在頁面插入和刪除後仍能保留,因為它從一開始就沒有指定頁面。與明確的跳轉連結相比,後者儲存了目標頁面索引,一旦檔案成長,您就必須重新編號該索引。
隱藏動作及其陣列陷阱
隱藏動作(ISO 32000-1 §12.6.4.10,表 196)可切換一個或多個欄位的內建可見性。這是在不使用指令碼的情況下建立顯示和隱藏行為的最乾淨方法,也是您在「顯示詳細資料」連結或兩個互斥面板(顯示一個會隱藏另一個)中所需的行為。該動作在其 /T 項目中攜帶目標,並在布林值 /H 中決定方向:為 true 時隱藏,為 false 時顯示。
其中的微妙之處完全在於該目標是如何編碼的,這種細節會導致表單在您的電腦上運作正常,但在客戶的電腦上卻失敗。當動作指定單個欄位時,/T 會寫入為單個文字字串。當它指定多個欄位時,/T 會寫入為文字字串陣列。較舊的檢視器處理單元素陣列的方式與處理單個字串的方式不同,因此編碼必須根據數量進行分支:如果要想讓最廣泛的閱讀器都支援它,單個名稱必須以字串形式發送,而不是長度為一的陣列。PDFlibPas 會為您做出決定。您傳遞以逗號、分號或換行符號分隔的欄位名稱,寫入器會針對一個名稱發送單個字串,針對兩個或多個名稱發送陣列。
// 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);
因為該動作不參照外部資源,所以它與 PDF/A 保持相容。您傳遞的名稱是完整限定的欄位標題,這就是為什麼群組內的子欄位必須透過其完整的點號路徑來定址,而不是僅靠其葉節點名稱。
ImportData:從 FDF 預填資料
隱藏動作重排了頁面上已有的內容,而匯入資料動作則從頁面外部引入值。ISO 32000-1 §12.6.4.8 的表 198 將其定義為從磁碟上的表單資料格式 (FDF) 檔案填入 AcroForm 的動作。這是「重新載入範例資料」或「重設為預設值」控制項背後的動作,其中 FDF 檔案與 PDF 一起出貨並保存規範欄位值。該呼叫與其他呼叫類似,接受熱點矩形、FDF 的路徑和外觀位元遮罩:Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1)。建立 PDF時檔案不需要存在,但在使用者按一下時必須存在,並且路徑中的任何反斜線都會為您重寫為 PDF 標準斜線格式。
有一個限制值得明確說明,因為這常常令人感到意外。匯入資料動作指向外部檔案,因此在 PDF/A 中是不允許的。當檔案處於 PDF/A 模式時,該呼叫會傳回零且不新增任何內容,而不是產生一個無法通過驗證的檔案。如果您的管線目標是封存輸出,則預填動作必須在產生時透過直接寫入欄位值來完成,而不是延遲到按一下時。
JavaScript:全域套件與單一動作指令碼
對於超出顯示、隱藏和匯入範圍的邏輯,動作系列會延伸到檔案級 JavaScript。指令碼可以存在於兩個不同的地方,這兩者之間的差異很重要。檔案級 JavaScript 套件針對整個檔案只儲存一次,並在檔案開啟時執行,這使其成為函式定義和共享狀態的理想歸宿。單一動作指令碼附加到單個連結或欄位,且僅在該物件被啟用時執行,這使其成為呼叫套件已定義函式的單行程式碼的理想位置。
PDFlibPas 兩者都公開。AddGlobalJavaScript 在檔案級儲存具名套件;重複使用名稱會替換儲存在其下的內容。AddLinkToJavaScript 將指令碼附加到熱點,以便按一下時執行它。
// 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);
將函式保留在全域套件中並將呼叫保留在連結中並非風格偏好。它避免了在每個需要的控制項上重複相同的程式體,並且意味著停用指令碼功能的檢視器在按一下時只會什麼都不做,而不是因格式錯誤的內嵌二進位大型物件 (blob) 而卡住。它還使單一動作項目保持較小,從而在您稍後檢查檔案時保持其可讀性。
欄位、子欄位與凍結結果
動作需要操作欄位,因此了解欄位如何產生會有所幫助。NewFormField 在目前頁面上建立一個欄位並傳回其索引;整數類型選擇種類,其中 1 代表文字,2 代表按鈕,3 代表核取方塊,4 代表選項按鈕,5 代表選擇,6 代表簽名,7 代表擁有子欄位但自身不繪製任何內容的父欄位。您傳遞的標題不能包含點號,因為點號是動作在定址子欄位時所使用的完整限定名稱中的分隔符號。
選項群組和階層式表單是透過為父欄位提供子欄位來建立的。NewChildFormField 在具名父欄位下新增子欄位,而對於選項和選擇的情況,AddFormFieldSub 會新增各個選項並傳回一個臨時索引,您可以使用該索引來定位每個選項。當互動階段結束且您想要凍結欄位,使其目前的外觀成為永久頁面內容時,FlattenFormField 會將欄位繪製到頁面上並將其從表單中移除。扁平化後,後續欄位的索引會向下移動一格,如果您在迴圈中扁平化多個欄位,這是需要記住的一點。
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;
扁平化呼叫是特意被註解掉的。不使用它,檔案就會作為動態表單出貨,其動作在閱讀器中觸發。啟用它,欄位就會被演算為靜態標記,當表單已填寫完畢且結果應作為固定記錄傳輸時,這正是您所需要的。同一個欄位,相同的程式碼,根據您是否凍結它,會產生兩個非常不同的檔案。
選擇合適的動詞
這四個動作根據它們所觸及的對象進行了清晰的劃分。命名動作移動視埠且不需要欄位。隱藏動作變更可見性並需要欄位標題,且字串與陣列的編碼會為您處理好。匯入資料動作會存取磁碟上的檔案,因此在 PDF/A 中是禁止的。JavaScript 動作執行任意邏輯,最好分割為全域函式套件與小型單一動作呼叫。使用能完成工作最簡單的動作:隱藏動作比設定隱藏旗標的指令碼更具可移植性,而命名動作比儲存的頁面目標更耐用,因為沒有需要維護的數字。
從這裡開始,兩個鄰近的主題完善了整個畫面。如果表單是無障礙檔案的一部分,螢幕閱讀器讀取的結構樹會在我們關於標籤化 PDF 與無障礙結構的文章中介紹。當填寫完畢的表單必須鎖定並簽名時,工作流程會在相容性與簽名工作台逐步解說中描述。這三者都建立在同一個引擎之上,該引擎作為 Delphi PDF 函式庫 出貨,同時也提供本部落格其他地方介紹的建立、表單和簽名 API。