for kindle、汉王等电纸书
需求为去白边,以及如果行列的空白太多,也要去空白。
对整本书,每一页先拆成图片,然后对每张图片load,使用cv处理。
python
import fitz # PyMuPDF
import cv2
import numpy as np
from PIL import Image
import os
import shutil
import glob
# ================= ⚙️ 配置中心 =================
# 输入文件
INPUT_FILE = "input.pdf"
# 输出文件
FINAL_OUTPUT = "output.pdf"
# 两个临时文件夹
DIR_RAW = "temp_1_raw" # 存放第一步提取的原图
DIR_PROCESSED = "temp_2_processed" # 存放第二步处理后的图
# 参数设置
DPI = 150 # 提取图片的清晰度 (汉王电纸书150够了,想要更清就200)
JPEG_QUALITY = 70 # 最终图片的压缩质量 (越小体积越小)
NOISE_TOLERANCE = 15 # 抗噪点强度 (一行少于15个黑点视为白边)
PADDING = 20 # 切完后保留的边缘空隙(像素)
# ==============================================
def ensure_dir(path):
if not os.path.exists(path):
os.makedirs(path)
def get_crop_box_cv2(image_path):
"""
读取图片路径,计算裁剪范围 (x, y, w, h)
"""
# 使用 OpenCV 读取图片 (灰度模式)
# 注意:cv2.imread 不支持中文路径,这里用 numpy 读取法绕过
img_np = np.fromfile(image_path, dtype=np.uint8)
gray = cv2.imdecode(img_np, cv2.IMREAD_GRAYSCALE)
if gray is None:
return None
h, w = gray.shape
# 二值化 (黑字白纸 -> 反转为 白字黑纸)
# 使用 OTSU 自动阈值 + 10 修正,过滤背景底色
base_thresh, _ = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
_, thresh_inv = cv2.threshold(gray, base_thresh + 10, 255, cv2.THRESH_BINARY_INV)
# 投影法 (Projection Profile)
row_sums = np.sum(thresh_inv, axis=1) / 255
col_sums = np.sum(thresh_inv, axis=0) / 255
# 寻找墨水点超过容差的行和列
valid_y = np.where(row_sums > NOISE_TOLERANCE)[0]
valid_x = np.where(col_sums > NOISE_TOLERANCE)[0]
if len(valid_y) == 0 or len(valid_x) == 0:
return None # 空白页
x0, y0 = valid_x[0], valid_y[0]
x1, y1 = valid_x[-1], valid_y[-1]
# 添加 Padding 并不超出边界
x0 = max(0, x0 - PADDING)
y0 = max(0, y0 - PADDING)
x1 = min(w, x1 + PADDING)
y1 = min(h, y1 + PADDING)
return (x0, y0, x1 - x0, y1 - y0) # x, y, w, h
def step_1_extract_images():
print(f"\n🚀 [Step 1/3] 正在将 PDF 每一页保存为图片...")
ensure_dir(DIR_RAW)
doc = fitz.open(INPUT_FILE)
total = len(doc)
# 缩放矩阵
matrix = fitz.Matrix(DPI / 72, DPI / 72)
for i, page in enumerate(doc):
page_num = i + 1
save_path = os.path.join(DIR_RAW, f"page_{page_num:04d}.png")
# 断点续传:如果图有了就跳过
if os.path.exists(save_path):
continue
try:
pix = page.get_pixmap(matrix=matrix)
pix.save(save_path)
if page_num % 20 == 0:
print(f" - 已提取 {page_num}/{total} 页")
except Exception as e:
print(f" ❌ 第 {page_num} 页提取失败: {e}")
print("✅ 第一步完成:所有原图已保存。")
def step_2_process_images():
print(f"\n🚀 [Step 2/3] 正在对图片进行 切白边 + 压缩...")
ensure_dir(DIR_PROCESSED)
# 获取第一步文件夹里所有的 png
raw_files = sorted(glob.glob(os.path.join(DIR_RAW, "*.png")))
total = len(raw_files)
for i, file_path in enumerate(raw_files):
filename = os.path.basename(file_path) # page_0001.png
# 把后缀改成 .jpg (我们要压缩)
out_name = filename.replace(".png", ".jpg")
save_path = os.path.join(DIR_PROCESSED, out_name)
if os.path.exists(save_path):
continue # 跳过已处理的
try:
# 1. 计算裁剪框
crop_rect = get_crop_box_cv2(file_path)
# 使用 PIL 打开处理 (方便裁剪和保存 JPG)
with Image.open(file_path) as img:
if crop_rect:
x, y, w, h = crop_rect
# PIL 的 crop 需要 (left, upper, right, lower)
box = (x, y, x + w, y + h)
img_cropped = img.crop(box)
else:
print(f" - {filename} 检测为空白或全黑,保留原图")
img_cropped = img
# 2. 转为 RGB (防止 PNG 的 Alpha 通道导致存 JPG 报错)
if img_cropped.mode in ("RGBA", "P"):
img_cropped = img_cropped.convert("RGB")
# 3. 保存压缩后的 JPG
img_cropped.save(save_path, "JPEG", quality=JPEG_QUALITY, optimize=True)
if (i + 1) % 20 == 0:
print(f" - 已处理 {i + 1}/{total} 张图片")
except Exception as e:
print(f" ❌ 处理图片 {filename} 失败: {e}")
print("✅ 第二步完成:图片处理完毕。")
def step_3_merge_pdf():
print(f"\n🚀 [Step 3/3] 正在合并所有图片为 PDF...")
# 获取第二步文件夹里所有的 jpg
processed_files = sorted(glob.glob(os.path.join(DIR_PROCESSED, "*.jpg")))
if not processed_files:
print("❌ 错误:没有找到处理后的图片,无法合并!")
return
doc = fitz.open()
for i, img_path in enumerate(processed_files):
try:
# 打开图片
img = fitz.open(img_path)
# 获取图片尺寸
rect = img[0].rect
# 创建一样大的 PDF 页面
pdfbytes = img.convert_to_pdf()
img.close()
img_pdf = fitz.open("pdf", pdfbytes)
doc.insert_pdf(img_pdf)
if (i + 1) % 50 == 0:
print(f" - 已合并 {i + 1} 页...")
except Exception as e:
print(f" ❌ 合并 {os.path.basename(img_path)} 时出错: {e}")
doc.save(FINAL_OUTPUT, garbage=4, deflate=True)
print(f"\n🎉 全部完成!最终文件: {FINAL_OUTPUT}")
print(f"📂 你可以去查看临时文件夹 {DIR_RAW} 和 {DIR_PROCESSED},确认无误后可手动删除。")
if __name__ == "__main__":
if not os.path.exists(INPUT_FILE):
print(f"❌ 找不到文件: {INPUT_FILE}")
input("按回车退出...")
else:
# 按顺序执行三步
step_1_extract_images()
step_2_process_images()
step_3_merge_pdf()
input("\n👉 运行结束,按回车键退出...")
``