技術文章

Delphi 中的柔邊遮罩與透明度平面化

PDF Library for Delphi 可在 Delphi 中製作兩種 PDF 柔邊遮罩(soft mask)。SetImageSoftMaskEx 把一張遮罩圖片附加到目標圖片上,並能寫入預乘(pre-multiplied)圖稿所需要的 /Matte 項目。NewSoftMaskGraphicsState 則從擷取的內容建立 alpha 或亮度遮罩,並回傳一個你可套用到後續繪製動作上的圖形狀態。而當某個消費端完全無法處理透明度時,FlattenTransparency 會把視覺結果渲染出來,並用它取代每一頁

柔邊遮罩是 PDF 用來表達漸層淡出、暈影效果、照片與背景融合,以及其他任何不透明度會隨區域變化之效果的方式。固定透明度只是圖形狀態中的單一數值;柔邊遮罩則是一整個位置函式,在你能夠製作它之前,整整一類設計效果都無法重現

兩種遮罩類型,各自讀取什麼

alpha 遮罩的數值取自遮罩內容的 alpha 色版。亮度遮罩則取自遮罩內容的亮度,因此白色區域完全不透明,黑色區域完全透明。設計師通常會選用亮度遮罩,因為繪製一個灰階漸層,是描述淡出效果最自然的方式

表單型柔邊遮罩是從擷取的內容建立而成的,其中有幾項細節是固定的,而非可選的。擷取的內容會成為一個以 DeviceRGB 表示的獨立透明度群組,讓遮罩在一個已知的色彩空間中求值。因此背景色必須具有三個分量,與該群組相符。轉換函式接受 /Identity 或一個有效的函式物件,讓你能重塑遮罩曲線,而不必重新繪製圖稿

示意圖:對照亮度柔邊遮罩與 alpha 柔邊遮罩如何送入 NewSoftMaskGraphicsState,並提醒圖形狀態會持續存在,直到被清除為止
亮度遮罩讀取的是亮度,alpha 遮罩讀取的是覆蓋率,NewSoftMaskGraphicsState 會把其中任一種包裝成一個持續存在的圖形狀態
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  MaskCapture, MaskGS, PhotoID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('brochure.pdf', '');

    // 第 4 頁繪有一個供遮罩使用的灰階漸層
    MaskCapture := Lib.CapturePage(4);

    MaskGS := Lib.NewSoftMaskGraphicsState(MaskCapture,
      PDF_SOFT_MASK_LUMINOSITY, '0 0 0', '/Identity');

    Lib.SelectPage(1);
    Lib.SetGraphicsState(MaskGS);
    PhotoID := Lib.AddImageFromFile('hero.jpg', 0);
    Lib.SelectImage(PhotoID);
    Lib.DrawImage(40, 700, 515, 260);        // 在遮罩之下淡出

    // ExtGState 設定會持續存在:必須明確結束作用範圍
    Lib.SetGraphicsState(Lib.NewSoftMaskGraphicsState(0,
      PDF_SOFT_MASK_NONE, '', ''));

    Lib.SaveToFile('brochure-masked.pdf');
  finally
    Lib.Free;
  end;
end;

最後那一步並不是裝飾用的。圖形狀態參數會持續存在,直到被變更為止,而事後套用一個普通的圖形狀態,並不會清除仍在作用中的柔邊遮罩。若沒有明確設定 /SMask /None,在受遮罩圖稿之後繪製的一切內容,都會繼承這個遮罩,這是視覺上最令人困惑、最難診斷的臭蟲之一

圖片遮罩與 Matte 項目

對於圖片層級的遮罩,遮罩圖片會先正規化為 DeviceGray,因此無論以哪種色彩空間提供遮罩,行為都能保持可預期。選用的 /Matte 項目——依圖片色彩空間而定,具有一到四個分量——會告訴消費端該圖片資料已針對該顏色做過預乘處理,藉此避免預乘圖稿被當成一般 alpha 進行合成時所產生的深色暈邊

示意圖:缺少 Matte 項目會產生深色暈邊,宣告了 Matte 則能乾淨合成,旁邊並列 PDF 1.4 與 PDF/A、PDF/X 的防護規則
宣告 /Matte 能避免預乘圖稿產生深色暈邊,而 PDF/A-1 與 PDF/X-1 檔案則會在任何物件被修改之前就被拒絕

這裡有兩道防護規則值得注意。PDF/A-1 與 PDF/X-1 禁止透明度,因此呼叫會在修改任何物件之前就拒絕執行,而不是先產生一份之後驗證會失敗的檔案。此外,成功建立柔邊遮罩需要 PDF 1.4,函式庫會自動設定這一點——透明度在該版本之前並不存在,一份含有柔邊遮罩、卻標示 1.3 版標頭的檔案,單純就是假的

