python将图片pdf文件转为透明文字的pdf文件

工作过程要在pdf的文件上查找内容,但是图片扫描的pdf文件,无法查找。为此用python写一个程序用于转换成ocr识别出透明文字,在原图片上的pdf文件。这个过程有点不容易,就是我喜欢使用paddleocr,且要用gpu识别,比较快。

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Sun Aug 30 10:58:00 2026

@author: YBK
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import os
import sys
import json
import shutil
from PyPDF2 import PdfReader, PdfWriter
from paddleocr import PaddleOCR

# 修复 hashlib 问题
import hashlib
_original_md5 = hashlib.md5
def patched_md5(*args, **kwargs):
    if 'usedforsecurity' in kwargs:
        del kwargs['usedforsecurity']
    return _original_md5(*args, **kwargs)
hashlib.md5 = patched_md5

# 设置 Ghostscript 环境
gs_bin = r'C:\Program Files\gs\gs10.07.1\bin'
os.environ['PATH'] = gs_bin + os.pathsep + os.environ.get('PATH', '')
os.environ['GS_PROG'] = os.path.join(gs_bin, 'gswin64c.exe')

# 导入 PdfOCRer
from pdfocrer.pdf_ocrer import PdfOCRer

# 每批处理的页数(用于降低显存峰值,但不会彻底释放显存)
PAGES_PER_BATCH = 30   # 可根据显存调整,设为 0 表示一次性处理所有页(但容易爆显存)

