在 Acrobat 中會重新計算總計,但在您自己的 Delphi 檢視器中卻凍結不動的表單,幾乎絕不是轉譯 Bug。除非主機附加了 JS 平台,否則 PDFium 會停用所有文件 JavaScript,而 PDFium 元件從 2.13.2 版本開始會自動連接該平台,因此只要載入了啟用 V8 的 DLL,AcroForm JavaScript、文件級指令碼和欄位計算就會執行。主機透過一組事件保持主導權,這些事件可以顯示、重定向或否決指令碼嘗試執行的所有操作
這句話解決了我們在多個表單中看到的支援問題:「我的訂單表單在 Acrobat 中會計算單行總計,但在我的應用程式中卻不會」、「app.alert 從未觸發」、「日期欄位無法自行格式化」。所有這些都可以追溯到同一個協定,花十分鐘去理解它是值得的,因為同一個協定也是您防範惡意文件的安全邊界
為什麼 PDF 表單計算在 PDFium 中無法運作?
PDFium 將文件 JavaScript 視為嚴格的選擇性啟用(opt-in):如果表單填寫環境的 m_pJsPlatform 成員為 NULL,文件中的每個指令碼都會被默默跳過。沒有錯誤,沒有日誌行,也沒有部分執行。計算欄位會保留其最後儲存的值, app.alert 消失,格式指令碼從不執行。Acrobat 總是隨附其專屬的 JavaScript 引擎,因此能順利執行同一個檔案,這就是為什麼這種差異看起來非常像您程式碼中的 Bug,而實際上它只是底層程式庫刻意的預設行為
PDFium 元件對此曾有第二個歷史性的限制。在 2.13.1 版本之前,繫結僅在 XFA 初始化分支內部附加 m_pJsPlatform,因此即使存在啟用 V8 的 DLL,普通 AcroForm 文件(佔有腳本 PDF 的絕大多數)也根本無法取得 JavaScript 平台。2.13.2 版本將附加操作從 XFA 判定中移出:無論文件是否為 XFA,只要 V8FeaturesAvailable 回傳 True, InitializeFormFill 現在都會建置 IPDF_JsPlatform 表格。實際的結果是,文件級指令碼、欄位計算鏈和 app.alert 現在也能在普通的 AcroForm 文件中執行
AcroForm JavaScript 在 Delphi 中需要哪一個 V8 DLL?
AcroForm JavaScript 需要使用與 V8 引擎一同編譯 the PDFium 二進位檔案,當全域的 EnableV8Engine 旗標在程式庫首次載入前為 True 時,PDFium 元件會將其載入為 pdfium.v8.dll。這裡有一個值得明確說明的陷阱:元件會透過預先掃描 /XFA 標記來自動偵測 XFA 文件,並為您切換 EnableV8Engine,但帶有 JavaScript 的普通 AcroForm 文件不包含 XFA 標記,因此不會進行自動偵測。如果您的檢視器需要執行表單指令碼,請在載入第一個文件之前,自己手動設定該旗標一次;在普通 pdfium.dll 載入後,處理程序將無法切換引擎
uses PDFium;
procedure TViewerForm.FormCreate(Sender: TObject);
begin
// Must run before the singleton PDFium library first loads;
// once plain pdfium.dll is in the process it cannot be swapped
EnableV8Engine := True;
end;
procedure TViewerForm.OpenDocument(const FileName: string);
begin
FPdf.LoadDocument(FileName);
if not V8FeaturesAvailable then
StatusBar.SimpleText :=
'JavaScript disabled: pdfium.v8.dll not deployed';
end;
V8FeaturesAvailable 是可靠的執行階段探針:它回報載入的二進位檔案是否確實解析了依賴 V8 的匯出函式,因此無論安裝程式最終部署了哪一個 DLL,此檢查都能正常運作。相同的引擎和相同的旗標也支援動態 XFA 轉譯;關於 XFA 偵測與 V8 自動選擇如何互動的背景資訊,請參閱使用 PDFium 偵測 XFA 表單並擷取 XFA 封包
用於警示、列印、郵件和表單提交的主機事件
指令碼所做的一切可見操作都會透過 TPdf 事件路由回您的程式碼,且在未分配(unassigned)時,每一個事件都預設為安全的無操作(no-op)。 OnJavaScriptAlert 會接收來自 app.alert 的訊息、標題、按鈕類型和圖示,並讓您回傳按下的按鈕結果; OnJavaScriptResponse 服務 app.response 輸入提示(包含密碼旗標); OnJavaScriptBeep、 OnJavaScriptPrint、 OnJavaScriptMail 和 OnJavaScriptSubmitForm 則會顯露嗶聲、列印、郵件和提交請求及其完整參數集。未指定任何這些事件的檢視器仍然可以正常轉譯和計算;只是文件將無法開啟對話方塊、進行列印或發送任何內容
procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
const Msg, Title: WString; ButtonType, Icon: Integer;
var Result_: Integer; var Handled: Boolean);
begin
// Route app.alert through the VCL so it is owned by our window
Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
MB_OK or MB_ICONINFORMATION);
Handled := True;
end;
procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
const Url: WString; const Data: TBytes);
begin
// Nothing is transmitted unless this handler chooses to send it
LogAudit(Format('Document requested form submit to %s (%d bytes)',
[Url, Length(Data)]));
end;
預設行為的分離對於威脅建模(threat modeling)非常重要。 OnJavaScriptMail 和 OnJavaScriptSubmitForm 屬於通知類型:PDFium 元件本身不執行任何網路或 MAPI 動作,因此針對未做準備的主機,呼叫 this.submitForm 或 this.mailDoc 的惡意文件完全無法達成任何目的。主機必須藉由指定處理常式並自行實現傳輸方式來選擇啟用,這意味著將資料移出機器的決定權始終在您手中,由您在程式碼中結合手中的 URL 和負載(payload)來做出決定
如何阻止惡意 PDF 啟動 URL?
URI 動作是歷史預設行為主動而非被動的唯一地方,這就是為什麼它們獲得了專用的否決權。在 2.13.1 版本之前, FFI_DoURIActionWithKeyboardModifier 回呼函式會無條件地 ShellExecute 執行 XFA 欄位提供的任何 URI,這使惡意文件能夠在被點選時啟動任意 URL。 OnXfaUriAction 解決了這個問題:元件在執行前會諮詢此事件,而將 Handled 設為 True 的處理常式會完全抑制 ShellExecuteW 的預設行為。使該事件保持未分配狀態會保留舊有行為以維持相容性,因此注重安全的檢視器應始終分配此事件
procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
// Veto anything that is not plain https; the default would
// otherwise ShellExecuteW the URI as-is
if not SameText(Copy(Uri, 1, 8), 'https://') then
begin
LogAudit('Blocked URI action: ' + Uri);
Handled := True;
end;
end;
對協定方案(scheme)的允取清單(allow-list)是最低要求;更嚴格的檢視器會向使用者確認,或將所有內容路由至其專屬的沙箱瀏覽器元件。相同的否決哲學延伸到整個 v2.13.1 事件集: OnXfaEmail、 OnXfaHttpRequest 和 OnXfaOpenFile 各自顯露請求動作的文字參數,而元件本身則不執行任何網路或檔案傳輸。這些防護如何融入更廣泛的未受信任文件策略(包括轉譯隔離),是在 Delphi 中建置安全的 PDF 預覽的主題
使用 SetFocusedFormFieldText 寫入有焦點的欄位
在 2.13.2 版本中新增的 TPdf.SetFocusedFormFieldText 是 FocusedFormFieldText 的寫入側夥伴:它透過 FORM_SelectAllText 選取有焦點欄位的目前內容,並透過 FORM_ReplaceSelection 將其覆寫,效果完全就像使用者親自輸入了替代內容一樣。因為文字是透過互動式編輯路徑輸入的,所以附加到欄位的任何按鍵、格式化和計算指令碼都會以與手動輸入相同的方式觸發,這使得它成為在保持 JavaScript 啟用狀態的檢視器中,進行程式化填寫的合適基本元素。欄位周遊及圍繞其的焦點管理在使用 PDFium 元件進行表單欄位導覽中有所討論
if FPdf.FocusedFormFieldIndex >= 0 then
begin
if not FPdf.SetFocusedFormFieldText('42.50') then
ShowMessage('No focused field accepts text on this page');
// AcroForm: value commits to /V when focus leaves the field.
// XFA: the write stays in the in-memory edit buffer only and
// will not survive a save
end;
該註解中持久性的不對稱性是一個真實的界限,而不是為了流於形式而提出的警告。對於 AcroForm 文字和下拉式組合方塊(combo)欄位,編輯後的值會在失去焦點時提交至欄位的 /V 項目中,並在儲存時持久化。對於 XFA 文字欄位,寫入的內容僅保留在記憶體內的編輯緩衝區中,因為 PDFium 沒有公開的 API 來將控制項值協調回 XFA dataset 封包中;儲存文件不會帶走此變更。如果需要持久的 XFA 資料編輯,請直接編輯 dataset XML 本身,而不是編輯控制項
出貨前值得瞭解的限制
仍然存在兩個真實的界限。首先,PDFium 實現的 JavaScript API 是 Acrobat 物件模型的實用子集,以表單腳本核心為中心:欄位存取、計算和格式化事件、 app.alert 和 app.response、列印、郵件和提交請求。依賴於特定 Acrobat 專屬物件的文件將會退化(通常是默默地),因此請測試使用者實際處理的表單,而不是假定兩者完全相同。其次,啟用 V8 意味著需要出貨 pdfium.v8.dll,這是一個比普通建置版本大得多的二進位檔案;從不需要腳本或 XFA 的檢視器可以合情合理地保留較小的 DLL 並保持 m_pJsPlatform 未附加,這本身就是一種有效的安全態勢
由此產生的模式出奇地單純:在啟動時設定 EnableV8Engine,指定警示和回應事件以使對話方塊看起來像原生的一樣,指定 OnXfaUriAction 並稽核記錄郵件和提交事件,然後將其他所有內容保留為其無操作的預設值,直到有其他需求為止。完整的事件參考和表單填寫 API 介面均記錄在 PDFium 元件產品頁面上