重複的狀態會被重複使用:相同的擷取表單、遮罩類型、背景色與轉換函式簽章,會得到同一個圖形狀態物件,而不是每次呼叫都產生一個新物件。在一份對一百張圖片套用完全相同遮罩的文件中,這代表有九十九個物件根本不會被寫入

為何要平面化,以及平面化實際做了什麼

透明度至今仍是「螢幕上看起來沒問題、印刷時卻出錯」最常見的來源。較舊的 RIP、某些法規申報系統,以及 PDF/X-1a,全都要求檔案完全不含任何仍在作用中的透明度

這裡的平面化是刻意做得直截了當:所要求範圍內的每一頁,都會以選定的 DPI 對著不透明背景進行渲染,頁面內容與資源則被這單一張圖片取代。由於渲染器產生的是最終合成結果,這種做法能一次涵蓋所有情況——透明度群組、混合模式、柔邊遮罩、圖樣與巢狀表單——不需要針對每種結構各自實作近似邏輯、也就不會有各自出錯的風險。點陣化的峰值記憶體用量,也維持在單一頁面的範圍內

FlattenTransparency 管線示意圖:以 300 DPI 對不透明背景渲染每一個要求的頁面,並儲存為一份平面化、可供印刷的 PDF 1.3 檔案
每個要求的頁面都會以選定的 DPI 渲染一次,並以單一張不透明圖片的形式,儲存進一份名符其實的 PDF 1.3 檔案
// 以 300 DPI、白色背景平面化第 1 到 8 頁,再儲存為 PDF 1.3
Lib.FlattenTransparency('1-8', 300, $FFFFFF, 0);
Lib.SaveToFile('press-ready.pdf');

付出的代價同樣直截了當:輸出結果是一張圖片,因此文字不再可選取、也不再可搜尋,向量邊緣則會以選定的 DPI 重新取樣。平面化是一個交付步驟,不是編輯步驟。請保留含透明度的原始檔案,只對副本進行平面化

旋轉的頁面、使用者單位與頁面框

旋轉過的頁面需要一些天真實作會忽略的細心處理。渲染完成後,視覺寬度與高度會被寫回 MediaBox,使用者單位會被摺算進有效 DPI 與新的頁面尺寸,而 /Rotate 項目、使用者單位與輔助頁面框則會被移除

省略這一步,會產生一份平面化圖片本身正確、但檢視器卻又把它再旋轉一次,或依一個已不再適用的使用者單位重新縮放的檔案。移除這些頁面框,而不是重新計算它們,才是正確的做法,因為一個描述平面化之前幾何形狀的 TrimBox,比完全沒有 TrimBox 還要糟

附註與表單欄位預設會被渲染進靜態圖片中,互動物件則會被移除,這正是可供印刷交付成品所需要的效果。若你改為保留互動物件,渲染器的附註繪製步驟就會被關閉,避免任何內容被重複繪製兩次

會掃描一切的版本檢查

要儲存為 PDF 1.3,就要求檔案任何地方都不能再殘留透明度,而儲存前的檢查會檢視每一個間接物件,而不只是從頁面可以追溯到的那些物件。這比乍看之下更嚴格,而且是正確的做法:先前編輯留下的一個孤兒柔邊遮罩,仍然存在於檔案中,而一個依物件(而非依頁面)走訪的消費端,仍然會找到它

為了讓版本降級真正可行,一旦每個目標頁面都已成功替換,預設就會執行一次標記清除(mark-and-sweep)步驟,釋放無法再被追溯到的物件。需要事先規劃的後果是,在這次清除之前取得的圖片、擷取頁面與圖形狀態識別碼,事後可能就不再有效。請在繼續編輯之前重新取得你需要的資源識別碼,或者停用這次清除,並接受版本因此無法再降級

還有兩篇相關文章能補完整體圖像。使用漸層(shading)而非遮罩的向量圖稿,涵蓋於向量圖形、著色器與漸層一文,當效果能以漸層表達時,這往往是更好的路徑。而若你平面化的原因其實是色彩要求、而非透明度要求,把文件重新上色為另一個色彩空間一文所述的轉換路徑,能在平面化做不到的地方,保留仍可編輯的文字

柔邊遮罩製作、平面化與背後的渲染引擎,在 Delphi、C++Builder 與 Free Pascal 中同屬一套函式庫;完整功能清單請見PDF Library for Delphi 頁面