HotPDF 讓多選清單方塊的值以陣列型態走完 FDF 與 XFDF 的來回轉換,從頭到尾不變。從 2.755.0 起,ExportLoadedFormToFDF、ExportLoadedInterchangeToFDF 與 ExportLoadedFormToXFDF 把每個選中選項寫成自己的 FDF 字串或 XFDF <value> 元素,對應的匯入方法先檢查每個值是否落在欄位選項裡、重建 /I 選擇索引,然後才動任何東西。過程中沒有任何東西被黏成一條字串
這個修掉的失敗很容易重現。拿一張帶產品選項多選清單方塊的訂購單,讓使用者勾兩項,把表單資料匯出給後台系統,再把編輯過的檔案匯回 PDF。改動之前,清單方塊回來時是空的或錯的。原因是有個匯出值裡帶了換行符,而舊路徑把所有選擇壓平成一條以換行分隔的字串。想從那條字串裡可靠地還原多個選擇,本來就沒戲,匯出值自己就含換行符時,更是完全不可能
用換行符串接多選值,為什麼會毀掉來回轉換?
把選擇串接成一條字串,等於扔掉值與值之間的邊界,而值本身可以包含分隔符,所以任何匯入器都沒辦法正確切回去。ISO 32000-1 §12.7.4.4 允許 choice 欄位的 /V 條目是單一文字字串或文字字串陣列,帶 MultiSelect 旗標(/Ff 的 bit 22)的清單方塊,選超過一項時就用陣列形式。同一節把 /I 定義為遞增排列的 0 基選項索引陣列,檢視器靠它區分兩個碰巧共用匯出值的選項。HotPDF 的純量 getter GetFormFieldValue 只讀字串形式,把陣列灌進去,匯出就退化成空字串;舊的 XFDF 匯入則用 LF 把重複的 <value> 元素串起來。想像一個匯出值是 Deep、換行、Blue:串接之後,Deep\nBlue\nRed 可以是兩個選擇,也可以是三個,檔案裡沒有任何東西能告訴您是哪個。修法就是徹底不讓純量出現在來回轉換的中間
匯出的 FDF 與 XFDF 檔案裡裝了什麼?
HotPDF 把多選值寫成 FDF 裡的型別化陣列、XFDF 裡每個選擇一個 <value> 元素,邊界在磁碟上看得見。FDF 裡每個項目保持它在來源 PDF 裡的拼寫:hex 字串以 hex 出去,literal 字串由單一輔助函式跳脫,CR 與 LF 變成 \r 與 \n。XFDF 的根帶 ISO 19444-1 要求的 xml:space="preserve",意思是文字元素裡的任何空白都算資料。所以 HotPDF 把每個 <value> 的開始標籤、跳脫後的文字與結束標籤一口氣寫出,縮排留在元素外面,CR、LF 與 TAB 編碼成字元參照,這樣做行尾正規化的 XML 解析器動不了原始位元組
<!-- FDF:每個欄位一個型別化陣列 -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF:每個選擇一個 <value> -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
寫呼叫端程式之前,有兩個匯出邊界情況值得知道。第一,ExportLoadedFormToFDF 在建立目標檔之前先把完整 FDF 內容建在記憶體裡(2.755.1 修正),所以無法匯出的值,例如陣列裡裝了字串以外的東西,會丟例外而不截斷既有檔案。第二,清單方塊同時提供空字串匯出值時,空選擇在 XFDF 裡是曖昧的,因為 <value/> 可以指什麼都沒選、也可以指選了空選項。ExportLoadedFormToXFDF 在這種情況下丟例外而不猜,而且丟在目標檔被打開之前。FDF 沒有這種曖昧,/V [] 與 /V [()] 是兩回事。兩個 FDF 匯出器也跳過沒有 /T 名稱的純介面元件終端,與 XFDF 匯出器一致,因為沒有任何匯入器能把那些條目對回欄位
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// 多選清單方塊寫成 /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// 空選擇加空匯出選項:XFDF 分不出來,
// 既有 .xfdf 檔原封不動
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
匯入時,HotPDF 怎麼驗證多選值?
HotPDF 只有在目標是設了 MultiSelect 旗標的 choice 欄位、且陣列裡每個值都對得上欄位 /Opt 陣列裡某個匯出值時,才接受匯入的陣列。每個選項槽只能用一次,所以一個有兩個選項共用匯出值 b 的清單,接受 [<62> <62>] 當兩個不同的選擇,拒絕第三個 b。重建的 /I 跟 /Opt 順序走、不跟傳入值的順序,因為 §12.7.4.4 要求索引遞增。HotPDF 把新的 /V 與 /I 建成游離物件,全部值過了驗證才指派,所以被拒的值不會留下半個陣列或過期索引。副本寫進正在匯入的欄位、而不是共用的上層陣列,從 FDF 來的 hex 拼寫一路保持 hex 到存檔,計算依賴這個清單方塊的欄位被標記待重算。如果只需要設單一值,在已載入 PDF 中設定單一表單欄位值走純量路徑,那條路徑設計上就不處理多重選擇
有些其他工具把純 ASCII 的匯出值寫成不帶位元組順序標記的 hex 字串,例如 <416272>,然後把那些 hex 數位當文字寫出去匯出 XFDF。回程上嚴格的 literal 比對就失敗,匯入中止。2.755.1 加了一次重試:值比不上任何選項時,HPDFHexSpellingText 把那段文字當 hex 負載解碼、再比一次。重試只作用在本來會丟例外的輸入,已經比得上的值永遠不會被改動。同一版也讓純量與陣列路徑用同一個 Unicode 解碼器,認得 PDFDocEncoding、帶任一種位元組順序標記的 UTF-16 與 UTF-8。在此之前,混用編碼的文件裡,同一個邏輯值可能在這條路徑比得上、在那條路徑失敗
合法的 FDF 檔案為什麼還是可能在剖析時丟欄位?
不追蹤 hex 字串的 FDF 掃描器,會在一個 hex 值緊貼著字典終結符結束時,把欄位字典攔腰切斷。<< /T (region) /V <416273>>> 裡,第一個 > 關掉 hex 字串,天真的掃描器卻把它跟下一個 > 一起讀成字典結尾,悄悄丟掉整個欄位。檔案層級的 FDF 匯入器本來就追蹤自己是否在 hex 字串裡,2.755.1 讓 ImportLoadedInterchangeFromFDF 背後的陣列與字典掃描器也照做。第二個問題涉及間接參照。FDF 檔是一份帶自己物件編號的小型 PDF 語法文件(ISO 32000-1 §12.7.7),所以 /V [11 0 R] 指的是 FDF 檔的物件 11,不是您正在填的 PDF 的物件 11。HotPDF 的簡化 FDF 解析器不解析檔內參照,所以它拒收這種陣列,而不是去讀目標文件裡碰巧叫物件 11 的那個東西
檔案、串流與 XFDF 匯入的錯誤回報方式不同
三條匯入路徑的驗證相同,但回報失敗的方式不同,值得刻意挑一條。ImportLoadedFormFromFDF 跳過任何驗證失敗的欄位、回傳實際套用的欄位數,所以數字比預期少是唯一的問題跡象。ImportLoadedInterchangeFromFDF 與 ImportLoadedFormFromXFDF 在第一個被拒欄位就丟例外。每個欄位各自提交,所以例外之前處理過的欄位保有新值。別把其中任何一個當成對整個交換檔的交易:需要全有或全無,就在例外發生時丟棄已載入的文件,別存它
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// 只匯入欄位;值落在 /Opt 之外或目標非多選時丟例外
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
擴充 XFDF 回呼,而不弄壞既有呼叫端
底層 XFDF 單元裡的陣列支援住在一個獨立記錄 THPDFXFDFArrayAccess 裡,以及 HPDFXFDFExportFields 與 HPDFXFDFImportFields 的新重載裡,而不是往既有 THPDFXFDFAccess 記錄的尾部加欄位。理由是二進位相容。把 THPDFXFDFAccess 當區域變數填的程式碼,常常只設自己知道的槽、其餘不清,新函式指標加進那個記錄就會裝著堆疊殘渣,函式庫還當它是真回呼。用獨立記錄,舊呼叫端保持舊版面與舊重載,那些重載內部傳一個全 nil 的陣列記錄。原始的純量匯入重載為相容性照舊用 LF 串接重複值,只有認得陣列的重載把它們分開。綁自己的資料儲存時,從 Default(THPDFXFDFArrayAccess) 起步。任何清單型欄位都讓 GetFormFieldValueArray 回傳 True,包括沒選任何東西的;回傳 False 則退回純量回呼
uses HPDFXFDF;
// 普通函式指標,不是 "of object":Context 帶您自己的儲存
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // 您既有的純量綁定
ArrayAccess := Default(THPDFXFDFArrayAccess); // 每個未用的槽都是 nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
多選交換作用在已存在、且 /Ff 裡設了 MultiSelect 位元的清單方塊上。choice 欄位與它的旗標位元最初怎麼建立的,見為已載入 PDF 新增 ListBox 與其他 AcroForm 欄位;走 XFDF <annots> 樹的註解標記,見HotPDF 的 XFDF 註解匯入與匯出。完整 API 參考與試用版下載在 HotPDF Delphi PDF component 頁面