在檔案的每一頁上蓋上浮水印或標誌看起來像是五分鐘的工作,直到您在檔案大小檢查器中開啟結果。顯而易見的方法是遍歷頁面,並在每一頁上再次建立相同的文字或影像物件。這在視覺上是可行的,但其浪費方式是加倍的。直接繪製在百頁報告上的對角線「DRAFT」浮水印是在內容資料流中存放的一百份相同路徑和文字資料的複本,且儲存的檔案攜帶了其中的每一份。
表單 XObject (Form XObject) 是 PDF 提供用來精確避免此情況的結構。它將一片可重複使用的內容(整個頁面或一個小範本)包裝到單個具名物件中,該物件可以在多個位置繪製多次。該內容在檔案中存在一次。每個需要該戳記的頁面都包含一條簡短的指令,指出「在此處繪製 XObject N,並帶有此轉換」。百頁的浮水印隨後向檔案新增一個內容物件,而不是一百個,這就是隨頁數線性增長的檔案與不隨頁數增長的檔案之間的區別。浮水印、標誌戳記、頁碼範本和印章都是同一個形式的問題,而表單 XObject 是適用於其中每一個問題的正確工具。
為什麼一個儲存的物件勝過一百次重新繪製
節省是結構性的,而非化妝性的。PDF 頁面藉由執行其內容資料流(一系列繪製運算子)來轉譯。當您在每頁重新繪製戳記時,您正在將該戳記的完整運算子序列附加到每頁的資料流中,且位元組會重複您擁有的頁數那麼多次。表單 XObject 將這些運算子移至檔案內儲存一次的單個資料流中。單個頁面保留的參照很小:它推送轉換矩陣、叫用 XObject 並還原狀態。頁數不再使美工圖案的成本翻倍。
當戳記很重時,這最重要。具有數百個路徑段的向量印章或標誌點陣圖儲存成本高昂。儲存一次並進行參照,重磅部分只需支付一次,且每頁的額外負荷只是幾個位元組的叫用。頁面上的視覺結果與直接重新繪製完全相同,這正是關鍵。閱讀器無法區分差異;檔案大小則非常可以。
將頁面擷取到 XObject 中
PDFium 從現有頁面建立可重複使用的物件。來源是您已開啟的某些檔案中的頁面、除了您的浮水印美工圖案之外什麼都不包含的一頁 PDF 小檔案,或者是較大檔案的特定頁面。CreateXObjectFromPage 將該來源頁面的內容擷取到屬於目標檔案(您蓋戳記的那個檔案)的可重複使用的控制代碼中。
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
特徵標記為 CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject。該方法在失敗時傳回 nil 而不是引發異常,因此上面的明確檢查不是選用的。傳回的控制代碼是您擁有的 TPdfXObject,且附加到其上的兩個生命週期限制是整個練習中會讓人絆倒的部分,因此它們在下方有其專屬的章節。
將戳記放置在頁面上
擷取的 XObject 本身不起作用。為了讓它出現,您可以使用 InsertFormObjectFromXObject 將其複本插入到檔案的目前頁面上。該呼叫傳回底層的頁面物件 FPDF_PAGEOBJECT,且傳回的控制代碼是您定位放置的方式。沒有轉換,戳記會落在來源頁面自身座標的原點處,這很少是您想要的位置。
因為 InsertFormObjectFromXObject 每次呼叫插入一個複本並每次都傳回一個新的頁面物件,所以您可以在同一個頁面上以不同的轉換繪製同一個 XObject 幾次,且儲存的內容在檔案中仍然只計算一次。角落標誌和微弱的全頁浮水印可以來自同一個擷取的物件。
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
一個所有權細節使清除安全進行。一旦插入,頁面物件就屬於頁面,而不屬於 XObject。稍後釋放 XObject 不會使您已經進行的放置失效。這就是讓下面描述的「建立-放置-釋放」順序運作的原因。
會影響使用者的控制代碼生命週期規則
兩個限制控管著 XObject 控制代碼,忽略其中任何一個都會產生看起來與其原因無關的失敗。首先,來源檔案在您呼叫 CreateXObjectFromPage 的瞬間必須是作用中的。擷取會從動態來源檔案中讀取來源頁面的內容,因此在建立控制代碼時,該檔案及其頁面必須是開啟且有效的。其次(這是令人驚訝的地方),控制代碼必須在關閉來源頁面之前釋放,在實作中,要在關閉或釋放它所來自的來源檔案之前釋放。
原因在於 XObject 是對來源檔案仍然擁有的結構的參照。它不是在來源消失後您可以隨身攜帶的分離、自含複本。先關閉來源,控制代碼就會被留在指向已被拆除的內容,因此稍後釋放它或對其進行任何其他使用都會在不再有效的記憶體上操作。其症狀是懸置控制代碼的典型症狀:關閉時發生存取違規,或者間歇性損壞(根據分配順序而變動),且堆疊指向清除程式碼,而不是實際引起問題的程式碼行。修正方法是排序,而不是防禦性編碼。建立 XObject、將其插入到需要它的每個頁面上、釋放 XObject,然後才關閉來源檔案。TPdfXObject 解構子會為您釋放底層的 PDFium 控制代碼,因此在正確的時間釋放包裝器是您的全部職責。
矩形,以及其六個數字的意義
放置是 2D 仿射轉換,與 PDF 隨處用於定位內容的轉換相同(ISO 32000-1,第 8.3.4 節)。它是寫作 a, b, c, d, e, f 的六個數字,且 PDFium 將它們公開為 FS_MATRIX 記錄。它們將物件自身空間中的點對應到頁面空間:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
您可以手動填入這六個值,但手動撰寫它們是旋轉出錯的地方,因為旋轉將 a, b, c, d 所有四個混合在一起。TPdfMatrix 包裝器為您撰寫常見的操作並在執行時進行後乘,因此 Translate、Scale 和 Rotate 按您呼叫它們的順序鏈結。對角線浮水印是旋轉,接著是平移以使其重新置中;角落標誌是縮放,接著是平移。當矩形準備就緒時,將其原始值傳遞給 FPDFPageObj_SetMatrix(PageObj, M.Handle),其中 M.Handle is the underlying FS_MATRIX. 當您寧願傳遞數字而不是建立包裝器時,可以使用更低層級的 FPDFPageObj_Transform(它直接將六個值作為雙精確度浮點數接受)。
按正確順序蓋戳記於每一頁
完整的模式將這些部分與生命週期規則要求的順序組合在一起。開啟這兩個檔案、擷取一次戳記、遍歷目標頁面並依次選擇每個頁面,然後插入並定位複本,接著釋放 XObject,然後儲存,最後讓來源檔案關閉。
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
try 區段的形狀正在執行真正的工作。內部的 finally 在控制權到達釋放 Stamp 的外部 finally 之前釋放 XObject,因此即使在迴圈中途觸發異常,控制代碼也始終在其來源仍然處於活動狀態時釋放。弄對該巢狀結構,生命週期規則就會自行處理。(使用您的組建公開的任何目前頁面選取器;迴圈主體在兩種方式下都是相同的。)
蓋戳記是建立和編輯頁面內容的更大工具包中的一個角落。如果您的戳記本身是影像而不是擷取的頁面,使用 PDFium 將影像轉換為 PDF 檔案會先介紹如何將該點陣圖放入檔案中。而當您想要與可見戳記一起攜帶的是檔案而不是頁面上的墨水時,在 Delphi 中使用 PDF 附件會展示嵌入式檔案端。所有這些都隨附於適用於 Delphi 和 C++Builder 的 PDFium 元件,同時也提供本部落格其他地方介紹的轉譯、編輯和檔案 API。