PDF 文字擷取看似簡單,直到您遇到文字層不存在、損毀,或是被拆分到數十個毫無意義順序的微小字元組的文檔為止;PDFium 元件為您提供兩個進入點:Character[] 陣列用於對頁面上的每個字形進行基於索引的原始存取,而 ReadablePageContent 則用於結構化檢視,從 PDF 的標籤樹或啟發式分析中重建段落和標題;兩者並非總是唯一的正確選擇,因此了解每個方法所呈現的內容至關重要
開啟文件與靜默失敗陷阱
TPdf 藉由設定 FileName 並切換到 Active := True 來開啟檔案;關鍵的細節在於:Active := True 絕不會引發異常;如果檔案遺失、受密碼保護或損毀,PDFium 會在內部處理該錯誤,而 Active 只會保持為 False;這代表每個擷取迴圈都必須對此進行防護:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
受密碼保護的檔案需要在 Active := True 之前設定 Pdf.Password := '...';此時沒有第二次機會:一旦 Active 失敗,您就必須關閉並使用正確的密碼重新開啟
使用 Character[] 進行逐頁擷取
最低階的方法會走過每個頁面上的每個字元;設定 Pdf.PageNumber 以載入該頁面的文字層,然後使用 Character[] 屬性反覆檢視 CharacterCount 項目;每個項目上有兩個旗標值得檢查:CharacterGenerated[i] 標記由轉譯器插入的合成字形(例如換行處的軟連字號),這些字形沒有實際的 Unicode 值;而 CharacterMapError[i] 則發出訊號,表示 PDFium 無法將字形對應到字碼點,這通常發生在缺乏 ToUnicode 表的字型編碼中
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
其傳回結果是依據 PDFium 列舉順序排列的 Unicode 字碼點平面字串,這正是它們在內容串流中出現的順序,而不一定是從左到右的閱讀順序;對於大多數由標準辦公工具產生的拉丁文字文件,這樣做是沒有問題的;對於使用異常字形序列進行 OCR 處理的掃描 PDF,或是從右到左的文字,順序可能會出錯;此時 ReadablePageContent 就會變得更加實用
使用 ReadablePageContent 進行結構化擷取
ReadablePageContent 提升了一個層級:它傳回一個 TPdfReadableContent 記錄,其 Fragments 陣列帶有標記的內容片段,每個片段都有一個 Kind,用於識別段落、標頭、清單項目、表格儲存格等;當 PDF 帶有結構樹時(檢查 Pdf.IsTagged),來源為 rosStructure,且閱讀順序是具有權威性的;對於未標記的檔案,PDFium 會退而求其次使用 rosHeuristic,它會根據字元的邊框將其分組為合理的閱讀單元,但無法保證準確性
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
如果 Content.Source = rosHeuristic 且您的輸出看起來是亂碼,那文件的文字層在編寫時可能沒有考慮到閱讀順序;此時,唯一可靠的解決方法是從來源應用程式中以正確的標記重新匯出,或是執行一個按照 Y 軸再按 X 軸對字元原點進行排序的後處理步驟
CharacterOrigin 与 CharacterRectangle 提供的資訊
這兩個屬性都會傳回頁面空間中字元的位置(點,原點在左下角,Y 軸向上遞增);CharacterOrigin[i] 是字形的基準線錨點,CharacterRectangle[i] 是完整的邊框;這些是純文字之外任何應用的建置區塊:偵測欄邊界、透過比較容許度內的 Y 座標將字元分組為行,或是建置檢視器中用於文字選取的點擊測試對應表;如果您需要找出滑鼠點擊下是哪個字元,CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) 可以直接執行該查閱,而無需您重複走過各個矩形
置放 DLL 檔案
PDFium 元件將所有 PDF 解析委派給原生 DLL,根據您的目標平台,可能是 pdfium32.dll 或 pdfium64.dll;該元件隨附一個 CopyDlls.bat 指令碼,可將正確的檔案複製到 Windows 系統目錄中;在開發電腦上以系統管理員身分執行一次就足夠了;對於部署,您需要將 DLL 複製到應用程式可執行檔的旁邊;啟用 V8 的變體(pdfium32v8.dll、pdfium64v8.dll)體積明顯較大,且僅在您的 PDF 包含必須執行的 JavaScript 時才需要;對於純文字擷取,標準版本才是正確的選擇
如果執行階段缺少 DLL,Active := True 將會靜默失敗,就像檔案遺失一樣,因為元件會在內部擷取載入錯誤;在出貨前請務必在乾淨的電腦上進行測試
配合 Character[] 使用 FontSize[] 進行版面分析
除了純文字之外,字元層級的 API 還公開了 FontSize[i],它會傳回每個字形轉譯後的點大小;結合 CharacterOrigin[i] 和 CharacterRectangle[i],這可以讓您在不依賴結構樹的情況下,區分內文與標題;在未標記的文件中,字型大小跳到閾值以上的字元組幾乎可以確定是標題;相同的技術也適用於偵測圖說文字(影像邊框下方的小字)或頁尾註腳(靠近頁面底部的小字);這一切都不需要轉譯,所有三個屬性都是直接從 PDFium 在 Active := True 期間建立的文字層中讀取
一個細微差別:FontSize[i] 反映了套用頁面 CTM(當前轉換矩陣)之後的大小,因此如果作者縮放了整個頁面,文件會回報等比例調整後的大小;如果您在具有不同頁面尺寸的頁面之間比較大小,在做出閾值決策之前,請先針對每個頁面的 MediaBox 高度進行標準化
將輸出寫入檔案
自 XE 版本以來,Delphi 的 TStringList 就可以乾淨地處理 UTF-8 輸出;如果您需要無 BOM 的檔案,請設定 WriteBOM := False(許多下游使用者會因為前導 BOM 而無法運作):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
對於記憶體吃緊的極大型文件,請在頁面迴圈內使用 TEncoding.UTF8 直接寫入 TStreamWriter,而不是先將所有內容累積到清單中
此處顯示的 Character[]、CharacterCount、CharacterOrigin[]、CharacterRectangle[]、ReadablePageContent 和 CharacterIndexAtPos API 是適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分