現(xiàn)pdg批量轉(zhuǎn)PDF:從解析到合成的完整方案)
1. 項(xiàng)目背景與需求拆解1.1 pdg文件到底是個(gè)什么來頭先說清楚pdg是什么。pdg格式是早期數(shù)字圖書館系統(tǒng)里常見的一種掃描書格式很多老書、絕版書、內(nèi)部資料掃描入庫時(shí)用的就是它。這類文件最大的特點(diǎn)是沒有統(tǒng)一的公開標(biāo)準(zhǔn)本質(zhì)上它是一套自定義的圖像封裝方案——每一頁掃描圖被壓縮編碼后打包成獨(dú)立文件目錄、書簽、頁碼信息則存在配套的元數(shù)據(jù)文件里。所以直接拿它當(dāng)普通圖片看吧大多數(shù)看圖軟件不認(rèn)想直接打印吧打印驅(qū)動(dòng)也不認(rèn)想發(fā)給別人閱讀吧對(duì)方連打開都費(fèi)勁。我做這個(gè)項(xiàng)目時(shí)手上有一批掃描版的技術(shù)手冊(cè)大概幾百個(gè)pdg文件分散在幾十個(gè)文件夾里每一本都要轉(zhuǎn)成PDF才能方便地在平板、手機(jī)上閱讀、標(biāo)注、檢索。折騰了一圈后發(fā)現(xiàn)市面上現(xiàn)成的轉(zhuǎn)換工具要么收費(fèi)、要么帶水印、要么轉(zhuǎn)換批量處理時(shí)卡死最后干脆用Python寫了一個(gè)自動(dòng)化腳本核心思路就是標(biāo)題里寫的那樣先解析pdg文件把它還原成jpg圖片再把jpg按頁碼順序合成為一個(gè)pdf。這個(gè)方案的優(yōu)勢(shì)在于中間產(chǎn)物jpg是通用的隨時(shí)可以拿出來單獨(dú)使用比如圖片裁剪、OCR識(shí)別、局部放大去噪不必每做一步操作就重新解析一次pdg。而且整個(gè)過程邏輯非常直接不依賴任何商業(yè)閉源工具所有環(huán)節(jié)都能看得見、可修改、可中斷續(xù)跑。1.2 為什么中間要過一道jpg而不是直接轉(zhuǎn)pdf很多人會(huì)問pdg2pdf一步到位不就行了為什么非要先落成jpg再合成PDF答案是穩(wěn)定性和可控性。直接轉(zhuǎn)換的方案在遇到特殊編碼的pdg文件時(shí)非常容易中途報(bào)錯(cuò)一旦出錯(cuò)整本pdf就廢了又得從頭再來。而先轉(zhuǎn)jpg的話轉(zhuǎn)換進(jìn)度以圖片為單位可控哪一頁有問題單獨(dú)修復(fù)那一頁就行不影響整體流程。另外jpg中間產(chǎn)物讓你有機(jī)會(huì)在合成pdf之前做圖像預(yù)處理比如把歪斜的掃描頁校正、把黑邊裁掉、把對(duì)比度拉高這些都是實(shí)際處理掃描書時(shí)經(jīng)常遇到的需求。我自己在轉(zhuǎn)換過程中就遇到過一批頁面發(fā)灰的情況后來在合并前統(tǒng)一做了灰度增強(qiáng)出來的PDF閱讀體驗(yàn)好了不少。還有一點(diǎn)非常實(shí)際pdg文件本身有大有小直接轉(zhuǎn)大頁碼的pdg文件時(shí)如果內(nèi)存管理不到位很容易把進(jìn)程撐爆。而逐頁轉(zhuǎn)jpg再分批合入PDF每一步內(nèi)存占用都可控不會(huì)因?yàn)槟潮緯摂?shù)多就翻車。2. 環(huán)境準(zhǔn)備與工具選型2.1 Python環(huán)境的搭建與依賴安裝我是在Windows上跑的這套流程Python版本用的3.10。裝Python這一步?jīng)]什么特別的去官網(wǎng)下載安裝包勾選Add Python to PATH一路下一步就行。如果系統(tǒng)里已經(jīng)裝了Anaconda或者其他Python發(fā)行版直接用也行但建議為這個(gè)項(xiàng)目單獨(dú)建一個(gè)虛擬環(huán)境避免依賴沖突。核心依賴其實(shí)只有兩個(gè)一個(gè)是pdg2pdf庫另一個(gè)是圖像處理庫Pillow。pdg2pdf庫用來解析pdg文件、提取頁面圖像數(shù)據(jù)Pillow負(fù)責(zé)把解析出來的圖像數(shù)據(jù)編碼成jpg文件順便可以做基本的圖像處理。此外我建議再裝一個(gè)PyMuPDF也就是fitz這是一個(gè)非常好用的PDF生成和操作庫后面合成PDF時(shí)要比直接用Pillow的save方法靈活得多支持設(shè)置壓縮級(jí)別、嵌入書簽、調(diào)整頁面大小。pip install pdg2pdf Pillow PyMuPDF如果pip下載速度慢可以臨時(shí)指到國內(nèi)鏡像源比如清華源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pdg2pdf Pillow PyMuPDF裝上之后驗(yàn)證一下能不能import成功這一步別省很多問題其實(shí)都出在環(huán)境沒裝干凈。2.2 pdg2pdf庫的能力邊界與適用場(chǎng)景pdg2pdf這個(gè)庫我實(shí)測(cè)下來的感受是好用但有脾氣。它能處理的pdg文件主要是標(biāo)準(zhǔn)掃描版格式也就是每個(gè)頁面都有一個(gè)獨(dú)立的圖像數(shù)據(jù)段如果遇到加密過或者改版過的pdg文件庫會(huì)直接拋錯(cuò)或者輸出的圖片是花屏。所以用它之前最好先摸清楚你手上的pdg文件是什么來路是官方掃描入庫的還是第三方手工制作的手工制作的文件編碼往往不規(guī)范容易出問題。不過這個(gè)庫的設(shè)計(jì)思路跟我們的項(xiàng)目標(biāo)題完全吻合它內(nèi)部也是先做pdg到圖片的解析然后才涉及PDF合成。所以哪怕庫本身處理不了某些特殊情況至少它把最復(fù)雜的pdg解析邏輯給你實(shí)現(xiàn)了省去了你去逆向文件格式的功夫。我在項(xiàng)目里使用的是它的圖片解析能力把pdg文件的內(nèi)容讀出來喂給Pillow去保存jpg同時(shí)我也會(huì)直接調(diào)用它自帶的pdg2pdf函數(shù)做一次快速轉(zhuǎn)換當(dāng)作交叉驗(yàn)證確保手動(dòng)流程的結(jié)果和庫直轉(zhuǎn)的結(jié)果一致。我的建議是小批量文件、文件編碼比較規(guī)范的時(shí)候直接用庫自帶的一條龍轉(zhuǎn)換省事大批量文件、中間需要加圖像處理步驟的時(shí)候拆成兩段來處理更合適。這篇文章后面給出的腳本就是按兩段式來設(shè)計(jì)的。3. 核心實(shí)操pdg轉(zhuǎn)jpg再合成PDF的完整流程3.1 先理清輸入輸出的目錄結(jié)構(gòu)在實(shí)際動(dòng)手之前我先把任務(wù)拆成了三個(gè)子步驟掃描目錄找所有pdg文件、逐個(gè)把pdg轉(zhuǎn)成jpg、把所有jpg按頁碼合并成一個(gè)pdf。輸入目錄結(jié)構(gòu)大概是這樣的books/ ├── book_a/ │ ├── 0001.pdg │ ├── 0002.pdg │ └── ... ├── book_b/ │ ├── 0001.pdg │ ├── 0002.pdg │ └── ...輸出目錄我建議單獨(dú)建一個(gè)跟原文件隔離開因?yàn)橹虚gjpg文件和最終pdf文件加起來體積不小混在一起容易亂。我習(xí)慣的輸出結(jié)構(gòu)是output/ ├── jpg/ │ ├── book_a/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ │ └── ... │ └── book_b/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... └── pdf/ ├── book_a.pdf └── book_b.pdf這樣一本書對(duì)應(yīng)一個(gè)jpg文件夾、一個(gè)pdf文件后期想補(bǔ)轉(zhuǎn)某一本、想刪掉中間產(chǎn)物重來都非常好操作。3.2 第一步掃描目錄并解析pdg文件這部分代碼的核心是獲取目錄下所有pdg文件的完整路徑然后按文件名排序。注意文件名排序要用自然排序而不是字符串排序。因?yàn)槿绻募?.pdg、2.pdg、10.pdg字符串排序會(huì)把10.pdg排到2.pdg前面最終合并pdf時(shí)頁碼順序就錯(cuò)了。import os import re def natural_sort_key(filename): return [int(text) if text.isdigit() else text.lower() for text in re.split(r(\d), filename)] def find_pdg_files(directory): pdg_files [] for root, dirs, files in os.walk(directory): for f in files: if f.lower().endswith(.pdg): full_path os.path.join(root, f) pdg_files.append(full_path) return sorted(pdg_files, keylambda x: natural_sort_key(os.path.basename(x)))這里用os.walk是為了支持子目錄遞歸掃描有些掃描書會(huì)把一個(gè)章節(jié)放在一個(gè)子目錄里不遞歸的話會(huì)漏文件。natural_sort_key這個(gè)函數(shù)是用來做自然排序的關(guān)鍵正則表達(dá)式把數(shù)字和非數(shù)字拆開數(shù)字部分轉(zhuǎn)成int比較這樣1、2、10就能按數(shù)字大小排對(duì)了。3.3 第二步逐個(gè)pdg轉(zhuǎn)jpg解析pdg文件時(shí)我建議直接用pdg2pdf庫的內(nèi)部函數(shù)把頁面數(shù)據(jù)提取出來再轉(zhuǎn)成Pillow圖像對(duì)象。from pdg2pdf import pdg2img from PIL import Image import io def convert_pdg_to_jpg(pdg_path, jpg_output_path, quality85): images pdg2img(pdg_path) page_images [] for img_data in images: if isinstance(img_data, bytes): img Image.open(io.BytesIO(img_data)) else: img img_data if img.mode ! RGB: img img.convert(RGB) page_images.append(img) # 多頁pdg時(shí)第一頁存成主jpg其余頁按序號(hào)追加 if len(page_images) 1: page_images[0].save(jpg_output_path, JPEG, qualityquality) else: base_name os.path.splitext(jpg_output_path)[0] for idx, img in enumerate(page_images, start1): img.save(f{base_name}_{idx}.jpg, JPEG, qualityquality) return len(page_images)這里有一個(gè)非常重要的細(xì)節(jié)pdg2img返回的可能是一個(gè)包含多幀圖像的數(shù)據(jù)結(jié)構(gòu)因?yàn)閱蝹€(gè)pdg文件有時(shí)會(huì)包含同一個(gè)頁面的多個(gè)分辨率版本或者一個(gè)文件里打包了多頁掃描內(nèi)容。如果只取第一幀遇到多頁打包的pdg文件就會(huì)漏頁。所以代碼里做了一個(gè)遍歷把所有幀都提取出來。jpg的quality參數(shù)我推薦85到90之間。quality太高文件體積會(huì)爆炸一本書幾百頁每頁幾MB加起來就是幾個(gè)Gquality太低文字邊緣會(huì)發(fā)虛掃描書本身清晰度就一般再壓狠了就沒法看了。85是比較均衡的檔位。3.4 第三步把jpg列表合成一個(gè)PDF合成PDF的方案我用過兩種第一種是Pillow自帶的save方法def images_to_pdf_pillow(image_list, pdf_path): first_img Image.open(image_list[0]).convert(RGB) imgs [Image.open(img).convert(RGB) for img in image_list[1:]] first_img.save(pdf_path, save_allTrue, append_imagesimgs, resolution150.0) return pdf_path這種方法寫起來非常簡(jiǎn)潔適合幾百頁以下的書籍。但它有兩個(gè)短板一是沒法細(xì)粒度控制頁面尺寸和壓縮策略二是當(dāng)圖片數(shù)量特別多時(shí)內(nèi)存壓力很大因?yàn)镻illow會(huì)把所有圖片的句柄都保存在內(nèi)存列表里。第二種方案是用PyMuPDF也就是fitz逐頁插入圖片。這種方案內(nèi)存占用小得多而且可以邊插入邊壓縮對(duì)幾千頁的超大掃描書更友好。import fitz def images_to_pdf_pymupdf(image_list, pdf_path, target_dpi150): doc fitz.open() for img_path in image_list: img_doc fitz.open(img_path) pdf_bytes img_doc.convert_to_pdf() img_pdf fitz.open(pdf, pdf_bytes) page doc.new_page(widthimg_pdf[0].rect.width, heightimg_pdf[0].rect.height) page.show_pdf_page(page.rect, img_pdf, 0) img_doc.close() img_pdf.close() doc.save(pdf_path, deflateTrue, garbage3) doc.close() return pdf_path這個(gè)方法的核心思路是把jpg先轉(zhuǎn)成一個(gè)單頁pdf對(duì)象再用show_pdf_page把這一頁嵌入到目標(biāo)文檔中。好處是可以精確控制頁面大小等于圖片原始尺寸不會(huì)出現(xiàn)頁面被拉伸變形的問題。加target_dpi參數(shù)是給后續(xù)擴(kuò)展用的如果圖片分辨率過高可以在插入時(shí)按目標(biāo)DPI等比縮小。3.5 一鍵串聯(lián)完整的批量處理腳本把上面的步驟整合到一起再加上進(jìn)度打印和錯(cuò)誤日志就是一套可以直接跑批的腳本import os import time import traceback from pdg2pdf import pdg2img from PIL import Image import io import fitz def run_batch(): source_dir rD:\books output_jpg_dir rD:\output\jpg output_pdf_dir rD:\output\pdf os.makedirs(output_jpg_dir, exist_okTrue) os.makedirs(output_pdf_dir, exist_okTrue) book_dirs [d for d in os.listdir(source_dir) if os.path.isdir(os.path.join(source_dir, d))] book_dirs.sort() error_log [] start_time time.time() for book in book_dirs: book_path os.path.join(source_dir, book) print(f開始處理: {book}) book_start time.time() pdg_files find_pdg_files(book_path) if not pdg_files: print(f 未找到pdg文件跳過) continue jpg_dir os.path.join(output_jpg_dir, book) os.makedirs(jpg_dir, exist_okTrue) jpg_paths [] try: for idx, pdg_file in enumerate(pdg_files, start1): base_name os.path.splitext(os.path.basename(pdg_file))[0] jpg_path os.path.join(jpg_dir, f{base_name}.jpg) convert_pdg_to_jpg(pdg_file, jpg_path, quality88) jpg_paths.append(jpg_path) if idx % 50 0: print(f 已轉(zhuǎn)換 {idx}/{len(pdg_files)} 頁) pdf_path os.path.join(output_pdf_dir, f{book}.pdf) images_to_pdf_pymupdf(jpg_paths, pdf_path) elapsed time.time() - book_start print(f 完成: {book}.pdf, 共 {len(pdg_files)} 頁, 耗時(shí) {elapsed:.1f}s) except Exception as e: print(f 處理失敗: {e}) error_log.append((book, traceback.format_exc())) total_time time.time() - start_time print(f\n全部處理完成, 總耗時(shí) {total_time:.1f}s) if error_log: print(f有 {len(error_log)} 本書處理失敗:) for book, err in error_log: print(f - {book}: {err.splitlines()[-1]}) if __name__ __main__: run_batch()這套腳本我在實(shí)際項(xiàng)目里跑過很多次幾十本書批量處理下來基本不需要人工干預(yù)。關(guān)鍵設(shè)計(jì)是錯(cuò)誤日志的收集機(jī)制某本書中途失敗不會(huì)中斷整個(gè)批處理而是記錄到error_log里最后統(tǒng)一打印等你回頭排查。4. 常見問題與排查技巧實(shí)錄4.1 轉(zhuǎn)換后jpg全黑或花屏這是pdg轉(zhuǎn)jpg最經(jīng)典的問題。原因通常是pdg文件內(nèi)部的圖像數(shù)據(jù)不是標(biāo)準(zhǔn)的JPEG編碼而是自定義的壓縮格式。pdg2pdf庫對(duì)這種情況會(huì)嘗試用內(nèi)部解碼器解析但一旦遇到特別古怪的編碼變體解析出來就是一團(tuán)黑或者花屏。我的排查思路分三步。第一步先確認(rèn)單個(gè)pdg文件在原生閱讀器里能正常顯示如果能顯示就說明文件本身沒問題第二步檢查pdg2pdf庫的版本有些老舊版本對(duì)某些編碼支持不全升級(jí)到最新版往往能解決第三步如果升級(jí)后仍然花屏考慮用另一個(gè)解析庫pypdg作為備用方案不同庫的解碼邏輯有差異這個(gè)解不開也許那個(gè)能解開。還有一種容易忽略的情況pdg文件雖然擴(kuò)展名是.pdg但內(nèi)部其實(shí)就是標(biāo)準(zhǔn)的jpg數(shù)據(jù)。遇到這種情況最省事的辦法是直接用二進(jìn)制方式讀取文件頭判斷是否是jpg格式的magic numberFFD8FF如果是就直接復(fù)制改擴(kuò)展名完全不需要復(fù)雜的解析流程。def check_pdg_file_type(pdg_path): with open(pdg_path, rb) as f: header f.read(16) if header.startswith(b\xff\xd8\xff\xe0) or header.startswith(b\xff\xd8\xff\xe1): return jpeg elif header.startswith(b\x00\x00\x01\x00): return tiff-like else: return unknown4.2 合并PDF后發(fā)現(xiàn)頁面順序錯(cuò)亂頁面順序錯(cuò)亂基本只可能是一個(gè)原因文件排序時(shí)用了字符串排序而不是自然排序。前面已經(jīng)提過1.pdg會(huì)排在10.pdg之后的坑這里再補(bǔ)充一種更隱蔽的情況文件名里有下劃線或者漢字時(shí)不同操作系統(tǒng)的排序規(guī)則不一樣Windows和Linux下os.walk返回的順序可能就不是一致的所以必須顯式用自然排序絕不能依賴遍歷順序。另外還有一種情況某些掃描書的第一頁是封面文件名可能叫cover.pdg后面的頁面叫0001.pdg、0002.pdg。自然排序會(huì)把cover.pdg排在0001.pdg前面但cover.pdg的語義上應(yīng)該是封面。遇到這種情況需要對(duì)文件名做映射給特殊命名的文件指定一個(gè)排序權(quán)重。def book_page_sort_key(filename): name os.path.splitext(filename)[0].lower() if name.startswith(cover) or name.startswith(front): return (0, 0) elif name.startswith(back) or name.startswith(end): return (99999, 0) else: digits re.search(r\d, name) num int(digits.group()) if digits else 0 return (1, num)4.3 生成的PDF體積過大掃描書轉(zhuǎn)出的PDF動(dòng)輒幾十上百M(fèi)B這個(gè)問題我踩過很多次坑。主要原因是jpg保存時(shí)quality設(shè)置太高以及合成PDF時(shí)沒有啟用壓縮。先說jpg階段的控制。掃描書的內(nèi)容大多是文字和簡(jiǎn)單圖形其實(shí)對(duì)圖片質(zhì)量的要求沒有照片那么高quality設(shè)置在80到85之間完全夠用肉眼幾乎看不出區(qū)別但文件體積能小一半以上。另外如果原圖是純黑白文字頁可以在轉(zhuǎn)jpg時(shí)把圖像模式改成L灰度或者1二值文件體積會(huì)急劇縮小。判斷一頁是否是純黑白頁也很簡(jiǎn)單看圖像的顏色數(shù)量如果每個(gè)像素的RGB三通道差值都很小就當(dāng)成灰度圖處理。再說PDF階段的控制。PyMuPDF的save方法里我用了deflateTrue和garbage3兩個(gè)參數(shù)前者對(duì)PDF內(nèi)部的流對(duì)象做壓縮后者清理文檔結(jié)構(gòu)里的冗余對(duì)象。這兩個(gè)參數(shù)對(duì)PNG頁面效果顯著對(duì)JPEG頁面也有一定作用。如果PDF里嵌入的圖片本身就是jpg格式PDF容器通常會(huì)原樣存儲(chǔ)而不是重新壓縮所以在jpg階段把體積控制好才是治本之策。4.4 轉(zhuǎn)換中途內(nèi)存暴漲導(dǎo)致程序被殺遇到大頁數(shù)pdg文件時(shí)內(nèi)存管理必須講究。pdg2img這個(gè)函數(shù)一次調(diào)用會(huì)把整本書的所有頁面數(shù)據(jù)全部讀入內(nèi)存如果一本書有上千頁內(nèi)存直接飆升到幾個(gè)GB。一個(gè)變通方案是分塊解析先單獨(dú)把pdg文件按某種粒度切成小段再逐段調(diào)用pdg2img。如果格式不允許切片那就用多進(jìn)程而不是多線程——把不同書的轉(zhuǎn)換任務(wù)分給不同進(jìn)程單進(jìn)程內(nèi)仍然是順序解析。Python的多線程在多核環(huán)境下受GIL鎖限制對(duì)CPU密集型的圖像解碼任務(wù)提升幾乎為零用multiprocessing模塊才能真正利用多核能力。from concurrent.futures import ProcessPoolExecutor def process_one_book(book): # 這里面只處理一本書的完整流程 return book, True with ProcessPoolExecutor(max_workers4) as executor: futures [executor.submit(process_one_book, book) for book in book_dirs] for future in futures: book, success future.result() print(f{book}: {成功 if success else 失敗})實(shí)測(cè)下來4個(gè)進(jìn)程同時(shí)跑4本不同的書轉(zhuǎn)換總耗時(shí)能縮減到原來的三分之一左右內(nèi)存消耗也平滑很多。4.5 斷點(diǎn)續(xù)轉(zhuǎn)處理到一半崩潰怎么辦大批量轉(zhuǎn)換時(shí)最怕的就是跑到第800頁程序崩了前面的努力全白費(fèi)。這個(gè)問題我用一個(gè)簡(jiǎn)單辦法解決jpg文件本身就天然具備斷點(diǎn)續(xù)傳功能。因?yàn)閖pg是逐頁生成的已經(jīng)生成的jpg文件就是已完成的部分重新跑腳本時(shí)可以先檢查jpg文件是否已存在且完整存在的直接跳過轉(zhuǎn)換只處理缺失的部分。def is_valid_jpg(path): try: with Image.open(path) as img: img.verify() return True except Exception: return False def convert_with_resume(pdg_file, jpg_path, quality88): if os.path.exists(jpg_path) and is_valid_jpg(jpg_path): print(f {jpg_path} 已存在且完整, 跳過) return convert_pdg_to_jpg(pdg_file, jpg_path, qualityquality)這個(gè)方案有個(gè)額外的好處如果某本pdf合成時(shí)報(bào)錯(cuò)不需要重新轉(zhuǎn)換所有圖片只重新執(zhí)行一遍合成步驟就行。jpg中間產(chǎn)物的價(jià)值在這里體現(xiàn)得淋漓盡致。5. 進(jìn)階技巧與擴(kuò)展玩法5.1 批量OCR讓掃描版PDF可以搜索PDF合成成功后如果你想更進(jìn)一步讓它支持文字搜索和復(fù)制就需要接入OCR識(shí)別。掃描書的文字是圖片形式的位圖不是真正的文本層OCR可以把圖片里的文字識(shí)別出來以不可見的文本層嵌入到PDF中。PyMuPDF本身不包含OCR能力需要配合Tesseract或者PaddleOCR使用。我的做法是先用OCR把jpg識(shí)別成帶坐標(biāo)的文字塊再用PyMuPDF的insert_text把這些文字寫入到對(duì)應(yīng)頁面上的透明圖層。這樣既保留了原始掃描圖像又增加了可搜索文本閱讀器里直接搜索關(guān)鍵詞和復(fù)制引用都沒問題。我自己的經(jīng)驗(yàn)是如果識(shí)別的是簡(jiǎn)體中文掃描書優(yōu)先用PaddleOCR它的中文識(shí)別準(zhǔn)確率在開源方案里是拔尖的如果主要是英文和技術(shù)符號(hào)Tesseract配置好語言包也夠用。5.2 目錄書簽自動(dòng)生成很多掃描版PDF沒有目錄書簽幾百頁的書翻起來很痛苦。如果你能拿到pdg文件配套的目錄結(jié)構(gòu)文件比如Contents或者Bookinfo文件可以解析出章節(jié)標(biāo)題和對(duì)應(yīng)的頁碼然后在合成PDF時(shí)通過PyMuPDF的set_toc方法把目錄寫入。def add_toc(doc, toc_entries): # toc_entries 格式: [level, title, page_number] doc.set_toc(toc_entries)即使沒有目錄文件也可以根據(jù)頁面的視覺特征做啟發(fā)式識(shí)別比如每章第一頁的文字區(qū)域分布和普通頁有明顯差異通過簡(jiǎn)單判斷就能定位章節(jié)起始頁然后手動(dòng)命名章節(jié)標(biāo)題。這個(gè)方法準(zhǔn)確率在八成左右足以用來做一個(gè)可用的粗略目錄。5.3 圖像預(yù)處理黑邊裁剪與方向校正老式掃描書經(jīng)常會(huì)有掃描儀黑邊、頁面歪斜、方向顛倒等問題。這些問題如果在jpg階段處理成本非常低如果等合成PDF后再想修就得對(duì)每一頁重新做圖像處理再替換頁面內(nèi)容麻煩得多。黑邊裁剪可以用Pillow的getbbox方法找到內(nèi)容區(qū)域的最小外接矩形然后按這個(gè)矩形裁剪。處理方向顛倒時(shí)可以先用OCR識(shí)別首頁的開頭文字如果倒過來了就旋轉(zhuǎn)180度再輸出。def trim_black_borders(img, threshold240): gray img.convert(L) bbox gray.point(lambda x: 255 if x threshold else 0).getbbox() if bbox: return img.crop(bbox) return img注意threshold的值要根據(jù)實(shí)際掃描質(zhì)量調(diào)整掃描底色太暗或者紙張發(fā)黃的閾值太大會(huì)把內(nèi)容邊緣裁掉太小又裁不干凈黑邊。6. 實(shí)操總結(jié)與經(jīng)驗(yàn)沉淀整個(gè)pdg到PDF的轉(zhuǎn)換鏈路核心心得可以歸結(jié)為三條第一中間產(chǎn)物很重要jpg這一步別看多占了點(diǎn)磁盤空間但它給你帶來了斷點(diǎn)續(xù)傳、圖像預(yù)處理、獨(dú)立使用三個(gè)優(yōu)勢(shì)這是直轉(zhuǎn)方案沒法比的第二排序邏輯是所有轉(zhuǎn)換工具里最容易出錯(cuò)但又最容易忽略的環(huán)節(jié)不管用什么方案頁碼順序正確永遠(yuǎn)是最優(yōu)先要保證的第三批量處理場(chǎng)景下寧可慢一點(diǎn)也要加錯(cuò)誤日志和斷點(diǎn)續(xù)傳機(jī)制否則幾百本書跑到一半全部重來時(shí)間成本無法接受。我實(shí)際把這套腳本用在日常的工作流中已經(jīng)有一年多時(shí)間幾百本pdg格式的資料都轉(zhuǎn)成了統(tǒng)一PDF格式歸入電子圖書館統(tǒng)一管理。工具本身不復(fù)雜但設(shè)計(jì)思路上把穩(wěn)定性放在第一位遇到異常能恢復(fù)、能定位這比單純追求轉(zhuǎn)換速度更重要。如果你們手上的pdg文件量不大二三十本以內(nèi)直接用pdg2pdf庫自帶的一條龍轉(zhuǎn)換就夠了如果跟我一樣有批量處理和歷史積累需求花點(diǎn)時(shí)間把兩段式流程和斷點(diǎn)續(xù)傳寫進(jìn)腳本長期來看非常值得。