技術文章

Delphi 中的 PDF Type 2/3/4 函數:指數、拼接與 PostScript

HotPDF 是適用於 Delphi 與 C++Builder 的原生 VCL PDF 元件,會評估三種以公式而非取樣網格建立的 PDF 函數類型:Type 2 指數插值、Type 3 拼接,以及 Type 4 PostScript 計算器函數,分別對應 ISO 32000-1 §7.10.3、§7.10.4 與 §7.10.5。Type 2 沿著曲線在兩個輸出向量之間混合,Type 3 在單一輸入網域中串接多個子函數,而 Type 4 執行受限制的 PostScript 程式,可以分支、比較,並從輸入計算內容串流幾乎需要的任何結果。只要其中任一種的實作稍有錯誤,失敗通常不會直接宣告自己是錯誤,而會表現為帶有死平帶的漸層、渲染成純黑的特別色,或在測試套件碰巧未嘗試的輸入上恰好差一的計算器函數

這三種函數旁邊還有第四種 Type 0,它儲存的是取樣網格而非公式,另請參閱Type 0 色彩查找表的配套文章。這兩個家族都在解決將輸入對映至輸出的相同問題,但 Type 0 是預先計算並寫入檔案的資料,而 Type 2、3 與 4 則是閱讀器每次呼叫時都會評估的程式碼。四種函數在 HotPDF 的渲染器中共用同一個分派點,以函數字典的 /FunctionType 項目為依據,因此陰影、色調轉換或半色調網點函數不必先知道收到的是哪一種,就能要求取得色彩

PDF Type 2 指數函數如何運作?

PDF Type 2 函數計算一個公式 — y = C0 + x^N × (C1 − C0),逐個元件套用 — 其中 x 是函數的單一輸入,會在公式執行前依據 /Domain 正規化(ISO 32000-1 §7.10.3)。/C0 與 /C1 是該範圍兩端的輸出向量,每個輸出元件各有一個數字;/N 則是控制兩者之間曲線形狀的指數:N = 1 會產生大多數漸層停止點與雙色調轉換背後的直線漸變,N 大於 1 會將曲線拉向 C0,而介於 0 與 1 之間的 N 會將曲線推向 C1。RegisterExponentialFunction 會從五個引數建立該字典,並傳回可插入陰影、半色調網點函數,或規格接受 /Function 鍵的其他位置的函數物件

C0 與 C1 之間的元件數量關係有兩次重要性:一次是在建立 Type 2 函數時,另一次則是在 HotPDF 必須渲染它未建立的函數時。在建立端,RegisterExponentialFunction 會相互檢查 C0 與 C1,若兩者不一致便引發例外,因此抵達 BeginDoc 的呼叫已經擁有自洽的函數物件。然而在渲染端,評估器必須信任來源檔案實際宣告的 /C0 與 /C1 陣列,例如開啟供預覽的印刷廠檔案,或顯示給使用者檢視的已簽署文件;2.376.0 之前的版本會將這些陣列讀入為四個元件配置的緩衝區,也就是 CMYK 案例。若是含有一或三個元素 /C0 與 /C1 的 DeviceGray 或 DeviceRGB 指數色調,讀取會無聲失敗並讓兩個陣列都保持為零,於是色調會塗成純黑而非預期色彩。2.376.0 版將讀取器調整為函數實際宣告的輸出數量,而非固定緩衝區大小,這正是只有非 CMYK 測試案例才能揭露的錯誤,因為現有測試全程使用 CMYK,四個寫入四個總是能夠容納

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Type 3 拼接:透過 Bounds 陣列串接子函數

PDF Type 3 函數會將 k 個子函數拼接成單一輸入 /Domain 上的分段對映,而讓這項工作成立的兩個陣列是 /Bounds 與 /Encode(ISO 32000-1 §7.10.4)。/Bounds 包含 k − 1 個內部分割點,將 /Domain 切成 k 個連續區間;評估器會選擇上限大於輸入的第一個區間,或在輸入到達最後一個界限後選擇最後一個區間,然後交給該區間的子函數。接著 /Encode 會將輸入從它在該區間內的位置重新對映至所選子函數本身預期的輸入範圍,通常在子函數是另一個指數區段時為 [0, 1],之後評估會再深入一次,進入該子函數自己的 /Domain 與 /Range

HotPDF 的拼接評估器過去只處理恰好兩個子函數,而 /Bounds 讀取器要求完整的八元素陣列,因此兩段漸層實際需要的單一分割點 — /Bounds 中的一個數字 — 總是解析失敗,函數也就不傳回任何結果。/Encode 完全沒有套用。2.376.0 版依照規格所述,將選擇邏輯重寫為一般化的 k 子函數搜尋,並開始依據 /Bounds 實際宣告的長度讀取,因此由三、四或五個指數區段拼接而成的三段、四段或五段停止點漸層,現在都能以宣稱支援兩段函數的相同方式解析。下例建立黑色經紅色到白色的兩段漸變,當單一指數曲線無法承載設計所需的每個色彩停止點時,軸向或徑向陰影便會採用這種形狀

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

Type 4 PostScript 計算器函數能做什麼,而 Type 2 和 3 不能?

