PDFium 元件為您提供了一個用於分割 PDF 的方法:ImportPages;其他的一切操作,不論您是隔離單一頁面、在任意邊界上進行剪切,還是遵循文件本身的書籤結構,都只是決定哪些頁碼進入每個輸出檔案的不同方式;其實作機制完全相同;儘早理解這一點可以避免許多錯誤的嘗試
分割迴圈如何運作
不論您如何分割來源文件,其模式都是相同的;建立一個全新的 TPdf 執行個體、呼叫其 CreateDocument 以在記憶體中初始化一個空的 PDF、使用 ImportPages 匯入您想要的頁面、儲存結果,然後在下一次迭代之前將 Active 重設為 False;最後一個步驟是人們容易遺漏的:CreateDocument 並不會隱含地關閉仍在記憶體中的文件,因此在再次呼叫它之前,您必須明確地儲存您的輸出並重設 Active := False;先進行重設可以保持狀態的乾淨與定義明確;外層的 TPdf 執行個體在所有迭代中都會被重複使用,這能在大批量工作中保持較低的分配壓力
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
ImportPages 的 Range 參數是 PDFium 內部使用的相同字串格式:以逗號分隔的頁碼清單或以連字號分隔的範圍,皆以 1 為基準;'3' 匯入第 3 頁,'1-5' 依序匯入第 1 到第 5 頁,'2,5,8' 則匯入這三個頁面;第三個參數是目標文件中以 1 為基準的插入位置,傳遞 1 始終會將匯入的頁面放在原本為空的文件開頭,這正是您在此處所需要的
依據頁面範圍進行分割
當呼叫者提供像 1-12,13-24,25-36 這樣的清單時,您將其解析為起始/結束對並執行相同的迴圈,根據每一對建置範圍字串:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
在呼叫 ImportPages 之前進行驗證非常重要;當範圍字串中的頁碼超過 Source.PageCount 時,ImportPages 會傳回 False,但它不會引發異常,也不會產生您可以單憑名稱偵測到的部分輸出檔案;請檢查 SaveAs 的傳回值並單獨記錄失敗情況,在有人開啟該檔案之前,產生空輸出檔案的範圍並非顯而易見的錯誤
在書籤邊界進行分割
第三種方法使用文件本身的結構,而不是外部提供的清單;每個頂層書籤都帶有一個目標頁碼,它所定義的章節從該頁面開始,到下一個書籤頁面的前一頁為止,或者對於最後一個項目,則執行到文件的結尾
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // skip a malformed section instead of writing an empty file
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
沒有書籤的文件並不是一個需要向使用者呈現的錯誤情況,這只代表此分割模式沒有可供運作的依據;Length(Bm) = 0 保護檢查會靜默地處理該情況;值得呈現的情況是,書籤的頁碼超出了文件的範圍,這通常發生在格式錯誤的檔案中,即頁面被刪除後大綱卻從未更新;對 StartPage 和 EndPage 進行的界限檢查會跳過這些項目,而不是將垃圾範圍傳遞給 ImportPages
輸出檔案命名與 Active 重設
書籤標題可能包含在 PDF 字串中有效但但在檔案系統路徑中無效的字元;在建置輸出路徑之前,至少要替換斜線、反斜線和冒號;在 Windows 上,*、?、"、<、> 和 | 也是被禁止的,只需在固定集合上進行簡單的迴圈即可處理它們,無需引入規則運算式
每次迭代結束時的 Active := False 這一行非常需要強調,因為它是該模式中唯一不明顯的要求;CreateDocument 並不會隱含地關閉已開啟的內容;如果在再次執行 CreateDocument 時 Active 仍然為 True,則仍在記憶體中的文件從未被正確關閉或儲存,您將無法在該狀態下依賴定義明確的行為,因此請在開始下一個文件之前明確地儲存並重設;請將其視為 try/finally 的配對:finally 區塊釋放外層物件,而 Active := False 則在迴圈迭代之間重設內部文件狀態
使用此方法在大型分割工作中的記憶體使用量將會保持平穩,開於您在記憶體中絕不會同時持有超過一個輸出文件;來源文件在整個過程中都保持開啟且唯讀,ImportPages 會將頁面資料複製到新文件中,而不會修改來源;如果來源檔案已加密,請在迴圈前使用其密碼開啟它,這樣每個輸出檔案中複製的頁面都將是未加密的,這對於發送給不同收件者的分割輸出通常是正確的行為
關於 SaveAs 還有一點:它會傳回 Boolean;不存在的輸出目錄、包含作業系統拒絕之字元的路徑,或是磁碟空間不足的狀況,都會導致 SaveAs 傳回 False 而不引發異常;在將 200 頁的文件分割為 200 個單頁檔案的批次工作中,第 147 頁的靜默失敗很容易被忽略;請檢查每次呼叫的傳回值,並在迴圈結束時與預期的總數進行比對
此處顯示的 ImportPages 和 CreateDocument 方法是適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分