python去除pdf白边

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👉 运行结束,按回车键退出...")
``
相关推荐
m0_706653231 天前
用Python批量处理Excel和CSV文件
jvm·数据库·python
Yvonne爱编码1 天前
JAVA数据结构 DAY5-LinkedList
java·开发语言·python
witAI1 天前
**AI漫剧制作工具2025推荐,零成本实现专业级动画创作*
人工智能·python
千秋乐。1 天前
C++-string
开发语言·c++
孞㐑¥1 天前
算法—队列+宽搜(bfs)+堆
开发语言·c++·经验分享·笔记·算法
yufuu981 天前
并行算法在STL中的应用
开发语言·c++·算法
charlie1145141911 天前
嵌入式C++教程——ETL(Embedded Template Library)
开发语言·c++·笔记·学习·嵌入式·etl
陳10301 天前
C++:AVL树的模拟实现
开发语言·c++
zfoo-framework1 天前
docker desktop
开发语言
qq_423233901 天前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python