有欄位值卻沒有外觀串流的 PDF 表單,經過 Linux 渲染作業可能變成空白方塊、錯的字型或對齊,或丟出一個訊息為「Widget field requires a name field type」的 EHPDFHeadlessRenderError。HotPDF 的 headless 渲染器為文字、按鈕與選擇型 widget 繪製臨時外觀,沿欄位樹繼承 /FT、/V、/DA 與 /Q
widget 把欄位設定放在哪裡
大多數麻煩始於欄位與其 widget 的分工。欄位字典保管表單控制項的邏輯部分:類型、值、旗標與預設外觀。widget 註解保管頁面需要的東西:/Rect、邊框、指向所屬頁面的指標。只有一個 widget 的欄位通常合併成單一字典。同一個值得出現在多個頁面時,widget 就成為欄位的子項,往往只帶幾何資訊與一個 /Parent 參照。ISO 32000-1 §12.7.3.1 讓 /FT、/Ff、/V 與 /DV 沿這條鏈可繼承(表 220),§12.7.3.3 則只把兩個可變文字屬性加進表 222 的可繼承集合:/DA(必填,但可來自祖先)與 /Q。互動式表單字典(表 218)提供全文件層級的 /DA 與 /Q 預設值,外加 /DR——查找 /DA 字型名稱時用的預設資源字典。/DR 不在表 222 裡;它屬於表單整體。只讀 widget 字典的渲染器,在下面的檔案裡找不到類型、值或 /DA,便退回自己的預設字型
1 0 obj % 目錄
<< /Type /Catalog /Pages 2 0 R
/AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
/DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj % 終端欄位:類型、值與外觀預設都在這裡
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
/DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
/Kids [11 0 R 12 0 R] >>
endobj
11 0 obj % 第 1 頁的 widget:只有幾何,角點先給右上
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % 第 2 頁的 widget:只有幾何
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF 先在 widget 上找每個欄位項目,找不到就沿 /Parent 一層字典一層字典往上爬;最近的值贏,只有 /DA 與 /Q 在整條鏈走完之後才退回 AcroForm 字典。就上面的檔案而言,預期結果是兩個 widget 都以 10 點 Courier、深藍色、右對齊顯示 INV-2026-0417,全文件層級的 /Helv 0 Tf 從頭到尾派不上用場,因為物件 10 有自己的 /DA。當欄位反而以 12 點黑色 Helvetica 出現,頭號嫌犯是那台 Linux 機器上缺 Courier 字型。字的大小出賣了它——那正是 widget 上、任何祖先上、AcroForm 字典裡都找不到 /DA 時,HotPDF 畫出來的東西。所以把 widget 傾印出來,手動沿著 /Parent 走,找出產品把 /DA 放在哪裡,例如放在 widget 從未回指的欄位字典上
/DA 裡的字型名稱按這個順序解析:widget 或父欄位上的 /DR、AcroForm 的 /DR、頁面的字型資源。欄位層級的 /DR 不在表 222 裡,但它只要存在就毫無歧義,所以 HotPDF 接受它。向上爬是有界的:樹裡出現環時以「Widget field inheritance contains a cycle」停住,/Parent 不是字典時以「Widget Parent requires a dictionary」停住,鏈深超過渲染選項裡的 MaxDepth(預設 32)時以「Widget field inheritance depth budget exceeded」停住。VCL 元件在讀取繼承的 AcroForm 值並重設欄位時,套用同一套繼承
headless 渲染器如何決定畫什麼?
HPDFRenderHeadlessPagePNG 與其檔案版本,只對可見且 /AP 缺失或為 null 的 widget 跑後備繪製;ISO 32000-1 §7.3.7 說 null 字典值等同條目缺席,渲染器也把 /AP null 當成這樣對待。已存在的一般外觀永遠贏,/NeedAppearances 說什麼都一樣,所以先前填表留下的過期外觀會一直顯示,直到有東西重新產生它。已載入的文件絕不被修改。畫什麼,取決於解析出的欄位類型與旗標:
| 欄位 | 合成的外觀 |
|---|---|
單行文字,/FT /Tx | 字串 /V,以解析出的 /DA 與 /Q 排版在 /Rect 內 |
核取方塊,/FT /Btn | 矩形邊框配 /Border 給定的圓角半徑、/MK /BG 填色,選中時加一道描邊的向量勾選記號 |
| 選擇鈕,Btn 設定位元 16 | 圓形邊框,選中時加實心圓點 |
| 按鈕,Btn 設定位元 17 | 矩形邊框,/MK /CA 標題文字置中 |
下拉式方塊,/FT /Ch 設定位元 18 | 所選 /Opt 項目的顯示文字作為置中標題 |
| 清單方塊,Ch 位元 18 未設 | 自 /TI 起每個選項一列,每個選取列後方有高亮色帶 |
勾選記號與選擇鈕圓點是向量路徑,而不是許多產生器採用的 ZapfDingbats 字形,因為精簡的 Linux 容器裡可能根本沒裝 ZapfDingbats。按鈕與下拉式方塊的面貌與 HotPDF 的 VCL 外觀產生器為同一欄位畫出的一致,所以無論圖像由哪一端產出,表單看起來都相同。標題文字的字型、大小與顏色取自 /DA;標題排版不會自動縮放,/DA 大小為 0 時取方塊高度的 70%,上限 12 點。核取方塊與選擇鈕的狀態來自 widget 自己的 /AS,退回到繼承的 /V,任何非 Off 的狀態都畫出記號。給每個選擇鈕子項自己的 /AS。父項的 /V 為整組指名選中狀態,分不清哪個子項是哪個
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
out Diagnostic: string): Boolean;
var
Doc: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Result := False;
Diagnostic := '';
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
Options := THPDFHeadlessRenderOptions.Default; // RenderAnnotations = True, MaxDepth = 32
Options.Usage := hruPrint; // 如同檢視器的列印:沒有 Print 旗標的 widget 一律跳過
try
// PageIndex 以 0 為基底
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// 例如 'Widget field requires a name field type';表單已填值時,
// 別用 RenderAnnotations := False 重試,那會連 widget 帶值一起丟掉
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
文字欄位的後備繪製只處理持有字串值的單行欄位。更豐富的文字排版,請在填表當下於 Windows 端產生外觀——EnsureLoadedFieldAppearanceStream 會在檔案扁平化或出貨之前補出缺失的 /AP 串流;Linux 作業於是收到真正的外觀串流,而 headless 渲染器會指名它遇到的任何仍缺外觀的欄位。不過,判斷 Linux 輸出要用 Linux 渲染器。RenderLoadedPageToBitmap 透過 Windows GDI 後端繪製,從不跑這套後備邏輯,所以那裡看起來正常的頁面,對 Cairo 裝置會從同一份檔案產出什麼,毫無預測力
欄位旗標的位元從 1 起算
ISO 32000-1 §12.7.3.1 規定 /Ff 字組的位元位置從低位端的 1 數到 32,表 226、228 與 230 都用這套編號。所以在 Delphi 裡,位元 N 對應 1 shl (N - 1)。照位元編號本身去位移,每個測試都會上移一格:為「radio,位元 16」寫的 1 shl 16 實際測的是 pushbutton 旗標,為「combo,位元 18」寫的 1 shl 18 測的是 Edit。兩個錯的遮罩都是真實旗標。程式碼照樣編譯、照樣執行,然後把欄位歸錯類
| 旗標 | 表 | 位元 | Delphi 遮罩 |
|---|---|---|---|
| Multiline | 228(文字) | 13 | 1 shl 12 = 4096 |
| Password | 228(文字) | 14 | 1 shl 13 = 8192 |
| Comb | 228(文字) | 25 | 1 shl 24 = 16777216 |
| RichText | 228(文字) | 26 | 1 shl 25 = 33554432 |
| Radio | 226(按鈕) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226(按鈕) | 17 | 1 shl 16 = 65536 |
| Combo | 230(選擇) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// ISO 32000-1 對 Ff 的位元從 1(低位端)起算,所以位元 N 是 1 shl (N - 1)
TxMultiline = 1 shl 12; // 位元 13
TxPassword = 1 shl 13; // 位元 14
TxFileSelect = 1 shl 20; // 位元 21
TxDoNotScroll = 1 shl 23; // 位元 24
TxComb = 1 shl 24; // 位元 25
TxRichText = 1 shl 25; // 位元 26
TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
TxDoNotScroll or TxComb or TxRichText;
procedure ListTextFieldsForProducerAppearance(const FileName: string;
Report: TStrings);
var
Doc: THPDFHeadlessDocument;
Field: THPDFHeadlessFormField;
I: Integer;
begin
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
for I := 0 to Doc.FormFieldCount - 1 do
begin
// FieldType、Flags 與 MaxLength 抵達時已繼承自祖先
Field := Doc.GetFormField(I);
if Field.FieldType <> 'Tx' then
Continue;
if (Field.Flags and TxProducerLayout) <> 0 then
Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
[Field.Name, Field.Flags]))
else if Field.MaxLength > 0 then
Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
[Field.Name, Field.MaxLength]))
else if Field.Value = '' then
Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
end;
finally
Doc.Free;
end;
end;
掃描讀的是欄位層級的旗標——產品正是把旗標放在那裡;渲染器也認得獨立 widget 上的 /Ff,所以把清單當規劃輔助,把渲染診斷當最終裁決。空白檢查很重要,因為沒有 /V 也沒有 /AP 的文字欄位無東西可合成:渲染以「A text-field widget without a value has no appearance to synthesize」收場,解方是在檔案抵達渲染器之前補上值或外觀
「Degenerate appearance geometry」是什麼意思?
它指的是矩形或外觀方塊沒有面積,而反轉的 /Rect 不是原因。ISO 32000-1 §7.9.5 允許矩形以任兩個對角角落指名,其註記要處理軟體做好把它們正規化的準備。想都不想就把寬度算成 x2 - x1 的渲染器,從 [300 720 160 700] 得到負數,接著不是拒絕註解,就是鏡射它的外觀。HotPDF 在任何尺寸檢查之前,先把座標換成左下與右上的順序,合成面貌與既有 /AP 串流一視同仁,所以上面的 widget 11 渲染起來如同角點是常規寫法。這個錯誤只針對帶有外觀串流的註解拋出,且是在正規化之後 /Rect 寬或高為零,或串流的 /BBox 在其 /Matrix 之下塌縮成線段或點時。錯誤出現時,把 widget 的 /Rect 連同串流的 /BBox 與 /Matrix 一起傾印出來:三者之一有零尺寸
沒有 /AP 且 /Rect 為零面積的 widget 走另一條路:後備邏輯跳過它,繼續畫頁面。ISO 32000-2 §12.5.2 只豁免 Popup、Projection 與 Link 註解,以及塌縮成單一一點的矩形。僅是一條線的矩形在 PDF 2.0 下仍欠一份外觀;渲染器連它也跳過,純粹因為沒有面積可畫
找不到欄位類型時
「Widget field requires a name field type」意味著渲染器從 widget 沿 /Parent 連結爬到了頭,卻沒找到 /FT。渲染器從頁面的 /Annots 陣列出發,向上的 /Parent 連結是它通往欄位的唯一路徑。載入走的是另一個方向,從 AcroForm 的 /Fields 陣列沿 /Kids 向下,這就是 LoadFromFile 能在一份頁面後來渲染失敗的檔案上成功的原因。典型病因是欄位 /Kids 裡列著的 widget 沒有回指。ISO 32000-1 表 188 給了這種做法一點依據:它只要求 widget 是多個子項之一時帶 /Parent,並寫著「其餘情況省略」,於是照字面實作的寫入端,會在只有一個獨立 widget 的欄位上省掉連結。ISO 32000-2 已把同一條目放寬為「其餘情況選用」。無論如何,我們會在每個未與欄位合併的 widget 上寫 /Parent。ISO 32000-2 允許這樣做,而這也是頁面驅動的讀取端——我們的也罷、別人的也罷——能夠觸及欄位的唯一方法。無法修正產品時,把單獨的 widget 合併進其欄位字典能得到同樣效果
widget 回指其父項、並在向上的路上某處找到 /DA,那麼即使毫無 /AP,在HotPDF Delphi component 的 headless 渲染器裡也能正確渲染——這使得在 Linux 表單管線裡,最該先弄對的是欄位樹,而非外觀串流