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帮忙的,但它就不会有我这种续传的想法,可见还是人比较聪明。

相关推荐
泡海椒2 小时前
jquick-pdf 表格行列尺寸控制实战:单元格合并的可行边界
java·开发语言·pdf
java_logo18 小时前
Docker 部署 Gotenberg 完整教程:Compose 搭建文档转 PDF API
docker·pdf·chromium·libreoffice·文档转换·轩辕镜像·gotenberg
泡海椒1 天前
jquick-pdf 防止 PDF 内容分页断裂:keepTogether 属性妙用
java·开发语言·pdf
泡海椒1 天前
jquick-pdf 文本元素实战:段落、行内文本、制表符用法详解
java·开发语言·pdf
SamChan902 天前
Python 处理小语种 PDF 的编码坑:重音字符、连字与 (cid:xx) 乱码的解决方案
python·ai·pdf·机器翻译
微咣科技2 天前
平台化工业AI再突破|大捷智能携手岚图汽车,打造车身焊装智能设计行业标杆
pdf·音视频
我滴老baby2 天前
工业物联网数据库选型:把计算能力放回第一维度
数据库·人工智能·架构·pdf
SamChan902 天前
多栏PDF阅读顺序重建:用Python按坐标聚类还原双栏论文的翻译顺序
python·ai·pdf
浩风祭月2 天前
ChatGPT上传PDF后漏读图表?文字版、扫描件和图片要分开处理
人工智能·chatgpt·pdf·提示词·办公效率
pengzhang1302 天前
在线改 PDF 文字,我把三个网站用同一份文件实测了一遍
pdf·工具·pdf编辑·在线免费·niubai.vip