頁面拼貼是在共享的畫布上組合現有頁面的內容,這與合併檔案是不同的操作。losLab PDF 函式庫為 Delphi 實作了 Form XObject 頁面拼貼與可組合的浮水印範本:StitchPageOverlay、StitchPagesSideBySide 和 StitchPagesVertically 可以以 any 比例放置擷取的頁面,並保持完整的向量保真度;而 TPDFlibCompositeWatermark 則可在一次呼叫中跨頁面範圍堆疊文字和影像浮水印。如果您實際需要的是將多個文件的完整頁面附加到一個檔案中,那是合併系列功能,已在快速位元組級 PDF 合併文章中介紹;本篇文章則是關於在單一座標空間中組合頁面內容。
導致這些需求的場景非常普遍且棘手。例如,學校希望將兩個考卷頁面印在同一張橫向紙張上以節省一半的紙張;在地化審查人員希望將英文頁面及其翻譯版本並排顯示在單個頁面上,並一起捲動;法務團隊希望在每個草稿上同時蓋上三個圖層的浮水印:角落的標誌、中間對角線的 CONFIDENTIAL(機密)字樣,以及一個小頁尾行,且僅限於第 1 頁到第 3 頁。這三者本質上都是同一個底層問題,即將已排版好的頁面內容放置在其他地方並以其他大小呈現,而不會將其降級為點陣圖。
為什麼串接 PDF 內容資料流會失敗?
天真的拼貼做法(即直接將來源頁面的 /Contents 資料流複製到目標頁面中)會因為三個結構性原因而失敗。第一,座標衝突:兩個內容資料流都假設自己的原點位於自己 MediaBox 的左下角,因此貼上的內容會落在錯誤的位置並與現有內容重疊。第二,資源名稱衝突:每個頁面都會根據自己的 /Resources 字典來解析像 /F1 Tf 和 /Im0 Do 這樣的運算子,而兩個頁面通常會將同一個名稱繫結到不同的字型或影像,因此其中一個頁面會默認以錯誤的資源進行轉譯。第三,原始的資料流複製完全無法為您提供縮放功能,您能做的頂多只有平移。
Form XObject 一舉解決了這三個問題,這就是為什麼 losLab PDF 函式庫將其拼貼功能建立在它們之上。ISO 32000-1 §8.10 將 Form XObject 定義為一個獨立的內容資料流,具有自己的 /Resources 字典和自己的座標空間,透過在任何目前作用中的轉換矩形下呼叫其名稱來放置到頁面中。CapturePage 將整個現有頁面(內容加上資源)包裝成一個 /Subtype /Form XObject,然後拼貼函式會使用 cm 矩陣來定位它:隨意進行縮放、平移、旋轉。因為來源保持為向量格式,所以在任何縮放層級下,文字仍是文字,路徑仍是路徑,這與使向量圖形與陰影圖案具備解析度獨立性的屬性相同。沒有任何內容會被點陣化,而且隔離的資源字典意味著被擷取的頁面不會與目標頁面爭奪 /F1 這個名稱。
如何將兩個 PDF 頁面合併到同一個頁面上?
StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) 是最直接的答案:它擷取兩個來源頁面,建立一個新的較寬頁面,並將這兩個擷取內容左右放置,中間留有間距,且每個內容都按比例縮放以符合 TargetHeight。該函式會傳回新的頁碼(以 1 為基準)或在失敗時傳回 0,且兩個來源頁面都會作為操作的一部分被擷取並隱藏。每張紙印兩頁的考卷案例只需三行程式碼即可完成。
var
Lib: TPDFlib;
NewPage: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('exam.pdf', '');
// Pages 1 and 2 on one new wider page with an 18pt gutter,
// both scaled to a 595pt output height (A4 landscape)
NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
if NewPage > 0 then
Lib.SaveToFile('exam-2up.pdf');
finally
Lib.Free;
end;
end;
對於垂直組合,StitchPagesVertically(PageRanges, Gap, TargetWidth) 會將整個頁面範圍堆疊成一個高聳的、海報樣式的頁面。PageRanges 採用標準範圍語法,例如 '1-3,5'。每個來源頁面都經過縮放以使其寬度符合 TargetWidth,因此每個頁面的高度貢獻是其自身高度乘以其自身的縮放係數,而輸出頁面的高度是這些高度的總和加上每對頁面之間的一個 Gap(間距)。這是您在處理連續捲動審查文件或由信件大小來源組合而成的長收據時所需的版面形式。
如何將一個頁面重疊到另一個頁面上?
StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) 會將一個現有頁面繪製到另一個現有頁面上,而不是建立一個新頁面。來源頁面會被擷取為 Form XObject,並以您指定的矩形繪製到目標頁面上,其 Opacity(不透明度)介於 0 到 1 之間(1 表示完全不透明)。寬度和高度是自由參數,因此如果您的比例與來源頁面自身的長寬比不同,放置的內容會被拉伸;當您需要來源頁面不失真的縮圖時,請傳入成比例的寬高值。典型的用途是將核准區塊作為獨立頁面維護的文件,並將其蓋印到封面。
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('report.pdf', '');
// Page 5 holds the approval block; place it on page 1
// in a 260 x 180 box at 45% opacity. Page 5 is consumed:
// it is captured and hidden, so pages 6..n become 5..n-1
if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
Lib.SaveToFile('report-approved.pdf');
finally
Lib.Free;
end;
end;
擷取後頁碼會發生什麼變化?
CapturePage 會消耗來源頁面:其內容會被移動到 XObject 中,而現在空白的頁面會被隱藏,因此頁碼大於來源頁面的每個頁面都會立即向下移動一位。這是整個拼貼 API 中最重要的一個語義。losLab PDF 函式庫會在每個拼貼呼叫中處理這個運算,StitchPageOverlay 在目標頁面位於來源頁面之上時會調整目標索引,而 StitchPagesSideBySide 會先擷取頁碼較高的頁面,以便第二次擷取的索引仍然有效,但您自己的周圍程式碼必須考慮到呼叫之間的這種位移。有兩個規則可以確保多重拼貼程式碼的正確性:在進行第一次擷取之前,先記錄您將需要的所有頁面大小或頁碼,加為它們在擷取後會發生變化;當您自己在迴圈中擷取多個頁面時,請以遞減的頁面順序進行反覆運算,這樣隱藏後面的頁面就永遠不會干擾您尚未擷取的前面頁面的索引。
如何將多圖層浮水印套用到頁面範圍?
PDFlibWatermark.pas 中的浮水印範本層將「定義一次,套用到多個頁面」的概念轉換為物件。TPDFlibWatermarkBase 承載了所有浮水印共用的屬性:九點 Position(wpTopLeft 到 wpBottomRight,加上使用原始 X/Y 座標的 wpCustom)、以度為單位的 Angle(角度)、介於 0 到 1 之間的 Opacity(不透明度),以及 PageRange 字串(空白代表所有頁面)。TPDFlibTextWatermark 增加了文字、字型 ID(預設為 Helvetica)、大小和 RGB 顏色;TPDFlibImageWatermark 增加了來自先前加入之影像的影像 ID,以及放置的寬度和高度。TPDFlibCompositeWatermark 持有子浮水印清單並以遞迴方式套用它們,而 TPDFlib.ApplyWatermark 則是單次呼叫的進入點,它會遵循範本自身的頁面範圍並傳回蓋印的頁面數。三圖層的法務浮水印程式碼如下所示。
var
Lib: TPDFlib;
Logo: TPDFlibImageWatermark;
Diagonal, Footer: TPDFlibTextWatermark;
Stack: TPDFlibCompositeWatermark;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('draft.pdf', '');
Stack := TPDFlibCompositeWatermark.Create;
try
Logo := TPDFlibImageWatermark.Create;
Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
Logo.Position := wpTopRight;
Logo.Width := 90;
Logo.Height := 32;
Stack.Add(Logo);
Diagonal := TPDFlibTextWatermark.Create;
Diagonal.Text := 'CONFIDENTIAL';
Diagonal.Position := wpCenter;
Diagonal.Angle := 45;
Diagonal.FontSize := 60;
Diagonal.Opacity := 0.15;
Stack.Add(Diagonal);
Footer := TPDFlibTextWatermark.Create;
Footer.Text := 'Internal review copy';
Footer.Position := wpBottomCenter;
Footer.FontSize := 9;
Footer.Opacity := 0.6;
Stack.Add(Footer);
Stack.PageRange := '1-3'; // children inherit this range
Lib.ApplyWatermark(Stack);
Lib.SaveToFile('draft-marked.pdf');
finally
Stack.Free; // composite frees its children
end;
finally
Lib.Free;
end;
end;
有兩個組合細節值得詳細說明。範圍繼承是單向且非破壞性的:具有空 PageRange 的子浮水印在 Apply 期間會暫時繼承複合浮水印的範圍,而設定了自己範圍的子浮水印則會保留自己的設定,因此您可以將頁尾固定到每個頁面,而對角線戳印僅覆蓋草稿區段。此外,所有權隨複合浮水印而定:TPDFlibCompositeWatermark.Destroy 會釋放其子浮水印,因此您只需釋放複合浮水印一次即可,無需釋放其他內容,如上例所示。
值得了解的界限
有兩個實際的限制,且兩者都源於該機制的工作方式。第一,擷取操作僅包裝頁面的內容資料流和資源。註解、連結、表單欄位和註釋是作為頁面字典中的獨立物件存在,而不是存在於內容資料流中,因此它們不會傳送到 XObject 中;拼貼後的版面配置會帶有可見的頁面內容,但不會帶有其來源頁面的互動層。第二,九點文字定位是一個近似值:在沒有完整版面配置引擎尊況下,函式庫無法知道字串的精確轉譯寬度,因此在解析 wpCenter 等位置時,它會將文字方塊估算為頁面寬度的 40%。這對於將短標籤置中已足夠精確,但對於長字串的像素級精確放置,請切換到 wpCustom 並自行計算座標。
縮放比例在這裡並不是一個實際的限制,因為擷取和拼貼是操作物件參考,而不是重新編碼頁面內容;如果您的來源檔案有數百 MB,在拼貼之前,適用於具有直接存取的大型檔案合併與分割指南中描述的相同原則。當您需要三個預建拼貼呼叫未涵蓋的版面配置時,此處展示的拼貼函式和浮水印範本類別會與較低階的 CapturePage、DrawCapturedPage 和 DrawCapturedPageMatrix 基本項目一起隨附在目前的 Delphi、C# 和 VB.NET losLab PDF 函式庫中。