技術文章

Delphi PDF Block 範本:文字、影像與 PDF 填充

PDFlibPas(losLab PDF Library)讓 Delphi 填充 PDF block 範本:block 是範本頁上的一個具名矩形,型別為文字、影像或 PDF,而 FillTextBlock、FillImageBlock 與 FillPDFBlock 負責把變數資料畫進去。Block 以 PDFlibBlock:Text、PDFlibBlock:Image 與 PDFlibBlock:PDF 頁面註解的形式存在,或住在 /PieceInfo 底下的 page-piece 字典裡

用途是那種無聊但量大的場景。設計師把證書、活動票券或催繳信版面排好一次,程式把姓名、座位號、簽名影像或預先渲染的條碼頁蓋進固定槽位,跑上一萬筆記錄。表單欄位能做其中一部分,但 AcroForm 欄位是互動物件,有自己的外觀流與自己的規矩,到頭來您多半還得把它扁平化。Block 更簡單:一個矩形加幾條樣式提示,消費一次然後丟棄

在 PDFlibPas 裡,PDF block 到底是什麼?

PDFlibPas block 是一種頁面註解,/Subtype 為 PDFlibBlock:Text、PDFlibBlock:Image 或 PDFlibBlock:PDF,/Rect 是佔位區域,/Contents 字串裝著 name;key=value;key=value。第一段是 block 名稱,之後全是屬性清單。TPDFlib.AddBlock 寫入註解時帶 /F 4(Print)且沒有外觀流,所以未填充的 block 在螢幕和紙面上都不佔任何像素

Delphi 變數資料用的 PDFlibPas block 範本解剖:帶 /Subtype PDFlibBlock:Text、Image 或 PDF 的頁面註解、/Rect 佔位區、name 加 key=value 屬性的 /Contents 字串、/F 4 Print、無外觀流,並以 /PieceInfo /PDFlib /Blocks page-piece 字典作為第二個來源
Block 按頁從 1 起算列舉,註解在前、page-piece block 在後;陣列項目命名為 Block1 與 Block2,/Custom 鍵攤平成屬性,Image、PDF、Page 以外的型別一律當文字

讀取端還有第二個來源。列舉完 block 註解後,PDFlibPas 退回頁面字典路徑 /PieceInfo << /PDFlib << /Blocks ... >> >> 及其變體 /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>。Page-piece 字典是標準 PDF,ISO 32000-1 §14.5 把它定義為應用程式存放附加在頁面上的私有資料的地方,外部範本製作工具產生的 block 定義通常就住在那裡。/Blocks 容器可以是按 block 名稱索引的字典,也可以是普通陣列;陣列形式下 PDFlibPas 把項目命名為 Block1、Block2 依此類推,除非 block 自帶 /Name

在 page-piece block 內部,/Subtype(或 /Type)值為 Image 對應影像 block,PDF 或 Page 對應 PDF block,其他一律當文字。除了 /Type、/Subtype、/Name、/Rect 與 /Custom 之外的每個鍵都變成屬性,巢狀 /Custom 字典的鍵也一併攤平進來,所以 /Custom << /department (Finance) >> 項目可以用 GetBlockProperty(Page, Index, 'department') 讀到。Block 索引按頁從 1 起算,註解 block 在前,page-piece block 在後

用 AddBlock 製作 block 範本

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) 把 block 加進目前選中的頁面,成功回傳 1。名稱為空、寬或高為零或負、或 BlockType 不是 'text'、'image'、'pdf'(不分大小寫)時回傳 0。Properties 以分號附加在名稱後面,所以用的是與儲存字串相同的 key=value;key=value 語法

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // 靜態版面:邊框、商標、固定文字...
    Lib.DrawText(72, 200, 'This certifies that');

    Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
      'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
    Lib.AddBlock('text', 'course', 72, 290, 450, 24,
      'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
    Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
    Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');

    if Lib.GetBlockCount(1) <> 4 then
      raise Exception.Create('Template blocks missing');
    Lib.SaveToFile('certificate-template.pdf');
  finally
    Lib.Free;
  end;
end;

屬性解析器刻意寬鬆。鍵名比對不分大小寫,值會去頭尾空白、其餘原樣返回(寫成 {top left} 的值回來時帶著大括號),缺鍵回傳空字串。唯一的硬限制是分隔符:值裡不能有分號,因為分號就是段落結束

FillTextBlock 怎麼套用字型、顏色與對齊?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) 讀取 block 屬性,套用到繪圖狀態上,用 FitTextBox 把文字塞進 block 矩形,然後在 finally 區塊裡還原呼叫方的字型、字號、文字顏色與對齊。這個還原在合併迴圈裡很關鍵:紅色 30 pt 標題 block 不能把樣式漏進您在同一頁上的下一次 DrawText 呼叫

  • fontname(或 font):先重用文件裡已載入的同名字型,再對映 Helvetica-Bold、Times-Roman、Courier 等 Standard 14 名稱,最後嘗試以該名稱呼叫 AddTrueTypeFont
  • fontsize(或 size):傳給排版器的字號,文字塞不下時會縮小
  • fillcolor(或 textcolor、color):#RRGGBB、{rgb r g b}、{cmyk c m y k}、{gray g}、裸的 3 或 4 個數字清單,或 black、white、red、green、blue、gray 這些名稱;大於 1 的 RGB 分量按 0–255 讀取
  • alignment、align 與 position 一起讀取:left、right、center(或 centre)與 justify 設定水平對齊,top 與 bottom 設定垂直位置,預設垂直置中
  • fitmethod:word 或 nofitsplit 阻止長單字被拆斷,scale 或 expand 允許字號放大填滿方框