class PDFOCRApp:
    def __init__(self, root):
        self.root = root
        self.root.title("PDF OCR 转换工具 (PdfOCRer) - 断点续传")
        self.root.geometry("600x400")
        self.root.resizable(False, False)

        self.input_path = tk.StringVar()
        self.output_path = tk.StringVar()
        self.lang = tk.StringVar(value="ch")
        self.is_processing = False
        self.debug = False   # 添加这一行

        self._create_widgets()

    def _create_widgets(self):
        main_frame = ttk.Frame(self.root, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        ttk.Label(main_frame, text="1. 选择输入的PDF文件:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
        input_frame = ttk.Frame(main_frame)
        input_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
        input_frame.columnconfigure(0, weight=1)
        ttk.Entry(input_frame, textvariable=self.input_path, state="readonly").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        ttk.Button(input_frame, text="浏览...", command=self._browse_input).pack(side=tk.RIGHT)

        ttk.Label(main_frame, text="2. 选择输出PDF的位置:").grid(row=2, column=0, sticky=tk.W, pady=(0, 5))
        output_frame = ttk.Frame(main_frame)
        output_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
        output_frame.columnconfigure(0, weight=1)
        ttk.Entry(output_frame, textvariable=self.output_path, state="readonly").pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        ttk.Button(output_frame, text="浏览...", command=self._browse_output).pack(side=tk.RIGHT)

        ttk.Label(main_frame, text="3. 选择识别语言:").grid(row=4, column=0, sticky=tk.W, pady=(0, 5))
        lang_frame = ttk.Frame(main_frame)
        lang_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 20))
        languages = [("中文", "ch"), ("英文", "en"), ("韩文", "korean"), ("日文", "japan"), ("拉丁文", "latin"), ("阿拉伯文", "arabic")]
        for text, value in languages:
            ttk.Radiobutton(lang_frame, text=text, variable=self.lang, value=value).pack(side=tk.LEFT, padx=(0, 15))

        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=6, column=0, columnspan=3, pady=(0, 10))
        self.start_button = ttk.Button(button_frame, text="开始转换", command=self._start_ocr, width=15)
        self.start_button.pack(side=tk.LEFT, padx=(0, 20))
        self.progress_bar = ttk.Progressbar(button_frame, mode='indeterminate', length=200)
        self.progress_bar.pack(side=tk.LEFT)

        ttk.Label(main_frame, text="处理日志:").grid(row=7, column=0, sticky=tk.W, pady=(10, 5))
        self.log_text = tk.Text(main_frame, height=10, state='disabled', wrap=tk.WORD)
        self.log_text.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.log_text.yview)
        scrollbar.grid(row=8, column=3, sticky=(tk.N, tk.S))
        self.log_text['yscrollcommand'] = scrollbar.set

        main_frame.rowconfigure(8, weight=1)
        main_frame.columnconfigure(0, weight=1)

    def _browse_input(self):
        file_path = filedialog.askopenfilename(title="选择PDF文件", filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")])
        if file_path:
            self.input_path.set(file_path)
            base, ext = os.path.splitext(file_path)
            self.output_path.set(f"{base}_ocr{ext}")

    def _browse_output(self):
        file_path = filedialog.asksaveasfilename(title="保存PDF文件为", defaultextension=".pdf", filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")])
        if file_path:
            self.output_path.set(file_path)

    def _log(self, message):
        self.log_text.config(state='normal')
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state='disabled')
        self.root.update_idletasks()

    # ============== 断点续传核心函数 ==============
    def _get_temp_dir(self, input_pdf):
        """获取临时文件夹路径(在输入PDF同目录下创建)"""
        base_dir = os.path.dirname(input_pdf)
        basename = os.path.splitext(os.path.basename(input_pdf))[0]
        temp_dir = os.path.join(base_dir, basename + "_ocr_temp")
        return temp_dir

    def _get_progress_file(self, temp_dir):
        return os.path.join(temp_dir, "progress.json")

    def _load_progress(self, temp_dir):
        """读取进度文件,返回 last_completed(已完成的最后一页页码)和 total_pages"""
        prog_file = self._get_progress_file(temp_dir)
        if os.path.exists(prog_file):
            try:
                with open(prog_file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                return data.get("last_completed", 0), data.get("total_pages", 0)
            except:
                return 0, 0
        return 0, 0

    def _save_progress(self, temp_dir, last_completed, total_pages):
        prog_file = self._get_progress_file(temp_dir)
        with open(prog_file, 'w', encoding='utf-8') as f:
            json.dump({"last_completed": last_completed, "total_pages": total_pages}, f, indent=2)

    def _clear_progress(self, temp_dir):
        prog_file = self._get_progress_file(temp_dir)
        if os.path.exists(prog_file):
            os.remove(prog_file)

    # ============== 合并单页PDF ==============
    def _merge_page_pdfs(self, page_pdfs, output_path):
        """按页码顺序合并所有单页PDF"""
        writer = PdfWriter()
        for pdf_path in page_pdfs:
            if os.path.exists(pdf_path):
                reader = PdfReader(pdf_path)
                for page in reader.pages:
                    writer.add_page(page)
            else:
                self._log(f"警告:找不到页面文件 {pdf_path}")
        with open(output_path, 'wb') as f:
            writer.write(f)
        self._log(f"合并完成,输出文件:{output_path}")

    # ============== 核心处理 ==============
    def _run_ocr(self):
        try:
            input_pdf = self.input_path.get()
            output_pdf = self.output_path.get()
            lang = self.lang.get()

            self._log(f"开始处理: {input_pdf}")
            self._log(f"输出文件: {output_pdf}")
            self._log(f"识别语言: {lang}")
            self._log("-" * 40)

            # 创建临时目录
            temp_dir = self._get_temp_dir(input_pdf)
            os.makedirs(temp_dir, exist_ok=True)
            self._log(f"临时目录: {temp_dir}")

            # 读取进度
            last_completed, total_pages_saved = self._load_progress(temp_dir)
            # 获取总页数
            reader = PdfReader(input_pdf)
            total_pages = len(reader.pages)
            if total_pages_saved != total_pages:
                # 如果文件页数有变化,重置进度
                self._log("检测到文件页数变化,重置进度。")
                last_completed = 0
                total_pages_saved = total_pages
                self._save_progress(temp_dir, 0, total_pages)

            # 检查是否已全部完成
            if last_completed >= total_pages:
                self._log("所有页面已处理完毕,直接合并...")
                # 收集所有单页PDF(假设已存在)
                page_files = [os.path.join(temp_dir, f"page_{i+1:04d}.pdf") for i in range(total_pages)]
                self._merge_page_pdfs(page_files, output_pdf)
                self._clear_progress(temp_dir)
                self._log("✅ 转换成功完成!")
                self._log(f"可搜索PDF已保存至: {output_pdf}")
                return

            # 初始化 OCR 引擎(低内存参数)
            self._log("加载OCR模型...")
            ocr_engine = PaddleOCR(
                lang=lang,
                use_gpu=True,
                gpu_mem=2048,          # 限制显存
                use_angle_cls=False,
                rec_batch_num=1,
                det_batch_num=1,
                max_text_length=50
            )
            self._log("OCR模型加载完成。")

            # 初始化 PdfOCRer 实例(统一临时目录)
            ocr = PdfOCRer(debug=False, temp_dir=temp_dir)

            # 从 last_completed+1 开始处理
            start_page = last_completed + 1
            self._log(f"从第 {start_page} 页开始处理...")

            for page_num in range(start_page, total_pages + 1):
                self._log(f"正在处理第 {page_num}/{total_pages} 页...")
                try:
                    # 处理单页,返回该页的OCR PDF路径
                    page_pdf_path = ocr.process_single_page(input_pdf, page_num, ocr_engine)
                    # 将生成的页面文件重命名为统一格式(方便合并)
                    target_name = os.path.join(temp_dir, f"page_{page_num:04d}.pdf")
                    if os.path.exists(page_pdf_path):
                        os.rename(page_pdf_path, target_name)
                    else:
                        self._log(f"警告:第 {page_num} 页未生成有效文件。")
                        # 继续处理下一页?或者抛异常?视情况而定
                    # 更新进度
                    last_completed = page_num
                    self._save_progress(temp_dir, last_completed, total_pages)
                    self._log(f"第 {page_num} 页完成,进度已保存。")
                except Exception as e:
                    self._log(f"第 {page_num} 页处理失败: {e}")
                    # 发生异常时,进度停留在当前-1,下次可继续
                    raise  # 重新抛出,终止流程

            # 所有页面处理完毕,合并
            self._log("所有页面处理完毕,开始合并...")
            page_files = [os.path.join(temp_dir, f"page_{i+1:04d}.pdf") for i in range(total_pages)]
            self._merge_page_pdfs(page_files, output_pdf)

            # 清理临时目录(可选)
            try:
                shutil.rmtree(temp_dir)
                self._log("临时目录已清理。")
            except Exception as e:
                self._log(f"清理临时目录时出错: {e}")

            # 删除进度文件
            self._clear_progress(temp_dir)

            self._log("-" * 40)
            self._log("✅ 转换成功完成!")
            self._log(f"可搜索PDF已保存至: {output_pdf}")

        except Exception as e:
            self._log(f"❌ 转换过程中出现错误: {e}")
            import traceback
            self._log(traceback.format_exc())
            messagebox.showerror("转换错误", f"处理失败,请查看日志了解详情。\n错误信息: {e}")
        finally:
            self.root.after(0, self._reset_ui)

    def _start_ocr(self):
        if self.is_processing:
            return
        if not self.input_path.get():
            messagebox.showwarning("提示", "请先选择一个输入的PDF文件。")
            return
        if not self.output_path.get():
            messagebox.showwarning("提示", "请先设置输出文件的位置。")
            return
        if not os.path.exists(self.input_path.get()):
            messagebox.showerror("错误", "输入的PDF文件不存在。")
            return

        self.is_processing = True
        self.start_button.config(state='disabled', text="处理中...")
        self.progress_bar.start(10)
        self.log_text.config(state='normal')
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state='disabled')

        thread = threading.Thread(target=self._run_ocr, daemon=True)
        thread.start()

    def _reset_ui(self):
        self.is_processing = False
        self.start_button.config(state='normal', text="开始转换")
        self.progress_bar.stop()

if __name__ == "__main__":
    root = tk.Tk()
    app = PDFOCRApp(root)
    root.mainloop()

主要是装一些库和Ghostscript,我在官网下不了,在这个网站下载。https://www.pdfblog.at/https://www.pdfblog.at/

然后要给python建一个自己的环境:

复制代码
conda activate paddle_ocr_env

安装:

以 CUDA 11.2 为例,安装对应的 GPU 版本reference:8reference:9

如果你的 CUDA 版本不同,请到 PaddlePaddle 官网查找对应的安装命令

pip install paddlepaddle-gpu==2.4.2.post112 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html

直接安装 PaddleOCR

pip install paddleocr

这一步会出错,需要conda install -c conda-forge python-lmdb -y

然后:pip install paddlepaddle==2.4.2 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html

pip install paddleocr==2.6.1.3

pip install pdfocrer

将C:\Program Files\gs\gs10.07.1\bin里面的gswin64c.exe修改为gs.exe

然后用我上面的程序应该就好用了,还是有点复杂的。在过程中,我还遇到处理pdf到125页就爆内存,所以我后来改了这个,可以"续传",不怕它爆内存。这些都是deepseek帮忙的,但它就不会有我这种续传的想法,可见还是人比较聪明。

相关推荐
小新科研测评14 小时前
2026 年 3 种 PDF 文献翻译方案横评:公式/表格/双栏排版保真度实测
人工智能·ai·pdf·自动翻译
学术 学术 Fun21 小时前
PDF 转 draw.io:把 PDF 图表恢复成可编辑图形
pdf·流程图·drawio
开开心心就好21 小时前
电子教鞭工具支持画框写字插图片功能齐全
android·开发语言·前端·javascript·人工智能·pdf·html
2601_949950631 天前
Word、PDF、Excel题目怎么快速整理?
pdf·word·excel
redfred2 天前
Stirling-PDF 使用教程:开源 PDF 工具箱 50+ 工具 PDF转Word/合并/压缩/OCR 桌面版 Docker 部署详解
pdf·开源·word
2501_930707782 天前
如何使用C#代码向 PDF 文档添加多种类型的注释
pdf
SamChan902 天前
Python+PyMuPDF提取PDF表格并翻译:表格结构检测与双语重建实战
windows·python·ai·pdf·wpf
E_ICEBLUE2 天前
Python 实现 Markdown 转 Word、PDF:从转换到页面设置
python·pdf·word·markdown·格式转换
zhonyu鱼2 天前
Drawpile:多人实时协作绘画,一起在同一块画布上画画
笔记·pdf·开源·开源软件