合併和分割是每個人最先接觸的兩個頁面操作,且它們涵蓋了很大的範圍;但它們並非涵蓋一切;還有一個獨立的工作系列是重新排列頁面,而不是移動整個檔案:將四張投影片放到一張紙上作為講義、將頁面從檔案的背面拖到前面,或者在不觸及其他內容的情況下將第 3、7 和 12 頁提取到一個簡短的精選中;PDFium 為此提供了三種方法,且每一種方法的行為都與您已經知道的合併和分割不同;本文將引導您了解它們做些什麼、輸出點位在哪裡,以及一個在實際工作環境中曾導致崩潰的所有權細節
這三者是:用於多頁合一(N-up)拼版的 ImportNPagesToOne、用於就地重排的 MovePages,以及用於子集提取的 ImportPagesByIndex;合併將檔案端到端地堆疊,並使頁數等於輸入的總和;分割從一個輸入寫入多個輸出檔案;此處的三個操作介於兩者之間:其中一個改變了共用一張紙的來源頁面數量,一個改變了單個檔案內部的順序,另一個則將選定的一小部分頁面複製到另一個檔案中;知道哪一個是哪一個,可以避免您在單個呼叫即可完成時,強行進行合併後刪除的繁瑣操作
多頁合一拼版實際做的工作
拼版是印前術語,指的是將數個來源頁面安排到一張更大的紙上,以便列印和摺疊後的結果以正確的順序呈現;日常版本是雙面講義、4 頁小冊子簽名,或者在一頁上放置十幾個縮圖的聯絡清單;PDFium 透過單個呼叫處理幾何配置:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX 和 NumY 描述了網格;2, 1 的值將兩個來源頁面並排放置;2, 2 將四個打包成象限配置;4, 3 建置一個 12 頁合一的聯絡清單;PDFium 按順序讀取來源頁面,將每一頁縮小以適應其單元格,並從左到右、從上到下填滿網格,每當目前網格填滿時就開始一張新的輸出頁面;來源頁面不會被修改;您拿回的是一個頁面為複合頁面的新檔案
輸出大小是以點為單位,而不是像素
OutputWidth and OutputHeight 是 PDF 使用者單位,一個 PDF 使用者單位是一點,即 1/72 英吋;該單位宣告了輸出紙張的物理大小,它與螢幕像素或轉譯 DPI 無關;這是最容易把拼版弄錯的單一地方,因為習慣使用點陣圖的開發人員會去獲取像素計數,最終得到一張郵票或看板大小的紙張
值得記住的數字是您最常使用的兩種頁面大小;US Letter 是 612 乘 792 點,因為 8.5 英吋乘以 72 是 612,11 英吋乘以 72 是 792;A4 大約是 595 乘 842 點(來自其 210 乘 297 公釐的尺寸);繫結自身的標頭清楚地說明了規則,即一個單位是 1/72 英吋,如果您更願意在程式碼中從英吋計算大小而不是寫入字面量,該單位會出貨一個等於 72 的 PointsPerInch 常數
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
傳回的控制代碼是您的責任去釋放
請再次閱讀特徵標記;ImportNPagesToOne 傳回 TPdf,而不是布林值;該傳回值是一個全新的檔案控制代碼,獨立於來源進行配置,且呼叫者擁有它;您在其上呼叫該方法的來源 TPdf 未受影響且仍擁有其自身的控制代碼;複合檔案是第二個、獨立的物件;如果您讓傳回的 TPdf 在沒有釋放的情況下超出範圍,您就會洩漏整個 PDFium 檔案
更危險的錯誤方向相反;在底層,該方法透過 FPDF_ImportNPagesToOne 向 PDFium 要求一個新的 FPDF_DOCUMENT,然後將該原始控制代碼包裝在傳回的 TPdf 中,以便包裝器的生命週期引導控制代碼的生命週期;從那時起,控制代碼只有一個所有者,且只有一個地方應該被關閉:當您 Free 傳回的物件時;一條粗心的錯誤路徑既釋放了包裝器,又在其擷取的原始控制代碼上呼叫 FPDF_CloseDocument,這會關閉相同的 PDFium 檔案兩次;這是一個重複釋放,且是此處曾影響過呼叫者的特定錯誤;防止它的規則很短:僅在一條路徑上關閉檔案,即釋放該方法交給您的 TPdf,且絕不要越過包裝器去關閉它已經採用的控制代碼
由此得出兩個規則;第一,當 PDFium 拒絕引數(例如網格軸上的零或配置失敗)時,該方法會傳回 nil,因此在您觸及結果之前應進行 nil 檢查;第二,在 try 之前將您的輸出變數初始化為 nil 並在 finally 中釋放它,如上面的範例所示,這樣中途的失敗就不會讓您釋放未定義的參照或完全跳過釋放
在不重寫頁面的情況下重新排列它們
拼版會建置新檔案;重排則就地變更一個檔案;MovePages 將一組頁面從其目前位置移出並將其丟置到目標位置,在被移動的區塊周圍轉移其他所有內容,以便頁數保持不變:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
索引是從零開始的;PageIndices 列出要移動的頁面(按它們應該結束的順序),而 DestPageIndex 是第一個移動的頁面在移動確定後降落的索引;因為 PDFium 是重新定位頁面而不是複製並重新壓縮其內容,所以該操作是廉價且無損的:頁面物件保留其串流、其資源和其保真度;這是拖曳重排頁面面板背後的呼叫,使用者在其中將縮圖拉到新位置,您用一次移動提交新順序;當索引超出範圍時它會傳回 False,因此請驗證結果而不是假設重排已生效
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
透過索引提取子集
第三個操作將一組明確的頁面從一個檔案複製到另一個檔案中;ImportPagesByIndex 獲取來源檔案和從零開始的索引陣列,並將這些頁面插入到目標位置中的選定位置:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
您在目標檔案上呼叫它並將來源作為第一個引數傳遞;PageIndices 命名要提取的來源頁面(按您想要的順序);InsertAt 是目標中第一個匯入頁面放置的從零開始的插槽,因此 0 將它們放在現有第一頁之前,且目標目前頁數會附加在後;空陣列會匯入每一頁,這使得呼叫在您需要時成為完整複製;如果來源中的任何索引超出範圍,它會傳回 False
這就是與分割的對比發趣用處的地方;分割寫入獨立的檔案,一次操作在磁碟上產生多個輸出;ImportPagesByIndex 進行相反形式的工作:它將一組選定的頁面收集到記憶體中的單個目標檔案中,然後您只需儲存一次;當工作是「給我第 3、7 和 12 頁作為一個簡短的 PDF」時,這是直接的路徑,並在底層包裝了 FPDF_ImportPagesByIndex
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
乾淨地將它們結合在一起
這三者的端到端形狀是相同的:透過設定 FileName 並將 Active 切換為 True 來開啟來源、執行操作、使用 SaveAs 儲存,並釋放您所擁有的內容;唯一需要小心的分支是哪些呼叫會配置新檔案;MovePages 會變更您已持有的檔案,因此只有一個物件需要釋放;ImportPagesByIndex 寫入您自己建立的目標,因此您釋放來源和您開啟的目標;ImportNPagesToOne 是特例,因為新檔案是方法的傳回值,而不是您建構的內容,忘記它是獨立的、呼叫端擁有的控制代碼,正是洩漏和重複釋放發生的原因;將結果初始化為 nil,在呼叫後檢查它,並在單一路徑上釋放它
如果您實際的工作是合併整個檔案而不是重新排列頁面,請參閱將多個 PDF 檔案合併為一個檔案;如果是相反的工作,即將一個檔案拆分為多個檔案,請參閱將 PDF 檔案分割為多個檔案;此處所述的拼版和重排方法作為 Delphi 和 C++Builder 的 PDFium 元件的一部分出貨,並與本部落格其他地方介紹的載入、轉譯和編輯 API 搭配