CMYK 路徑正是印刷廠在意的部分。{cmyk 0 0.55 1 0.1} 呼叫 SetTextColorCMYK,變數文字以 DeviceCMYK 輸出,上機時直接命中品牌色,不必來回轉 RGB。FillTextBlock 在文字畫出時回傳 1;Text 為空、block 不存在或 block 不是文字 block 時回傳 0,且這些情況下 block 原地保留

PDFlibPas for Delphi 的 FillTextBlock 處理順序:block 屬性 fontname、fontsize、fillcolor 與 alignment 套到繪圖狀態,CMYK 值經 SetTextColorCMYK,文字以 FitTextBox 塞入方框,呼叫方的字型、字號、顏色與對齊在 finally 區塊中還原
Block 樣式套用一次後即回滾,紅色 30 pt 標題因此漏不進下一次 DrawText;畫出文字回傳 1,空文字或非文字 block 則回傳 0 且 block 原地保留

填充影像與 PDF block,以及 Options 位元

FillImageBlock(Page, BlockIndex, ImageID, Options) 畫出先前加入的影像,FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) 把另一份已載入文件的一頁匯入為 Form XObject 並畫出,印章、QR code 頁或信頭之類的向量內容因此保持全向量。兩者都保持來源長寬比,結果在 block 矩形內置中。三個填充函式共用同一組 Options 位元:bit 1(值 1)在成功填充後移除 block,刪掉註解或從 /Blocks 容器移除條目;bit 2(值 2)把影像與 PDF 內容拉伸到精確矩形,而不是等比填充

function FillCertificate(const TemplateFile, SealFile, SignatureFile,
  OutFile: WideString; Values: TStrings): Boolean;
var
  Lib: TPDFlib;
  SealID, SignatureID, I: Integer;
  BlockName, BlockType: WideString;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
    SealID := Lib.SelectedDocument;          // 真正的文件 ID
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // 此時選中的文件已是範本
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // 倒著走:bit 1 會移除每個 block,後面的編號跟著重排
    for I := Lib.GetBlockCount(1) downto 1 do
    begin
      BlockName := Lib.GetBlockName(1, I);
      BlockType := Lib.GetBlockType(1, I);
      if BlockType = 'text' then
        Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
      else if (BlockType = 'image') and (SignatureID <> 0) then
        Lib.FillImageBlock(1, I, SignatureID, 1)
      else if BlockType = 'pdf' then
        Lib.FillPDFBlock(1, I, SealID, 1, 1);
    end;
    Result := Lib.SaveToFile(OutFile) = 1;
  finally
    Lib.Free;
  end;
end;

那個迴圈裡有兩個細節容易踩雷。迴圈從最高索引往下跑,是因為移除 block 2 會讓原來的 block 3 變成 block 2;帶著 bit 1 往上數會跳過一半的 block。另外,FillPDFBlock 的來源文件要先載入並在範本載入前抓住其 ID,因為影像 ID 屬於目前選中的文件,AddImageFromFile 必須趁範本還被選中時執行。對印章或信頭逐筆記錄變化的批次執行,PDFlibPas 的 Form XObject 頁面拼接與可組合浮水印範本在整頁層級用的是同一套先抓住再繪製的機制

Block 座標為什麼會翻轉?

當程式碼混用 PDFlibPas 應對的兩套座標系統,block 矩形就會翻轉。註解 /Rect 存在 PDF 使用者空間,從頁面底部起量(ISO 32000-1 §12.5.2),而 TPDFlib 繪圖 API 從頂部起量。AddBlock 收頂部基準的輸入,把下緣存成 PageHeight - Top - Height;GetBlockRect 與三個填充函式把矩形讀回來,用 PageHeight - URy 換算

實務上這表示:需要槽位幾何時(例如畫除錯框,或把說明文字擺在 block 旁邊)請用 GetBlockRect(Page, BlockIndex, Left, Top, Width, Height),不要透過通用註解呼叫去讀 /Rect 再直接餵給 DrawText。792 pt 的 letter 頁面上,帶 /Rect [40 700 240 730] 的 page-piece block 經換算回報 Top = 62,不是 700。這攔下的是那個經典 bug:所有填充值鏡像落在下邊距附近,而且什麼錯誤都不報