PDF Type 4 函數會執行真正但刻意受限制的程式:PostScript 計算器會將輸入推入運算元堆疊,執行算術、比較、堆疊操作與布林運算子,以及 if/ifelse 條件式,完成時再將輸出留在堆疊上(ISO 32000-1 §7.10.5,表 42)。它沒有迴圈結構,也沒有具名變數儲存,只有堆疊,這讓符合規格的程式容易推理;但在這組受限制的運算子中,Type 4 仍能表達 Type 2 與 Type 3 無法表達的內容,例如 DeviceN 分色的真正多油墨混合公式,或帶有條件閾值的半色調網點函數。HotPDF 的評估器 HPDFEvalPostScriptCalculator 會先將程式標記化一次,包括數字、運算子與 { } 程序區塊,接著走訪 100 個項目的運算元堆疊,這是 ISO 32000-1 §7.10.5 所要求的深度,並以最多評估 50,000 個運算子作為防禦性上限,以避免病態或手寫程式造成問題

roll 運算子:方向很容易弄反

roll 是第一次實作時最容易弄反的運算子,因為它的引數順序與旋轉方向都和英文描述的直覺相反。n j roll 會取出數量 n 與旋轉量 j,接著將堆疊頂端 n 個項目循環移動 j 個位置,從一端移出的項目會繞回另一端;規格中的經典範例是 a b c 3 1 roll 產生 c a b,頂端項目會移到群組底部,而不是相反,其他每個項目都向上移動一格來騰出位置。HotPDF 的評估器會將堆疊項目 i 的新位置計算為 (i + j) mod n,與該範例完全一致,但這段兩行迴圈同樣很容易把旋轉寫反,而鏡像的 roll 仍會產生看似合理的色彩,只是那不是檔案作者要求的色彩

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round 不是 Delphi 的 Round:向上捨入與銀行家捨入

PostScript 的 round 運算子每次都會將 .5 的平手值朝較大的整數解析,而 Delphi 內建的 Round 函數並非如此:它採用四捨五入至偶數,也就是銀行家捨入慣例,會交替決定 .5 平手值的方向,使重複捨入不會累積偏差。兩者幾乎處處一致,只有在這裡重要的邊界上不同:Delphi 的 Round(0.5) 傳回 0、Round(2.5) 傳回 2,而 PDF 規格中的 round 對相同輸入要求傳回 1 與 3,因此差異會藏在一般測試中,之後只要計算器程式的中間運算結果恰好落在半整數,就會穩定重現差一的結果。ISO 32000-1 §7.10.5 表 42 明確指出 round 會將小數 .5 推向較大的整數,因此 HotPDF 將該運算子實作為 Floor(x + 0.5),而不是呼叫 Delphi 的 Round;任何手動重新實作或抽查 Type 4 程式算術的程式碼也需要採用相同替代方式

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

註冊時驗證可及早捕捉錯誤的計算器程式

格式錯誤的 Type 4 程式在建立時很容易捕捉,在其他時機則代價高昂,因此 RegisterPostScriptFunction 不只是儲存來源文字:它會在函數物件寫入文件前,先以宣告 /Domain 的中點對程式試算一次。不平衡的 { } 區塊、無法辨識的運算子、堆疊下溢,或與 /Range 不相符的輸出數量,都會使這次試算失敗並立即引發例外,呼叫堆疊會指向 RegisterPostScriptFunction 呼叫,而不是指向檔案已交付後在 QA 中才發現的渲染結果。中點試算不能證明程式在整個 /Domain 上都正確,因為只在輸入範圍一端附近出錯的條件分支仍可能避過單一取樣點,但它排除了整類結構上已損壞而不只是某個角落結果錯誤的程式

漸層與特別色如何使用這些函數

在真實 PDF 中,Type 2、3 與 4 很少單獨出現;它們會出現在規格接受 /Function 鍵的任何位置,而最常見的兩個使用者是陰影與特別色色調轉換。軸向或徑向漸層的 sh 運算子(ISO 32000-1 §8.7.4.5)會沿漸層軸線的每個位置評估其 /Function,這正是 Type 3 拼接存在的多停止點案例。Separation 或 DeviceN 色彩空間的色調轉換是這三種函數的另一個常見用途,也是 Type 4 發揮價值之處:單一特別色油墨通常可化為 Type 2 或 Type 0 曲線,但包含真實陷印與疊印行為的多油墨 DeviceN 混合,往往需要只有 PostScript 計算器才能表達的條件邏輯,這正是渲染 Separation 與 DeviceN 特別色的文章所涵蓋的案例。RegisterSeparationFunc 是建立端的配對呼叫:它接收色料名稱、替代色彩空間,以及 Register*Function 家族傳回的任何物件,並將該色調轉換接入 Separation 色彩空間資源,頁面的其餘部分即可使用 scn/SCN 選取它

Type 0 的取樣網格與這三種公式驅動的類型合在一起,涵蓋 PDF 能宣告的每個 /Function,而選擇正確類型主要取決於手邊已有的內容:在其他地方計算好的查找表就用 Type 0,兩端點混合用 Type 2,跨網域串接的多個混合用 Type 3,而任何真正包含條件邏輯的內容則用 Type 4。RegisterExponentialFunctionRegisterStitchingFunctionRegisterPostScriptFunction 都是適用於 Delphi 與 C++Builder 的標準HotPDF 元件的一部分,並與其餘 ISO 32000-1 函數及陰影 API 並列