PDF 將影像作為一等物件儲存在其內容串流中;當頁面引用照片、掃描影像或圖表時,像素資料會與頁面幾何形狀一起保存在 XObject 字典中;PDFium 元件透過 TPdf 上的兩個屬性來呈現:BitmapCount 會傳回當前頁面上有多少個內嵌點陣圖,而 Bitmap[Index] 則將其中一個解碼為您所擁有且必須釋放的 TBitmap;這就是整個擷取模型;迴圈只需四行,需要判斷的是周圍的配置
開啟文件
關於 TPdf 首先需要了解的是,Active := True 絕不會引發異常;載入失敗、密碼錯誤、損毀的檔案:所有這些都會在內部被吞掉,元件只會保持未作用狀態;您必須在指派後自行檢查旗標,否則您將進入 PageCount 傳回零的頁面迴圈,並疑惑為什麼沒有擷取到任何內容
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
Writeln('Failed to open: ', Pdf.FileName);
Exit;
end;
Writeln(Pdf.PageCount, ' pages');
// proceed to extraction
finally
Pdf.Free;
end;
end;
受密碼保護的檔案也遵循相同的模式:在設定 Active := True 之前指派 Pdf.Password;如果密碼錯誤,Active 會保持為 False,您不會收到任何可擷取的異常;在處理數百個檔案的批次工具中,這種靜默行為實際上非常有用:您可以將失敗記錄在清單中,而不是為每個檔案重整呼叫堆疊
反覆檢視頁面並擷取點陣圖
BitmapCount 是針對每個頁面的,因此您要在讀取它之前設定 Pdf.PageNumber;頁碼是從 1 開始的,預設值為 0,代表未載入頁面;Bitmap[Index] 屬性是從 0 開始的,並傳回呼叫者擁有的 TBitmap;您必須釋放它;如果在處理大型文件的大型迴圈中忽略了釋放,記憶體會快速攀升,因為在進行任何壓縮之前,每個點陣圖都可能是數 MB 的原始像素資料
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
Page, Idx: Integer;
Bmp: TBitmap;
OutPath: string;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
for Idx := 0 to Pdf.BitmapCount - 1 do
begin
Bmp := Pdf.Bitmap[Idx];
if not Assigned(Bmp) then
Continue;
try
OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
Bmp.SaveToFile(OutPath);
finally
Bmp.Free;
end;
end;
end;
end;
Assigned 的保護檢查至關重要;少數 PDF 產生器會寫入像素尺寸為零或格式錯誤的影像 XObject,在這些情況下,元件會傳回 nil 而不是空點陣圖;將傳回 nil 視為錯誤並停止擷取是錯誤的直覺反應:請跳過它,如果需要稽核軌跡,請記錄頁面和索引,然後繼續執行;頁面的其餘部分可能仍然會產生有效的影像
請注意,外層迴圈在每次迭代中都會設定 Pdf.PageNumber;該指派動作會將頁面載入到元件的內部狀態中,並使 BitmapCount 具有意義;如果跳過它,您將重複讀取同一個頁面的計數;在編寫時,這個模式感覺有些多餘,但這正是 API 的設計方式:頁面是一個游標,而不是一個集合
選擇輸出格式
BMP 是無損的,且在沒有其他單元的情況下始終可用,這使得它在您還不知道影像包含什麼內容時,成為一個可靠的預設選擇;當檔案大小很重要時,傳回的 TBitmap 像素格式會告訴您哪種轉碼器是合適的;32 位元點陣圖帶有 Alpha 頻道,PNG 可以無損地保留它;具有連續色調的大型 24 位元影像適合使用 JPEG;較小的影像或使用有限調色盤繪製的影像通常最好保持為 BMP,而不是透過 JPEG 處理,因為 JPEG 在低品質設定下會加入區塊偽影,而在高品質設定下省下的空間又微乎其微
procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
Jpg: TJPEGImage;
begin
case UpperCase(ExtractFileExt(FileName)) of
'.JPG', '.JPEG':
begin
Jpg := TJPEGImage.Create;
try
Jpg.Assign(Bmp);
Jpg.CompressionQuality := 85;
Jpg.SaveToFile(FileName);
finally
Jpg.Free;
end;
end;
else
Bmp.SaveToFile(FileName); // BMP: lossless, no extra units
end;
end;
在實務上,格式選擇是由 Bmp.PixelFormat 和尺寸決定的;如果 PixelFormat = pf32bit,您需要一個帶有 Alpha 的格式,PNG 是顯而易見的選擇,但在舊版的 Delphi 中需要 PNGImage 單元;對於寬度大於約 300 像素的 24 位元影像,品質 85 的 JPEG 與 BMP 相比可減少三倍的大小,且在大多數照片內容中無感知上的損失;低於該閾值時,BMP 的大小與其相當,且能完全避免任何品質上的抉擇
BitmapCount 計算與不計算的內容
PDF 區分了影像 XObject 和使用路徑運算子繪製的向量圖形;如果每個元素都是向量,那麼視覺上看起來複雜的頁面也可能會傳回零的 BitmapCount;掃描頁面幾乎總是傳回剛好一個:掃描器會將整個掃描影像寫入為單一的整頁影像 XObject,不論掃描器設定為多少解析度;將排版文字與內嵌照片混合的頁面會為每張照片傳回一個項目;裝飾性的線條、陰影背景和表格邊框通常根本不會出現在點陣圖計數中
該計數也不包含行內影像,這是一種很少使用的 PDF 結構,其中影像資料直接內嵌在頁面內容串流中,而不是作為具名的 XObject;這些超出了此 API 所呈現的範圍,它們在真實文件中非常少見,因此大多數擷取工具根本不處理它們
值得記住的一個細節:您讀取的 BitmapCount 是截至最後一次 PageNumber 指派時的當前頁面;如果您的程式碼在計算和取得之間分支或呼叫任何會變更 PageNumber 的函式,您讀取的影像數量可能會少於您分配的空間,或者索引超出末尾;請將計數讀取和 Bitmap[] 迴圈保持在同一個頁面上,期間不要接觸 PageNumber
在表單應用程式中使用 TPdfView
TPdfView 元件公開了相同的 BitmapCount 和 Bitmap[] 屬性,但它讀取的頁面是檢視器目前顯示的頁面,而不是 TPdf.PageNumber;這兩個頁面指標是獨立的,設定一個不會移動另一個;在具有即時檢視器的 VCL 表單應用程式中,您可以呼叫 Pdf.PageNumber := N 來透過 TPdf 驅動擷取,而檢視器則保持在使用者上次捲動到的任何位置;這種分離是有意設計的,可以在背景擷取執行時,保持檢視器的顯示狀態乾淨
批次工作中的記憶體與效能
在處理大型封存檔案時,記憶體預算是最需要注意的事項;每次 Bitmap[] 呼叫都會在堆積上分配一個新的 TBitmap,而在 300 DPI 的掃描頁面上,在進行任何編碼之前很容易達到 25 MB 的原始像素資料;如果您在緊密迴圈中處理頁面而不在迭代之間釋放,工作集會隨影像數量呈線性成長;正確的架構始終是:取得一個點陣圖、執行您需要的操作、釋放它,然後取得下一個;如果您需要為了比較步驟而同時持有數個點陣圖的參照,請先使用 BitmapCount 計算其數量並相應地分配您的容器,然後在完成時立即釋放每個點陣圖,而不是延遲到文件結尾的清理工作;在一份有 500 頁掃描頁面的文件中,這種區別可能代表著 25 MB 與 12 GB 峰值 RSS 之間的巨大差異
此處顯示的 BitmapCount 和 Bitmap[] 屬性是適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分