PDFlibPas 能在不使用 Office 自動化的情況下,把 PDF 內容轉換成兩種可編輯格式。ExportPageMarkdown 與 ExportDocumentMarkdown 會回傳含有推斷標題、有序與無序清單以及管線表格的語意化 Markdown;SaveDOCXToFile 與 SaveDOCXToStream 則會寫出一個 WordprocessingML 套件,內含段落、標題、原生清單編號、偵測到的表格、字型樣式、分頁與已定位的 PNG 圖片
兩者完全以 Pascal 執行,可在伺服器上運作,不需要安裝 Word,也不需要 COM。正是這項限制,讓這項功能出現在 PDF 函式庫中,而不是出現在桌面工具裡
為什麼「PDF 轉 Word」真的很難?
因為 PDF 頁面本身並不包含段落。它包含的是文字顯示運算子,把一串串字型符號放置在座標上,順序完全依照產生器當初寫入的方式,沒有任何義務標示兩個文字串是否屬於同一個句子,更遑論同一個清單項目。這個格式設計的初衷是精確描述印刷頁面的樣貌,而它之所以能做到這一點,正是因為捨棄了產生這個頁面的結構資訊
所以每一套轉換器都得重新推斷產生器當初丟棄的資訊。行的分組來自垂直間距與基線對齊。段落邊界來自間距變化與縮排。標題是一行字型比正文更大或更粗、且與後續內容明顯區隔的文字。清單是一連串以項目符號或編號模式開頭的段落。表格則是文字區塊排列成一個網格,其邊界在各列各欄之間對齊。以上每一項都是推斷,而推斷意味著在遵循一般排版慣例的文件上會有不錯的結果,在不遵循的文件上結果就普通
已標記的 PDF 是例外,而且是很大的例外。當文件本身帶有結構樹時,段落、標題、清單與表格的角色都是被記錄下來的,而不是被猜出來的,這也是為什麼 已標記 PDF 無障礙結構 中所描述的工作,在轉換品質上同樣有回報。如果你能掌控產生端,為輸出加上標記,就是你能為日後需要轉換它的人做的、最具槓桿效果的一件事
Markdown 匯出,一次一頁
當目的地是文字管線時,例如文件站台、搜尋索引,或供助理使用的檢索語料庫,Markdown 路徑就是該選用的方式。選項是一個位元遮罩:PDF_MARKDOWN_INCLUDE_PAGE_MARKERS、PDF_MARKDOWN_DETECT_HEADINGS、PDF_MARKDOWN_PRESERVE_STYLES,而 PDF_MARKDOWN_DEFAULT 則會結合這三者
var
Pdf: TPDFlib;
Md: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.LoadFromFile('handbook.pdf', '');
// 單一頁面,以字串形式取得
Md := Pdf.ExportPageMarkdown(1, PDF_MARKDOWN_DEFAULT);
// 一段頁面範圍,以不含 BOM 的 UTF-8 串流寫入磁碟
Pdf.SaveMarkdownToFile('1-40',
PDF_MARKDOWN_DETECT_HEADINGS or PDF_MARKDOWN_PRESERVE_STYLES,
'handbook.md');
finally
Pdf.Free;
end;
end;
頁面標記在檢索工作中特別有價值。一段帶有出處頁碼的文字片段可以被精確引用,讓跟隨引用連結的讀者直接抵達內容實際所在之處。若 Markdown 是給人閱讀的,則應該關閉頁面標記,因為來自原始版面的分頁資訊此時只是雜訊
串流式進入點對大型文件而言很重要。SaveMarkdownToStream 與 SaveMarkdownToFile 會一次一頁地寫入 UTF-8,不會把完整輸出先緩衝起來,因此一份 900 頁的手冊不會先在記憶體中變成一個 900 頁份量的字串。刻意不寫入位元組順序標記(BOM)同樣是有意為之:Markdown 檔案帶有 BOM,會讓相當多靜態網站產生器與差異比對工具感到困惑
不需要機器上安裝 Office 的 DOCX
DOCX 寫入器會自行產生整個套件:以原始 Deflate 演算法寫入並附帶 CRC 檢查的 ZIP 項目、WordprocessingML 各部件,以及把它們串連起來的關聯關係。整個過程完全不呼叫 Word,這代表轉換可以在無圖形介面的伺服器上執行、在服務帳號下執行、在容器中執行,也就是所有 Office 自動化被禁止使用、不穩定或未取得授權的場合
var
Pdf: TPDFlib;
Target: TFileStream;
begin
Pdf := TPDFlib.Create;
Target := TFileStream.Create('handbook.docx', fmCreate);
try
Pdf.LoadFromFile('handbook.pdf', '');
Pdf.SaveDOCXToStream('1-40',
PDF_DOCX_INCLUDE_IMAGES or PDF_DOCX_DETECT_HEADINGS or
PDF_DOCX_PRESERVE_STYLES or PDF_DOCX_PRESERVE_PAGE_BREAKS,
Target);
finally
Target.Free;
Pdf.Free;
end;
end;
圖片資料是在處理每一頁時就寫出,而不是先收集起來、最後才一次附加,因此記憶體用量的高峰只會與單一頁面相當,而不是整份文件。明確的頁面順序會被保留,匯出結束後也會還原原本選取的 PDF 頁面,這在整段轉換只是一項更長工作流程中的一個步驟、而該頁面選取另有他用時,就顯得格外重要
確定性封裝能帶來什麼好處?
逐位元組完全一致的可重現性。相同輸入搭配相同選項執行兩次轉換,會產生完全相同的套件,這代表你可以對輸出計算雜湊來偵測變化、比對兩次產生文件的差異,並可以放心地大量快取,不必擔心相同輸入產生出不同的產物
Office 自動化無法保證這一點。它會內嵌時間戳記、修訂識別碼與依機器而異的中繼資料,因此同一份文件轉換兩次,結果卻以會讓雜湊比對失效的方式產生差異。可重現建置的確定性 PDF ID 中討論的正是同一套推理:當輸出可重現時,驗證就從「檢查」變成了「比對」
輸出品質好壞的分界在哪裡
對使用者誠實說明這一點很重要,因為轉換品質的變動主要取決於輸入內容,而不是轉換器本身。已標記的 PDF 與乾淨產生的商業文件,例如發票、報表、合約,轉換效果良好:標題會被還原成標題,表格得以保留,清單在 Word 中也能正確重新編號。若欄位幾何規則的話,雙欄學術版面轉換結果也算可以接受。跨頁的表格是靠推斷重新組裝的,有時會被拆散。以視覺效果而非閱讀順序排列文字的重度設計行銷素材,轉換效果就很差,再多的推斷也彌補不了
掃描文件則完全是另一回事。一整頁如果只是一張圖片,就不含任何文字物件,在文字圖層存在之前根本沒有東西可以匯出;產生文字圖層的 OCR 流程是先決條件,而不是可選項。在執行大批次轉換之前,先抽樣十幾份具代表性的檔案看看輸出結果,並可以考慮先如 文字搜尋與頁面元素列舉 中所述枚舉頁面元素,看看這些頁面實際上包含了什麼內容
對於助理與檢索管線而言,Markdown 路徑通常是更好的目標:標題可以成為區塊邊界,表格能以管線表格的形式保持可讀,頁面標記則讓每個區塊都有可供引用的位置。若目的是給人編輯,答案就是 DOCX,因為使用者要的不是文字本身,而是修改它的能力
PDFlibPas 是一套適用於 Delphi、C++Builder 與 Lazarus 的 PDF 函式庫,並提供對應的 DLL 與 ActiveX 介面,因此相同的匯出呼叫也能供 C#、C++ 或指令碼主機使用。完整文件與試用版請參見 PDFlibPas Delphi PDF 函式庫頁面