PDFlibPas for Delphi 的 block 矩形座標翻轉:/Rect 值存在底部基準的 PDF 使用者空間,AddBlock 收頂部基準輸入並存成 PageHeight 減 Top 減 Height,因此 792 pt letter 頁上的 /Rect 40 700 240 730 經 GetBlockRect 與其 PageHeight 減 URy 換算讀回 Top 62
混用兩套座標系統會讓每個填充值鏡像落向下邊距且一個錯誤都不報;槽位幾何請透過 GetBlockRect 讀取,別把 /Rect 直接餵給 DrawText

值得記取的教訓

block 程式碼裡最有教育意義的 bug 是一個字元的疏失。Block 註解靠 subtype 的前綴測試辨識,而 'PDFlibBlock:' 有 12 個字元。早期版本拿 Copy(Subtype, 1, 13) 與這個 12 字元字面值比對,永遠不可能相等,於是 GetBlockCount 對每個範本都回傳 0,每次填充都報「找不到該 block」。沒有崩潰,也沒有日誌。教訓可以推廣到任何範本引擎:在測試裡斷言已知範本的 block 數量,因為靜默的零配對列舉看上去跟空白頁一模一樣

第二個陷阱是文件 ID。TPDFlib.LoadFromFile 回傳的是狀態碼,1 成功、0 失敗,不是剛載入文件的 ID。把那個 1 當 SourceDocumentID 傳給 FillPDFBlock 會失敗,因為 1 不是有效的文件 ID。載入成功後緊接呼叫 SelectedDocument 拿真正的 ID;若之後又載入了來源,用 SelectDocument 切回範本

還有一個小案例:空字串的 FillTextBlock 什麼都不做並保留 block。對有選填欄位的合併列印來說,比如缺第二行地址,這通常正是想要的行為,但未填充的註解就留在了輸出檔裡。若輸出必須乾淨,用 DeleteAnnotation 明確移除,或帶 bit 1 填一個空格。所有 block 呼叫都會保存並還原目前選中的頁面,所以對第 3 頁的填充不會改變您下一次繪圖呼叫的去向

Block 註解的互通性限制是什麼?

Block 註解是私有慣例,不是標準 PDF 功能。PDFlibBlock:Text 和它的兄弟們不在 ISO 32000-1 §12.5.6.1 列出的註解型別裡,檢視器會把它們當未知註解。因為 Invisible 旗標沒有設、又沒有外觀流,合規的檢視器不會畫出它們,但某些檢視器仍可能在留言面板列出它們,其他 PDF 工具也不會理解或保留其語意

  • 任何要離開您系統的輸出都帶 bit 1 填充,讓最終檔案只含普通頁面內容
  • PDF/A 不允許 PDF 規格之外的註解型別(ISO 19005-1 §6.5.2),所以歸檔輸出不得殘留未填充的 block 註解
  • Page-piece block 留在範本裡比較安全,因為 /PieceInfo 是標準結構,消費端會忽略不屬於自己的資料
  • Block 不是表單欄位:沒有互動、檔案裡沒有欄位值、也不能匯出成 FDF 或 XFDF;必須保持可編輯的資料請看PDFlibPas 的 FDF、XFDF 與 XFA 表單資料交換

如果印刷流程要求範本以標準化、可被驗證器檢查的方式宣告其變數欄位,block 就是錯的工具。那是 PDF/VCR-1 的職責,PDFlibPas 的 PDF/VCR-1 範本佔位清單說明了那些佔位如何住在結構樹裡而不是註解裡

FAQ

PDFlibPas 能填充其他工具建立的 block 範本嗎?

可以,只要工具把 block 存在 /PieceInfo /PDFlib /Blocks 或 /PieceInfo /PDFlib /Private /Blocks 底下,字典或陣列形式,帶四個數字的 /Rect。GetBlockCount 會在任何 PDFlibBlock:* 註解之後接著數這些,自訂鍵透過 GetBlockProperty 暴露

FillTextBlock 會改動呼叫前所選的字型嗎?

不會。FillTextBlock 保存目前選中的字型、字號、文字顏色與對齊,套用 block 的 fontname、fontsize、fillcolor 與 alignment 屬性,事後還原原狀,即使排版失敗也一樣

怎麼把向量商標或條碼頁放進 block?

載入來源 PDF,把 SelectedDocument 存成它的 ID,必要時再選回範本文件,然後呼叫 FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1)。該頁以 Form XObject 匯入,保持向量,除非加 bit 2,否則按原長寬比填充到 block 裡

Block 範本、Form XObject 頁面匯入與其餘變數資料工具都隨PDFlibPas,losLab 的 Delphi PDF Library 提供,涵蓋 Delphi 與 C++Builder 專案的 PDF 建立、編輯與印刷生產