吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 962|回复: 56
上一主题 下一主题
收起左侧

[学习记录] PDF批量合并(奇数页增加空白页-横版旋转成竖版)-方便一次性双面打印

[复制链接]
跳转到指定楼层
楼主
lizhipei78 发表于 2026-7-24 00:24 回帖奖励
应用场景,如有n个PDF文件,这些文件有些是偶数页的,有些是奇数页的,现在想合并之些PDF文件,以方便一次性打印,正常来说每一个文件的第一页都最好是位于奇数页,所以在合并pdf以前,最好先将所有奇数页的pdf后面补一个空白页,这样就可以拿一个文件到打印店进行双面打印了。另外有的是横版有的是竖版,在统一尺寸之前最好是先全部转成竖版。

目前该代码实现了PDF批量合并(奇数页增加空白页-横版旋转成竖版),求大佬看如何增加统一页面尺寸功能,我这边怎么的搞不好这个功能。




成品下载:PDF批量合并(奇数页增加空白页-横版旋转成竖版).exe
链接: https://pan.baidu.com/s/18YOaEsjg3Jike3mpWGwWKw?pwd=9geq 提取码: 9geq

代码
[Python] 纯文本查看 复制代码
# PDF批量处理工具.py
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import PyPDF2
from PyPDF2 import PdfReader, PdfWriter
import threading
from datetime import datetime
import shutil


class PDFProcessor:
    """PDF处理核心类 - 简化稳定版"""

    # 标准A4竖版尺寸(点)
    A4_WIDTH = 595.28
    A4_HEIGHT = 841.89

    @staticmethod
    def get_pdf_info(pdf_path):
        """获取PDF文件详细信息(逐页检测)"""
        try:
            reader = PdfReader(pdf_path)
            page_count = len(reader.pages)
            file_size = os.path.getsize(pdf_path) / 1024  # KB

            # 逐页检测方向和尺寸
            page_info = []
            has_landscape = False
            has_portrait = False

            for i, page in enumerate(reader.pages):
                media_box = page.mediabox
                width = float(media_box.width)
                height = float(media_box.height)
                is_landscape = width > height

                page_info.append({
                    'page_num': i + 1,
                    'width': width,
                    'height': height,
                    'is_landscape': is_landscape
                })

                if is_landscape:
                    has_landscape = True
                else:
                    has_portrait = True

            # 判断整体方向
            if has_landscape and has_portrait:
                overall_orientation = "混合"
            elif has_landscape:
                overall_orientation = "全横版"
            else:
                overall_orientation = "全竖版"

            return {
                'path': pdf_path,
                'name': Path(pdf_path).name,
                'pages': page_count,
                'size': f"{file_size:.2f} KB",
                'page_info': page_info,
                'has_landscape': has_landscape,
                'has_portrait': has_portrait,
                'is_odd': page_count % 2 == 1,
                'reader': reader,
                'orientation': overall_orientation,
                'landscape_pages': [p['page_num'] for p in page_info if p['is_landscape']],
                'landscape_count': len([p for p in page_info if p['is_landscape']])
            }
        except Exception as e:
            print(f"获取PDF信息失败 {pdf_path}: {e}")
            return None

    @staticmethod
    def create_blank_page():
        """创建A4竖版空白页"""
        writer = PdfWriter()
        # 使用A4竖版尺寸
        writer.add_blank_page(width=PDFProcessor.A4_WIDTH, height=PDFProcessor.A4_HEIGHT)
        return writer

    @staticmethod
    def process_pdf(pdf_info, output_path, rotate_landscape=True, add_blank=True):
        """
        处理单个PDF:
        1. 逐页检测并旋转横版页面
        2. 如果是奇数页则添加空白页(A4竖版)

        参数:
            pdf_info: PDF文件信息
            output_path: 输出路径
            rotate_landscape: 是否旋转横版
            add_blank: 是否添加空白页
        """
        try:
            reader = pdf_info['reader']
            writer = PdfWriter()

            rotated_count = 0

            # 逐页处理
            for i, page in enumerate(reader.pages):
                # 检测是否需要旋转
                if rotate_landscape:
                    media_box = page.mediabox
                    width = float(media_box.width)
                    height = float(media_box.height)

                    if width > height:
                        # 旋转90度(逆时针)
                        page.rotate(90)
                        rotated_count += 1
                        print(f"  旋转第 {i + 1} 页 (横版→竖版)")

                writer.add_page(page)

            # 如果是奇数页,添加A4竖版空白页
            if add_blank and pdf_info['is_odd']:
                blank_writer = PDFProcessor.create_blank_page()
                writer.add_page(blank_writer.pages[0])
                print(f"  添加A4竖版空白页 (奇数页→偶数页)")

            # 保存处理后的PDF
            with open(output_path, 'wb') as f:
                writer.write(f)

            return True, rotated_count
        except Exception as e:
            print(f"处理PDF失败: {e}")
            import traceback
            traceback.print_exc()
            return False, 0

    @staticmethod
    def merge_pdfs(pdf_paths, output_path):
        """合并多个PDF文件"""
        try:
            writer = PdfWriter()
            for pdf_path in pdf_paths:
                try:
                    reader = PdfReader(pdf_path)
                    for page in reader.pages:
                        writer.add_page(page)
                except Exception as e:
                    print(f"读取PDF失败 {pdf_path}: {e}")
                    return False

            with open(output_path, 'wb') as f:
                writer.write(f)

            return True
        except Exception as e:
            print(f"合并PDF失败: {e}")
            return False


class PDFToolGUI:
    """PDF工具GUI界面 - 简化稳定版"""

    def __init__(self, root):
        self.root = root
        self.root.title("PDF批量处理工具(横版转竖版-奇数面添加空白页-合并打印)")
        self.root.geometry("1100x750")

        # 设置样式
        style = ttk.Style()
        style.theme_use('clam')

        # 数据变量
        self.pdf_files = []
        self.processed_files = []
        self.output_dir = tk.StringVar()
        self.merge_filename = tk.StringVar(value="合并打印文件.pdf")
        self.status_var = tk.StringVar(value="就绪")

        # 处理选项变量
        self.auto_rotate = tk.BooleanVar(value=True)
        self.add_blank_page = tk.BooleanVar(value=True)

        # 统计变量
        self.landscape_count = tk.StringVar(value="0")

        # 创建界面
        self.create_widgets()

        # 设置默认输出目录
        self.output_dir.set(os.path.expanduser("~\\Documents"))

    def create_widgets(self):
        """创建界面组件"""
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        # ========== 顶部控制区域 ==========
        control_frame = ttk.LabelFrame(main_frame, text="文件管理", padding="10")
        control_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        control_frame.columnconfigure(1, weight=1)

        # 添加文件按钮
        ttk.Button(control_frame, text="📂 添加PDF文件",
                   command=self.add_files).grid(row=0, column=0, padx=(0, 10))

        # 添加文件夹按钮
        ttk.Button(control_frame, text="📁 添加文件夹",
                   command=self.add_folder).grid(row=0, column=1, padx=(0, 10))

        # 清空列表按钮
        ttk.Button(control_frame, text="🗑️ 清空列表",
                   command=self.clear_list).grid(row=0, column=2, padx=(0, 10))

        # 统计信息
        self.count_label = ttk.Label(control_frame, text="文件数: 0")
        self.count_label.grid(row=0, column=3, padx=(20, 0))

        self.pages_label = ttk.Label(control_frame, text="总页数: 0")
        self.pages_label.grid(row=0, column=4, padx=(20, 0))

        self.odd_label = ttk.Label(control_frame, text="奇数页: 0")
        self.odd_label.grid(row=0, column=5, padx=(20, 0))

        self.landscape_label = ttk.Label(control_frame, text="横版页: 0")
        self.landscape_label.grid(row=0, column=6, padx=(20, 0))

        # ========== 处理选项区域 ==========
        option_frame = ttk.LabelFrame(main_frame, text="处理选项", padding="10")
        option_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

        # 处理选项
        ttk.Checkbutton(option_frame, text="🔄 逐页检测并旋转横版为竖版",
                        variable=self.auto_rotate).grid(row=0, column=0, sticky=tk.W, padx=10)

        ttk.Checkbutton(option_frame, text="📄 奇数页添加A4竖版空白页",
                        variable=self.add_blank_page).grid(row=0, column=1, sticky=tk.W, padx=10)

        # 提示信息
        info_label = ttk.Label(option_frame,
                               text="💡 提示:空白页统一使用A4竖版,确保双面打印方向一致",
                               foreground="blue")
        info_label.grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=10, pady=(10, 0))

        # ========== 文件列表区域 ==========
        list_frame = ttk.LabelFrame(main_frame, text="PDF文件列表(逐页检测)", padding="10")
        list_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)

        # 创建Treeview
        columns = ('序号', '文件名', '页数', '尺寸', '方向', '横版页数', '大小', '状态')
        self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=14)

        # 设置列
        self.tree.heading('序号', text='序号')
        self.tree.heading('文件名', text='文件名')
        self.tree.heading('页数', text='页数')
        self.tree.heading('尺寸', text='尺寸(W×H)')
        self.tree.heading('方向', text='整体方向')
        self.tree.heading('横版页数', text='横版页数')
        self.tree.heading('大小', text='大小')
        self.tree.heading('状态', text='状态')

        self.tree.column('序号', width=50, anchor='center')
        self.tree.column('文件名', width=280, anchor='w')
        self.tree.column('页数', width=60, anchor='center')
        self.tree.column('尺寸', width=120, anchor='center')
        self.tree.column('方向', width=80, anchor='center')
        self.tree.column('横版页数', width=80, anchor='center')
        self.tree.column('大小', width=80, anchor='center')
        self.tree.column('状态', width=150, anchor='center')

        # 滚动条
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)

        self.tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))

        # 右键菜单
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="从列表中移除", command=self.remove_selected)
        self.context_menu.add_command(label="查看详细信息", command=self.show_details)
        self.context_menu.add_command(label="查看逐页详情", command=self.show_page_details)
        self.tree.bind("<Button-3>", self.show_context_menu)

        # ========== 底部操作区域 ==========
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        bottom_frame.columnconfigure(1, weight=1)

        # 输出目录
        ttk.Label(bottom_frame, text="输出目录:").grid(row=0, column=0, padx=(0, 10))
        output_entry = ttk.Entry(bottom_frame, textvariable=self.output_dir, width=50)
        output_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        ttk.Button(bottom_frame, text="浏览", command=self.select_output_dir).grid(row=0, column=2)

        # 输出文件名
        ttk.Label(bottom_frame, text="合并文件名:").grid(row=1, column=0, padx=(0, 10), pady=(10, 0))
        ttk.Entry(bottom_frame, textvariable=self.merge_filename, width=50).grid(
            row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=(10, 0))

        # ========== 操作按钮区域 ==========
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

        # 处理并合并按钮
        self.process_btn = ttk.Button(button_frame, text="&#128640; 处理并合并",
                                      command=self.process_and_merge, width=20)
        self.process_btn.pack(side=tk.LEFT, padx=(0, 20))

        # 仅处理不合并按钮
        ttk.Button(button_frame, text="&#128196; 仅处理(不合并)",
                   command=self.process_only, width=20).pack(side=tk.LEFT, padx=(0, 20))

        # 分析按钮
        ttk.Button(button_frame, text="&#128202; 分析PDF",
                   command=self.analyze_pdfs, width=15).pack(side=tk.LEFT, padx=(0, 20))

        # 进度条
        self.progress = ttk.Progressbar(button_frame, mode='indeterminate', length=200)
        self.progress.pack(side=tk.RIGHT, padx=(20, 0))

        # ========== 状态栏 ==========
        status_frame = ttk.Frame(main_frame)
        status_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E))

        ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN,
                  anchor=tk.W).pack(fill=tk.X, pady=(5, 0))

    def add_files(self):
        """添加PDF文件"""
        files = filedialog.askopenfilenames(
            title="选择PDF文件",
            filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
        )
        if files:
            self.add_pdf_files(files)

    def add_folder(self):
        """添加文件夹中的所有PDF"""
        folder = filedialog.askdirectory(title="选择包含PDF文件的文件夹")
        if folder:
            pdf_files = []
            for ext in ['*.pdf', '*.PDF']:
                pdf_files.extend(Path(folder).glob(ext))
            if pdf_files:
                self.add_pdf_files([str(f) for f in pdf_files])
            else:
                messagebox.showinfo("提示", "该文件夹中没有找到PDF文件!")

    def add_pdf_files(self, files):
        """批量添加PDF文件到列表"""
        added_count = 0
        for file_path in files:
            # 检查是否已存在
            if any(f['path'] == file_path for f in self.pdf_files):
                continue

            info = PDFProcessor.get_pdf_info(file_path)
            if info:
                self.pdf_files.append(info)
                added_count += 1

        if added_count > 0:
            self.update_tree()
            self.update_stats()
            self.status_var.set(f"已添加 {added_count} 个文件")
        else:
            self.status_var.set("没有新文件被添加")

    def update_tree(self):
        """更新文件列表显示"""
        for item in self.tree.get_children():
            self.tree.delete(item)

        for i, info in enumerate(self.pdf_files, 1):
            # 状态信息
            status_parts = []
            if info['is_odd']:
                status_parts.append("奇数页")
            if info['has_landscape']:
                if info['has_portrait']:
                    status_parts.append("混合")
                else:
                    status_parts.append("全横版")
            if not status_parts:
                status_parts.append("正常")
            status = "+".join(status_parts)

            # 尺寸显示(使用第一页的尺寸)
            if info['page_info']:
                first_page = info['page_info'][0]
                size_display = f"{first_page['width']:.1f}×{first_page['height']:.1f}"
            else:
                size_display = "未知"

            self.tree.insert('', 'end', values=(
                i,
                info['name'],
                info['pages'],
                size_display,
                info['orientation'],
                info['landscape_count'],
                info['size'],
                status
            ))

    def update_stats(self):
        """更新统计信息"""
        total = len(self.pdf_files)
        total_pages = sum(f['pages'] for f in self.pdf_files)
        odd_count = sum(1 for f in self.pdf_files if f['is_odd'])
        landscape_count = sum(1 for f in self.pdf_files if f['has_landscape'])
        total_landscape_pages = sum(f['landscape_count'] for f in self.pdf_files)

        self.count_label.config(text=f"文件数: {total}")
        self.pages_label.config(text=f"总页数: {total_pages}")
        self.odd_label.config(text=f"奇数页: {odd_count}")
        self.landscape_label.config(text=f"横版页: {total_landscape_pages}")

    def clear_list(self):
        """清空文件列表"""
        if self.tree.get_children():
            if messagebox.askyesno("确认", "确定要清空所有文件列表吗?"):
                self.pdf_files.clear()
                self.update_tree()
                self.update_stats()
                self.status_var.set("列表已清空")

    def remove_selected(self):
        """移除选中的文件"""
        selected = self.tree.selection()
        if not selected:
            return

        if messagebox.askyesno("确认", f"确定要移除选中的 {len(selected)} 个文件吗?"):
            indices = [self.tree.index(item) for item in selected]
            for idx in sorted(indices, reverse=True):
                if idx < len(self.pdf_files):
                    del self.pdf_files[idx]

            self.update_tree()
            self.update_stats()
            self.status_var.set(f"已移除 {len(selected)} 个文件")

    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def show_details(self):
        """显示文件详细信息"""
        selected = self.tree.selection()
        if not selected:
            return

        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.pdf_files):
            info = self.pdf_files[idx]

            detail = f"""文件信息:

文件名: {info['name']}
路径: {info['path']}
总页数: {info['pages']} 页
大小: {info['size']}
整体方向: {info['orientation']}

&#128208; 页面详情:
  横版页数: {info['landscape_count']} 页
  竖版页数: {info['pages'] - info['landscape_count']} 页
  横版页码: {','.join(str(p) for p in info['landscape_pages']) if info['landscape_pages'] else '无'}

&#128196; 状态:
  {'需要添加空白页' if info['is_odd'] else '页数正常'}
  {'包含横版页面,需要旋转' if info['has_landscape'] else '全部竖版'}
            """
            messagebox.showinfo("文件详细信息", detail)

    def show_page_details(self):
        """显示逐页详情"""
        selected = self.tree.selection()
        if not selected:
            return

        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.pdf_files):
            info = self.pdf_files[idx]

            # 构建逐页信息
            page_info = []
            for p in info['page_info']:
                orientation = "横版 &#9888;&#65039;" if p['is_landscape'] else "竖版 &#10003;"
                page_info.append(f"第{p['page_num']:2d}页: {p['width']:6.1f} × {p['height']:6.1f} 点  [{orientation}]")

            detail = f"""&#128196; 逐页详情 - {info['name']}

总页数: {info['pages']} 页
整体方向: {info['orientation']}

{'=' * 55}
{'页号':<6} {'宽度':<10} {'高度':<10} {'方向':<15}
{'=' * 55}
{chr(10).join(page_info)}
{'=' * 55}

&#128161; 处理建议:
{'- 将自动旋转所有横版页面' if info['has_landscape'] else '&#10003; 所有页面均为竖版,无需旋转'}
{'- 将添加A4竖版空白页' if info['is_odd'] else '&#10003; 页数为偶数,无需添加空白页'}
            """
            messagebox.showinfo("逐页详情", detail)

    def select_output_dir(self):
        """选择输出目录"""
        directory = filedialog.askdirectory(title="选择输出目录", initialdir=self.output_dir.get())
        if directory:
            self.output_dir.set(directory)

    def analyze_pdfs(self):
        """分析PDF文件"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        total = len(self.pdf_files)
        total_pages = sum(f['pages'] for f in self.pdf_files)
        odd = sum(1 for f in self.pdf_files if f['is_odd'])
        even = total - odd

        # 方向统计
        all_landscape = sum(1 for f in self.pdf_files if f['has_landscape'] and not f['has_portrait'])
        all_portrait = sum(1 for f in self.pdf_files if not f['has_landscape'])
        mixed = sum(1 for f in self.pdf_files if f['has_landscape'] and f['has_portrait'])

        # 横版页统计
        total_landscape_pages = sum(f['landscape_count'] for f in self.pdf_files)

        # 尺寸统计
        sizes = {}
        for f in self.pdf_files:
            if f['page_info']:
                first_page = f['page_info'][0]
                size_key = f"{first_page['width']:.1f}×{first_page['height']:.1f}"
                sizes[size_key] = sizes.get(size_key, 0) + 1

        size_info = "\n".join([f"  {k}: {v}个" for k, v in sorted(sizes.items())])

        analysis = f"""&#128202; PDF逐页分析报告

&#128193; 文件总数: {total} 个
&#128196; 总页数: {total_pages} 页
&#128196; 横版总页数: {total_landscape_pages} 页

&#128260; 文件方向统计:
  全竖版: {all_portrait} 个 (无需旋转)
  全横版: {all_landscape} 个 (全部需要旋转)
  混合方向: {mixed} 个 (部分页面需要旋转)

&#128196; 页数统计:
  奇数页: {odd} 个 (需要添加空白页)
  偶数页: {even} 个

&#128208; 尺寸统计:
{size_info}

&#128161; 处理建议:
{'- 将逐页检测并旋转横版页面' if total_landscape_pages > 0 else '&#10003; 所有页面均为竖版,无需旋转'}
{'- 将为奇数页PDF添加A4竖版空白页' if odd > 0 else '&#10003; 所有PDF页数均为偶数,无需添加空白页'}
{'&#9989; 建议启用所有处理选项以获得最佳打印效果' if (total_landscape_pages > 0 or odd > 0) else '&#9989; 所有文件已完美,可直接合并打印'}
        """

        messagebox.showinfo("PDF逐页分析报告", analysis)

    def process_and_merge(self):
        """处理并合并PDF"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        output_dir = self.output_dir.get()
        if not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir)
            except:
                messagebox.showerror("错误", "无法创建输出目录!")
                return

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

    def process_only(self):
        """仅处理不合并"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        output_dir = self.output_dir.get()
        if not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir)
            except:
                messagebox.showerror("错误", "无法创建输出目录!")
                return

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

    def _process_and_merge_thread(self):
        """处理并合并的后台线程"""
        self.process_btn.config(state='disabled')
        self.progress.start()
        self.status_var.set("正在处理PDF文件(逐页检测)...")

        try:
            output_dir = self.output_dir.get()
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

            temp_dir = Path(output_dir) / f"temp_{timestamp}"
            temp_dir.mkdir(exist_ok=True)

            # 获取处理参数
            rotate_landscape = self.auto_rotate.get()
            add_blank = self.add_blank_page.get()

            self.processed_files = []
            total = len(self.pdf_files)
            processed_count = 0
            total_rotated_pages = 0
            blank_added_count = 0

            print("\n" + "=" * 60)
            print("开始处理PDF文件...")
            print("=" * 60)

            for i, info in enumerate(self.pdf_files, 1):
                print(f"\n[{i}/{total}] 处理: {info['name']}")
                print(f"  总页数: {info['pages']} 页")
                print(f"  横版页: {info['landscape_count']} 页")
                print(f"  奇数页: {'是' if info['is_odd'] else '否'}")

                # 判断是否需要处理
                need_process = False
                if add_blank and info['is_odd']:
                    need_process = True
                if rotate_landscape and info['has_landscape']:
                    need_process = True

                if need_process:
                    output_name = f"{Path(info['path']).stem}_processed.pdf"
                    output_path = temp_dir / output_name

                    success, rotated = PDFProcessor.process_pdf(
                        info,
                        str(output_path),
                        rotate_landscape,
                        add_blank
                    )

                    if success:
                        self.processed_files.append(str(output_path))
                        processed_count += 1
                        total_rotated_pages += rotated
                        if info['is_odd']:
                            blank_added_count += 1
                        print(f"  &#9989; 处理完成 (旋转{rotated}页, 添加空白页: {info['is_odd']})")
                    else:
                        # 处理失败,使用原文件
                        self.processed_files.append(info['path'])
                        print(f"  &#10060; 处理失败,使用原文件")
                else:
                    self.processed_files.append(info['path'])
                    print(f"  &#9197;&#65039; 无需处理")

            print("\n" + "=" * 60)

            # 合并所有PDF
            if len(self.processed_files) > 1:
                self.status_var.set("正在合并PDF文件...")
                merge_name = self.merge_filename.get().strip()
                if not merge_name:
                    merge_name = "合并打印文件.pdf"
                if not merge_name.endswith('.pdf'):
                    merge_name += '.pdf'

                merge_path = Path(output_dir) / merge_name

                print(f"\n正在合并 {len(self.processed_files)} 个文件...")

                if PDFProcessor.merge_pdfs(self.processed_files, str(merge_path)):
                    # 获取合并后的总页数
                    try:
                        result_reader = PdfReader(str(merge_path))
                        total_pages = len(result_reader.pages)
                    except:
                        total_pages = "未知"

                    self.status_var.set(f"&#9989; 完成!合并文件已保存至: {merge_path}")

                    summary = f"""&#127881; 处理完成!

&#128202; 处理统计:
  &#128193; 处理文件: {processed_count}/{total} 个
  &#128196; 合并后总页数: {total_pages} 页
  &#128260; 旋转横版页: {total_rotated_pages} 页
  &#128196; 添加空白页: {blank_added_count} 个 (统一A4竖版)

&#128193; 输出文件: {merge_path}

&#128161; 所有横版页面已智能旋转,奇数页已补充A4竖版空白页,可直接双面打印!
"""
                    messagebox.showinfo("完成", summary)
                    print(summary)
                else:
                    self.status_var.set("&#10060; 合并失败!")
                    messagebox.showerror("错误", "PDF合并失败,请检查文件是否损坏!")
            elif len(self.processed_files) == 1:
                # 只有一个文件,直接复制
                merge_name = self.merge_filename.get().strip()
                if not merge_name:
                    merge_name = "合并打印文件.pdf"
                if not merge_name.endswith('.pdf'):
                    merge_name += '.pdf'

                merge_path = Path(output_dir) / merge_name
                shutil.copy2(self.processed_files[0], merge_path)

                self.status_var.set(f"&#9989; 完成!文件已保存至: {merge_path}")
                messagebox.showinfo("完成", f"&#127881; 处理完成!只有一个文件,已保存到:\n{merge_path}")

            # 清理临时文件
            try:
                shutil.rmtree(temp_dir)
                print("已清理临时文件")
            except:
                pass

            print("=" * 60)
            print("所有操作完成!")
            print("=" * 60 + "\n")

        except Exception as e:
            self.status_var.set(f"&#10060; 错误: {str(e)}")
            messagebox.showerror("错误", f"处理过程中发生错误:\n{str(e)}")
            import traceback
            traceback.print_exc()
        finally:
            self.progress.stop()
            self.process_btn.config(state='normal')

    def _process_only_thread(self):
        """仅处理的后台线程"""
        self.process_btn.config(state='disabled')
        self.progress.start()
        self.status_var.set("正在处理PDF文件(逐页检测)...")

        try:
            output_dir = self.output_dir.get()

            # 获取处理参数
            rotate_landscape = self.auto_rotate.get()
            add_blank = self.add_blank_page.get()

            processed_count = 0
            total_rotated_pages = 0
            blank_added_count = 0

            total = len(self.pdf_files)

            print("\n" + "=" * 60)
            print("开始处理PDF文件...")
            print("=" * 60)

            for i, info in enumerate(self.pdf_files, 1):
                print(f"\n[{i}/{total}] 处理: {info['name']}")
                print(f"  总页数: {info['pages']} 页")
                print(f"  横版页: {info['landscape_count']} 页")
                print(f"  奇数页: {'是' if info['is_odd'] else '否'}")

                # 判断是否需要处理
                need_process = False
                if add_blank and info['is_odd']:
                    need_process = True
                if rotate_landscape and info['has_landscape']:
                    need_process = True

                if need_process:
                    output_name = f"{Path(info['path']).stem}_processed.pdf"
                    output_path = Path(output_dir) / output_name

                    success, rotated = PDFProcessor.process_pdf(
                        info,
                        str(output_path),
                        rotate_landscape,
                        add_blank
                    )

                    if success:
                        processed_count += 1
                        total_rotated_pages += rotated
                        if info['is_odd']:
                            blank_added_count += 1
                        print(f"  &#9989; 处理完成 (旋转{rotated}页, 添加空白页: {info['is_odd']})")
                    else:
                        print(f"  &#10060; 处理失败")

            print("\n" + "=" * 60)

            self.status_var.set(f"&#9989; 完成!共处理了 {processed_count} 个PDF文件")

            summary = f"""&#127881; 处理完成!

&#128202; 处理统计:
  &#128193; 处理文件: {processed_count} 个
  &#128260; 旋转横版页: {total_rotated_pages} 页
  &#128196; 添加空白页: {blank_added_count} 个 (统一A4竖版)

&#128193; 输出目录: {output_dir}

&#128161; 所有横版页面已智能旋转,奇数页已补充A4竖版空白页!
"""
            messagebox.showinfo("完成", summary)
            print(summary)

        except Exception as e:
            self.status_var.set(f"&#10060; 错误: {str(e)}")
            messagebox.showerror("错误", f"处理过程中发生错误:\n{str(e)}")
            import traceback
            traceback.print_exc()
        finally:
            self.progress.stop()
            self.process_btn.config(state='normal')


def main():
    """主函数"""
    root = tk.Tk()
    app = PDFToolGUI(root)
    root.mainloop()


if __name__ == "__main__":
    try:
        import PyPDF2
    except ImportError:
        print("错误:未安装 PyPDF2 库!")
        print("请运行以下命令安装:")
        print("  pip install PyPDF2")
        input("\n按 Enter 键退出...")
        exit()

    main()


免费评分

参与人数 5吾爱币 +5 热心值 +3 收起 理由
MengYaChen + 1 谢谢@Thanks!
roadroller + 1 + 1 谢谢@Thanks!
yjn866y + 1 + 1 用心讨论,共获提升!
akendy + 1 我很赞同!
anning666 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
zngyngcpu 发表于 2026-7-24 04:45
#coding=UTF-8

# PDF批量处理工具.py
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import PyPDF2
from PyPDF2 import PdfReader, PdfWriter
import threading
from datetime import datetime
import shutil


class PDFProcessor:
    """PDF处理核心类 - 简化稳定版"""

    # 标准A4竖版尺寸(点)
    A4_WIDTH = 595.28
    A4_HEIGHT = 841.89

    # 标准A3竖版尺寸(点)
    A3_WIDTH = 841.89
    A3_HEIGHT = 1190.56

    # 尺寸匹配容差(点)
    SIZE_TOLERANCE = 2.0

    @staticmethod
    def get_pdf_info(pdf_path):
        """获取PDF文件详细信息(逐页检测)"""
        try:
            reader = PdfReader(pdf_path)
            page_count = len(reader.pages)
            file_size = os.path.getsize(pdf_path) / 1024  # KB

            # 逐页检测方向和尺寸
            page_info = []
            has_landscape = False
            has_portrait = False

            for i, page in enumerate(reader.pages):
                media_box = page.mediabox
                width = float(media_box.width)
                height = float(media_box.height)
                is_landscape = width > height

                page_info.append({
                    'page_num': i + 1,
                    'width': width,
                    'height': height,
                    'is_landscape': is_landscape
                })

                if is_landscape:
                    has_landscape = True
                else:
                    has_portrait = True

            # 判断整体方向
            if has_landscape and has_portrait:
                overall_orientation = "混合"
            elif has_landscape:
                overall_orientation = "全横版"
            else:
                overall_orientation = "全竖版"

            return {
                'path': pdf_path,
                'name': Path(pdf_path).name,
                'pages': page_count,
                'size': f"{file_size:.2f} KB",
                'page_info': page_info,
                'has_landscape': has_landscape,
                'has_portrait': has_portrait,
                'is_odd': page_count % 2 == 1,
                'reader': reader,
                'orientation': overall_orientation,
                'landscape_pages': [p['page_num'] for p in page_info if p['is_landscape']],
                'landscape_count': len([p for p in page_info if p['is_landscape']])
            }
        except Exception as e:
            print(f"获取PDF信息失败 {pdf_path}: {e}")
            return None

    @staticmethod
    def is_a4_size(width, height):
        """检查尺寸是否为A4(同时识别竖版/横版)"""
        t = PDFProcessor.SIZE_TOLERANCE
        w, h = PDFProcessor.A4_WIDTH, PDFProcessor.A4_HEIGHT
        return (abs(width - w) < t and abs(height - h) < t) or \
               (abs(width - h) < t and abs(height - w) < t)

    @staticmethod
    def is_a3_size(width, height):
        """检查尺寸是否为A3(同时识别竖版/横版)"""
        t = PDFProcessor.SIZE_TOLERANCE
        w, h = PDFProcessor.A3_WIDTH, PDFProcessor.A3_HEIGHT
        return (abs(width - w) < t and abs(height - h) < t) or \
               (abs(width - h) < t and abs(height - w) < t)

    @staticmethod
    def scale_to_a4(page):
        """将页面缩放为A4尺寸(保持原方向)"""
        media_box = page.mediabox
        width = float(media_box.width)
        height = float(media_box.height)
        if width > height:
            # 横版A4
            page.scale_to(PDFProcessor.A4_HEIGHT, PDFProcessor.A4_WIDTH)
        else:
            # 竖版A4
            page.scale_to(PDFProcessor.A4_WIDTH, PDFProcessor.A4_HEIGHT)

    @staticmethod
    def scale_to_a3(page):
        """将页面缩放为A3尺寸(保持原方向)"""
        media_box = page.mediabox
        width = float(media_box.width)
        height = float(media_box.height)
        if width > height:
            # 横版A3
            page.scale_to(PDFProcessor.A3_HEIGHT, PDFProcessor.A3_WIDTH)
        else:
            # 竖版A3
            page.scale_to(PDFProcessor.A3_WIDTH, PDFProcessor.A3_HEIGHT)

    @staticmethod
    def create_blank_page(size_mode='none'):
        """创建空白页(按尺寸模式选择A4/A3竖版)"""
        writer = PdfWriter()
        if size_mode == 'a3':
            writer.add_blank_page(width=PDFProcessor.A3_WIDTH, height=PDFProcessor.A3_HEIGHT)
        else:
            # 默认使用A4竖版尺寸
            writer.add_blank_page(width=PDFProcessor.A4_WIDTH, height=PDFProcessor.A4_HEIGHT)
        return writer

    @staticmethod
    def process_pdf(pdf_info, output_path, rotate_landscape=True, add_blank=True, size_mode='none'):
        """
        处理单个PDF:
        1. 逐页检测并旋转横版页面
        2. 如果是奇数页则添加空白页(A4/A3竖版)
        3. 按size_mode调整不匹配页面到A3或A4尺寸

        参数:
            pdf_info: PDF文件信息
            output_path: 输出路径
            rotate_landscape: 是否旋转横版
            add_blank: 是否添加空白页
            size_mode: 尺寸调整模式 ('none'/'a3'/'a4')
        """
        try:
            reader = pdf_info['reader']
            writer = PdfWriter()

            rotated_count = 0
            scaled_count = 0

            # 逐页处理
            for i, page in enumerate(reader.pages):
                media_box = page.mediabox
                width = float(media_box.width)
                height = float(media_box.height)
                is_landscape = width > height

                # 尺寸调整(基于原始方向,保持横/竖不变)
                if size_mode == 'a3' and not PDFProcessor.is_a3_size(width, height):
                    PDFProcessor.scale_to_a3(page)
                    scaled_count += 1
                    print(f"  调整第 {i + 1} 页尺寸为A3 ({width:.1f}×{height:.1f} → A3)")
                elif size_mode == 'a4' and not PDFProcessor.is_a4_size(width, height):
                    PDFProcessor.scale_to_a4(page)
                    scaled_count += 1
                    print(f"  调整第 {i + 1} 页尺寸为A4 ({width:.1f}×{height:.1f} → A4)")

                # 检测是否需要旋转(基于原始方向)
                if rotate_landscape and is_landscape:
                    # 旋转90度(逆时针)
                    page.rotate(90)
                    rotated_count += 1
                    print(f"  旋转第 {i + 1} 页 (横版→竖版)")

                writer.add_page(page)

            # 如果是奇数页,添加空白页(与目标尺寸一致)
            if add_blank and pdf_info['is_odd']:
                blank_writer = PDFProcessor.create_blank_page(size_mode)
                writer.add_page(blank_writer.pages[0])
                size_name = 'A3' if size_mode == 'a3' else 'A4'
                print(f"  添加{size_name}竖版空白页 (奇数页→偶数页)")

            # 保存处理后的PDF
            with open(output_path, 'wb') as f:
                writer.write(f)

            return True, rotated_count, scaled_count
        except Exception as e:
            print(f"处理PDF失败: {e}")
            import traceback
            traceback.print_exc()
            return False, 0, 0

    @staticmethod
    def merge_pdfs(pdf_paths, output_path):
        """合并多个PDF文件"""
        try:
            writer = PdfWriter()
            for pdf_path in pdf_paths:
                try:
                    reader = PdfReader(pdf_path)
                    for page in reader.pages:
                        writer.add_page(page)
                except Exception as e:
                    print(f"读取PDF失败 {pdf_path}: {e}")
                    return False

            with open(output_path, 'wb') as f:
                writer.write(f)

            return True
        except Exception as e:
            print(f"合并PDF失败: {e}")
            return False


class PDFToolGUI:
    """PDF工具GUI界面 - 简化稳定版"""

    def __init__(self, root):
        self.root = root
        self.root.title("PDF批量处理工具(横版转竖版-奇数面添加空白页-合并打印)")
        self.root.geometry("1100x750")

        # 设置样式
        style = ttk.Style()
        style.theme_use('clam')

        # 数据变量
        self.pdf_files = []
        self.processed_files = []
        self.output_dir = tk.StringVar()
        self.merge_filename = tk.StringVar(value="合并打印文件.pdf")
        self.status_var = tk.StringVar(value="就绪")

        # 处理选项变量
        self.auto_rotate = tk.BooleanVar(value=True)
        self.add_blank_page = tk.BooleanVar(value=True)
        self.convert_a3 = tk.BooleanVar(value=False)
        self.convert_a4 = tk.BooleanVar(value=False)
        # 互斥:选中A3时取消A4,反之亦然
        self.convert_a3.trace_add('write', self._on_convert_a3_changed)
        self.convert_a4.trace_add('write', self._on_convert_a4_changed)

        # 统计变量
        self.landscape_count = tk.StringVar(value="0")

        # 创建界面
        self.create_widgets()

        # 设置默认输出目录
        self.output_dir.set(os.path.expanduser("~\\Documents"))

    def create_widgets(self):
        """创建界面组件"""
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        # ========== 顶部控制区域 ==========
        control_frame = ttk.LabelFrame(main_frame, text="文件管理", padding="10")
        control_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        control_frame.columnconfigure(1, weight=1)

        # 添加文件按钮
        ttk.Button(control_frame, text="添加PDF文件",
                   command=self.add_files).grid(row=0, column=0, padx=(0, 10))

        # 添加文件夹按钮
        ttk.Button(control_frame, text="添加文件夹",
                   command=self.add_folder).grid(row=0, column=1, padx=(0, 10))

        # 改A3 / 改A4 复选框(互斥,置于清空列表之前)
        ttk.Checkbutton(control_frame, text="改A3",
                        variable=self.convert_a3).grid(row=0, column=2, padx=(0, 10))
        ttk.Checkbutton(control_frame, text="改A4",
                        variable=self.convert_a4).grid(row=0, column=3, padx=(0, 10))

        # 清空列表按钮
        ttk.Button(control_frame, text="清空列表",
                   command=self.clear_list).grid(row=0, column=4, padx=(0, 10))

        # 统计信息
        self.count_label = ttk.Label(control_frame, text="文件数: 0")
        self.count_label.grid(row=0, column=5, padx=(20, 0))

        self.pages_label = ttk.Label(control_frame, text="总页数: 0")
        self.pages_label.grid(row=0, column=6, padx=(20, 0))

        self.odd_label = ttk.Label(control_frame, text="奇数页: 0")
        self.odd_label.grid(row=0, column=7, padx=(20, 0))

        self.landscape_label = ttk.Label(control_frame, text="横版页: 0")
        self.landscape_label.grid(row=0, column=8, padx=(20, 0))

        # ========== 处理选项区域 ==========
        option_frame = ttk.LabelFrame(main_frame, text="处理选项", padding="10")
        option_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

        # 处理选项
        ttk.Checkbutton(option_frame, text="逐页检测并旋转横版为竖版",
                        variable=self.auto_rotate).grid(row=0, column=0, sticky=tk.W, padx=10)

        ttk.Checkbutton(option_frame, text="奇数页添加A4竖版空白页",
                        variable=self.add_blank_page).grid(row=0, column=1, sticky=tk.W, padx=10)

        # 提示信息
        info_label = ttk.Label(option_frame,
                               text="提示:空白页统一使用A4竖版,确保双面打印方向一致",
                               foreground="blue")
        info_label.grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=10, pady=(10, 0))

        # ========== 文件列表区域 ==========
        list_frame = ttk.LabelFrame(main_frame, text="PDF文件列表(逐页检测)", padding="10")
        list_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)

        # 创建Treeview
        columns = ('序号', '文件名', '页数', '尺寸', '方向', '横版页数', '大小', '状态')
        self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=14)

        # 设置列
        self.tree.heading('序号', text='序号')
        self.tree.heading('文件名', text='文件名')
        self.tree.heading('页数', text='页数')
        self.tree.heading('尺寸', text='尺寸(W×H)')
        self.tree.heading('方向', text='整体方向')
        self.tree.heading('横版页数', text='横版页数')
        self.tree.heading('大小', text='大小')
        self.tree.heading('状态', text='状态')

        self.tree.column('序号', width=50, anchor='center')
        self.tree.column('文件名', width=280, anchor='w')
        self.tree.column('页数', width=60, anchor='center')
        self.tree.column('尺寸', width=120, anchor='center')
        self.tree.column('方向', width=80, anchor='center')
        self.tree.column('横版页数', width=80, anchor='center')
        self.tree.column('大小', width=80, anchor='center')
        self.tree.column('状态', width=150, anchor='center')

        # 滚动条
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)

        self.tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))

        # 右键菜单
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="从列表中移除", command=self.remove_selected)
        self.context_menu.add_command(label="查看详细信息", command=self.show_details)
        self.context_menu.add_command(label="查看逐页详情", command=self.show_page_details)
        self.tree.bind("<Button-3>", self.show_context_menu)

        # ========== 底部操作区域 ==========
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        bottom_frame.columnconfigure(1, weight=1)

        # 输出目录
        ttk.Label(bottom_frame, text="输出目录:").grid(row=0, column=0, padx=(0, 10))
        output_entry = ttk.Entry(bottom_frame, textvariable=self.output_dir, width=50)
        output_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        ttk.Button(bottom_frame, text="浏览", command=self.select_output_dir).grid(row=0, column=2)

        # 输出文件名
        ttk.Label(bottom_frame, text="合并文件名:").grid(row=1, column=0, padx=(0, 10), pady=(10, 0))
        ttk.Entry(bottom_frame, textvariable=self.merge_filename, width=50).grid(
            row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=(10, 0))

        # ========== 操作按钮区域 ==========
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

        # 处理并合并按钮
        self.process_btn = ttk.Button(button_frame, text="处理并合并",
                                      command=self.process_and_merge, width=20)
        self.process_btn.pack(side=tk.LEFT, padx=(0, 20))

        # 仅处理不合并按钮
        ttk.Button(button_frame, text="仅处理(不合并)",
                   command=self.process_only, width=20).pack(side=tk.LEFT, padx=(0, 20))

        # 分析按钮
        ttk.Button(button_frame, text="分析PDF",
                   command=self.analyze_pdfs, width=15).pack(side=tk.LEFT, padx=(0, 20))

        # 进度条
        self.progress = ttk.Progressbar(button_frame, mode='indeterminate', length=200)
        self.progress.pack(side=tk.RIGHT, padx=(20, 0))

        # ========== 状态栏 ==========
        status_frame = ttk.Frame(main_frame)
        status_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E))

        ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN,
                  anchor=tk.W).pack(fill=tk.X, pady=(5, 0))

    def add_files(self):
        """添加PDF文件"""
        files = filedialog.askopenfilenames(
            title="选择PDF文件",
            filetypes=[("PDF文件", "*.pdf"), ("所有文件", "*.*")]
        )
        if files:
            self.add_pdf_files(files)

    def add_folder(self):
        """添加文件夹中的所有PDF"""
        folder = filedialog.askdirectory(title="选择包含PDF文件的文件夹")
        if folder:
            pdf_files = []
            for ext in ['*.pdf', '*.PDF']:
                pdf_files.extend(Path(folder).glob(ext))
            if pdf_files:
                self.add_pdf_files([str(f) for f in pdf_files])
            else:
                messagebox.showinfo("提示", "该文件夹中没有找到PDF文件!")

    def add_pdf_files(self, files):
        """批量添加PDF文件到列表"""
        added_count = 0
        for file_path in files:
            # 检查是否已存在
            if any(f['path'] == file_path for f in self.pdf_files):
                continue

            info = PDFProcessor.get_pdf_info(file_path)
            if info:
                self.pdf_files.append(info)
                added_count += 1

        if added_count > 0:
            self.update_tree()
            self.update_stats()
            self.status_var.set(f"已添加 {added_count} 个文件")
        else:
            self.status_var.set("没有新文件被添加")

    def update_tree(self):
        """更新文件列表显示"""
        for item in self.tree.get_children():
            self.tree.delete(item)

        for i, info in enumerate(self.pdf_files, 1):
            # 状态信息
            status_parts = []
            if info['is_odd']:
                status_parts.append("奇数页")
            if info['has_landscape']:
                if info['has_portrait']:
                    status_parts.append("混合")
                else:
                    status_parts.append("全横版")
            if not status_parts:
                status_parts.append("正常")
            status = "+".join(status_parts)

            # 尺寸显示(使用第一页的尺寸)
            if info['page_info']:
                first_page = info['page_info'][0]
                size_display = f"{first_page['width']:.1f}×{first_page['height']:.1f}"
            else:
                size_display = "未知"

            self.tree.insert('', 'end', values=(
                i,
                info['name'],
                info['pages'],
                size_display,
                info['orientation'],
                info['landscape_count'],
                info['size'],
                status
            ))

    def update_stats(self):
        """更新统计信息"""
        total = len(self.pdf_files)
        total_pages = sum(f['pages'] for f in self.pdf_files)
        odd_count = sum(1 for f in self.pdf_files if f['is_odd'])
        landscape_count = sum(1 for f in self.pdf_files if f['has_landscape'])
        total_landscape_pages = sum(f['landscape_count'] for f in self.pdf_files)

        self.count_label.config(text=f"文件数: {total}")
        self.pages_label.config(text=f"总页数: {total_pages}")
        self.odd_label.config(text=f"奇数页: {odd_count}")
        self.landscape_label.config(text=f"横版页: {total_landscape_pages}")

    def clear_list(self):
        """清空文件列表"""
        if self.tree.get_children():
            if messagebox.askyesno("确认", "确定要清空所有文件列表吗?"):
                self.pdf_files.clear()
                self.update_tree()
                self.update_stats()
                self.status_var.set("列表已清空")

    def _on_convert_a3_changed(self, *args):
        """选中改A3时,自动取消改A4"""
        if self.convert_a3.get() and self.convert_a4.get():
            self.convert_a4.set(False)

    def _on_convert_a4_changed(self, *args):
        """选中改A4时,自动取消改A3"""
        if self.convert_a4.get() and self.convert_a3.get():
            self.convert_a3.set(False)

    def get_size_mode(self):
        """获取当前尺寸调整模式:'a3' / 'a4' / 'none'"""
        if self.convert_a3.get():
            return 'a3'
        if self.convert_a4.get():
            return 'a4'
        return 'none'

    def remove_selected(self):
        """移除选中的文件"""
        selected = self.tree.selection()
        if not selected:
            return

        if messagebox.askyesno("确认", f"确定要移除选中的 {len(selected)} 个文件吗?"):
            indices = [self.tree.index(item) for item in selected]
            for idx in sorted(indices, reverse=True):
                if idx < len(self.pdf_files):
                    del self.pdf_files[idx]

            self.update_tree()
            self.update_stats()
            self.status_var.set(f"已移除 {len(selected)} 个文件")

    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)

    def show_details(self):
        """显示文件详细信息"""
        selected = self.tree.selection()
        if not selected:
            return

        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.pdf_files):
            info = self.pdf_files[idx]

            detail = f"""文件信息:

文件名: {info['name']}
路径: {info['path']}
总页数: {info['pages']} 页
大小: {info['size']}
整体方向: {info['orientation']}

&#128208; 页面详情:
  横版页数: {info['landscape_count']} 页
  竖版页数: {info['pages'] - info['landscape_count']} 页
  横版页码: {','.join(str(p) for p in info['landscape_pages']) if info['landscape_pages'] else '无'}

&#128196; 状态:
  {'需要添加空白页' if info['is_odd'] else '页数正常'}
  {'包含横版页面,需要旋转' if info['has_landscape'] else '全部竖版'}
            """
            messagebox.showinfo("文件详细信息", detail)

    def show_page_details(self):
        """显示逐页详情"""
        selected = self.tree.selection()
        if not selected:
            return

        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.pdf_files):
            info = self.pdf_files[idx]

            # 构建逐页信息
            page_info = []
            for p in info['page_info']:
                orientation = "横版 &#9888;&#65039;" if p['is_landscape'] else "竖版 &#10003;"
                page_info.append(f"第{p['page_num']:2d}页: {p['width']:6.1f} × {p['height']:6.1f} 点  [{orientation}]")

            detail = f"""&#128196; 逐页详情 - {info['name']}

总页数: {info['pages']} 页
整体方向: {info['orientation']}

{'=' * 55}
{'页号':<6} {'宽度':<10} {'高度':<10} {'方向':<15}
{'=' * 55}
{chr(10).join(page_info)}
{'=' * 55}

&#128161; 处理建议:
{'- 将自动旋转所有横版页面' if info['has_landscape'] else '&#10003; 所有页面均为竖版,无需旋转'}
{'- 将添加A4竖版空白页' if info['is_odd'] else '&#10003; 页数为偶数,无需添加空白页'}
            """
            messagebox.showinfo("逐页详情", detail)

    def select_output_dir(self):
        """选择输出目录"""
        directory = filedialog.askdirectory(title="选择输出目录", initialdir=self.output_dir.get())
        if directory:
            self.output_dir.set(directory)

    def analyze_pdfs(self):
        """分析PDF文件"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        total = len(self.pdf_files)
        total_pages = sum(f['pages'] for f in self.pdf_files)
        odd = sum(1 for f in self.pdf_files if f['is_odd'])
        even = total - odd

        # 方向统计
        all_landscape = sum(1 for f in self.pdf_files if f['has_landscape'] and not f['has_portrait'])
        all_portrait = sum(1 for f in self.pdf_files if not f['has_landscape'])
        mixed = sum(1 for f in self.pdf_files if f['has_landscape'] and f['has_portrait'])

        # 横版页统计
        total_landscape_pages = sum(f['landscape_count'] for f in self.pdf_files)

        # 尺寸统计
        sizes = {}
        for f in self.pdf_files:
            if f['page_info']:
                first_page = f['page_info'][0]
                size_key = f"{first_page['width']:.1f}×{first_page['height']:.1f}"
                sizes[size_key] = sizes.get(size_key, 0) + 1

        size_info = "\n".join([f"  {k}: {v}个" for k, v in sorted(sizes.items())])

        analysis = f"""&#128202; PDF逐页分析报告

&#128193; 文件总数: {total} 个
&#128196; 总页数: {total_pages} 页
&#128196; 横版总页数: {total_landscape_pages} 页

&#128260; 文件方向统计:
  全竖版: {all_portrait} 个 (无需旋转)
  全横版: {all_landscape} 个 (全部需要旋转)
  混合方向: {mixed} 个 (部分页面需要旋转)

&#128196; 页数统计:
  奇数页: {odd} 个 (需要添加空白页)
  偶数页: {even} 个

&#128208; 尺寸统计:
{size_info}

&#128161; 处理建议:
{'- 将逐页检测并旋转横版页面' if total_landscape_pages > 0 else '&#10003; 所有页面均为竖版,无需旋转'}
{'- 将为奇数页PDF添加A4竖版空白页' if odd > 0 else '&#10003; 所有PDF页数均为偶数,无需添加空白页'}
{'&#9989; 建议启用所有处理选项以获得最佳打印效果' if (total_landscape_pages > 0 or odd > 0) else '&#9989; 所有文件已完美,可直接合并打印'}
        """

        messagebox.showinfo("PDF逐页分析报告", analysis)

    def process_and_merge(self):
        """处理并合并PDF"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        output_dir = self.output_dir.get()
        if not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir)
            except:
                messagebox.showerror("错误", "无法创建输出目录!")
                return

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

    def process_only(self):
        """仅处理不合并"""
        if not self.pdf_files:
            messagebox.showwarning("提示", "请先添加PDF文件!")
            return

        output_dir = self.output_dir.get()
        if not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir)
            except:
                messagebox.showerror("错误", "无法创建输出目录!")
                return

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

    def _process_and_merge_thread(self):
        """处理并合并的后台线程"""
        self.process_btn.config(state='disabled')
        self.progress.start()
        self.status_var.set("正在处理PDF文件(逐页检测)...")

        try:
            output_dir = self.output_dir.get()
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

            temp_dir = Path(output_dir) / f"temp_{timestamp}"
            temp_dir.mkdir(exist_ok=True)

            # 获取处理参数
            rotate_landscape = self.auto_rotate.get()
            add_blank = self.add_blank_page.get()
            size_mode = self.get_size_mode()
            size_name = 'A3' if size_mode == 'a3' else ('A4' if size_mode == 'a4' else '不调整')

            self.processed_files = []
            total = len(self.pdf_files)
            processed_count = 0
            total_rotated_pages = 0
            total_scaled_pages = 0
            blank_added_count = 0

            print("\n" + "=" * 60)
            print(f"开始处理PDF文件... (尺寸调整: {size_name})")
            print("=" * 60)

            for i, info in enumerate(self.pdf_files, 1):
                print(f"\n[{i}/{total}] 处理: {info['name']}")
                print(f"  总页数: {info['pages']} 页")
                print(f"  横版页: {info['landscape_count']} 页")
                print(f"  奇数页: {'是' if info['is_odd'] else '否'}")

                # 判断是否需要处理
                need_process = False
                if add_blank and info['is_odd']:
                    need_process = True
                if rotate_landscape and info['has_landscape']:
                    need_process = True
                if size_mode != 'none':
                    need_process = True

                if need_process:
                    output_name = f"{Path(info['path']).stem}_processed.pdf"
                    output_path = temp_dir / output_name

                    success, rotated, scaled = PDFProcessor.process_pdf(
                        info,
                        str(output_path),
                        rotate_landscape,
                        add_blank,
                        size_mode
                    )

                    if success:
                        self.processed_files.append(str(output_path))
                        processed_count += 1
                        total_rotated_pages += rotated
                        total_scaled_pages += scaled
                        if info['is_odd']:
                            blank_added_count += 1
                        print(f"  &#9989; 处理完成 (旋转{rotated}页, 调整尺寸{scaled}页, 添加空白页: {info['is_odd']})")
                    else:
                        # 处理失败,使用原文件
                        self.processed_files.append(info['path'])
                        print(f"  &#10060; 处理失败,使用原文件")
                else:
                    self.processed_files.append(info['path'])
                    print(f"  &#9197;&#65039; 无需处理")

            print("\n" + "=" * 60)

            # 合并所有PDF
            if len(self.processed_files) > 1:
                self.status_var.set("正在合并PDF文件...")
                merge_name = self.merge_filename.get().strip()
                if not merge_name:
                    merge_name = "合并打印文件.pdf"
                if not merge_name.endswith('.pdf'):
                    merge_name += '.pdf'

                merge_path = Path(output_dir) / merge_name

                print(f"\n正在合并 {len(self.processed_files)} 个文件...")

                if PDFProcessor.merge_pdfs(self.processed_files, str(merge_path)):
                    # 获取合并后的总页数
                    try:
                        result_reader = PdfReader(str(merge_path))
                        total_pages = len(result_reader.pages)
                    except:
                        total_pages = "未知"

                    self.status_var.set(f"&#9989; 完成!合并文件已保存至: {merge_path}")

                    summary = f"""&#127881; 处理完成!

&#128202; 处理统计:
  &#128193; 处理文件: {processed_count}/{total} 个
  &#128196; 合并后总页数: {total_pages} 页
  &#128260; 旋转横版页: {total_rotated_pages} 页
  &#128207; 调整尺寸页: {total_scaled_pages} 页 (目标: {size_name})
  &#128196; 添加空白页: {blank_added_count} 个 (统一{size_name if size_mode != 'none' else 'A4'}竖版)

&#128193; 输出文件: {merge_path}

&#128161; 所有横版页面已智能旋转,奇数页已补充竖版空白页,可直接双面打印!
"""
                    messagebox.showinfo("完成", summary)
                    print(summary)
                else:
                    self.status_var.set("&#10060; 合并失败!")
                    messagebox.showerror("错误", "PDF合并失败,请检查文件是否损坏!")
            elif len(self.processed_files) == 1:
                # 只有一个文件,直接复制
                merge_name = self.merge_filename.get().strip()
                if not merge_name:
                    merge_name = "合并打印文件.pdf"
                if not merge_name.endswith('.pdf'):
                    merge_name += '.pdf'

                merge_path = Path(output_dir) / merge_name
                shutil.copy2(self.processed_files[0], merge_path)

                self.status_var.set(f"&#9989; 完成!文件已保存至: {merge_path}")
                messagebox.showinfo("完成", f"&#127881; 处理完成!只有一个文件,已保存到:\n{merge_path}")

            # 清理临时文件
            try:
                shutil.rmtree(temp_dir)
                print("已清理临时文件")
            except:
                pass

            print("=" * 60)
            print("所有操作完成!")
            print("=" * 60 + "\n")

        except Exception as e:
            self.status_var.set(f"&#10060; 错误: {str(e)}")
            messagebox.showerror("错误", f"处理过程中发生错误:\n{str(e)}")
            import traceback
            traceback.print_exc()
        finally:
            self.progress.stop()
            self.process_btn.config(state='normal')

    def _process_only_thread(self):
        """仅处理的后台线程"""
        self.process_btn.config(state='disabled')
        self.progress.start()
        self.status_var.set("正在处理PDF文件(逐页检测)...")

        try:
            output_dir = self.output_dir.get()

            # 获取处理参数
            rotate_landscape = self.auto_rotate.get()
            add_blank = self.add_blank_page.get()
            size_mode = self.get_size_mode()
            size_name = 'A3' if size_mode == 'a3' else ('A4' if size_mode == 'a4' else '不调整')

            processed_count = 0
            total_rotated_pages = 0
            total_scaled_pages = 0
            blank_added_count = 0

            total = len(self.pdf_files)

            print("\n" + "=" * 60)
            print(f"开始处理PDF文件... (尺寸调整: {size_name})")
            print("=" * 60)

            for i, info in enumerate(self.pdf_files, 1):
                print(f"\n[{i}/{total}] 处理: {info['name']}")
                print(f"  总页数: {info['pages']} 页")
                print(f"  横版页: {info['landscape_count']} 页")
                print(f"  奇数页: {'是' if info['is_odd'] else '否'}")

                # 判断是否需要处理
                need_process = False
                if add_blank and info['is_odd']:
                    need_process = True
                if rotate_landscape and info['has_landscape']:
                    need_process = True
                if size_mode != 'none':
                    need_process = True

                if need_process:
                    output_name = f"{Path(info['path']).stem}_processed.pdf"
                    output_path = Path(output_dir) / output_name

                    success, rotated, scaled = PDFProcessor.process_pdf(
                        info,
                        str(output_path),
                        rotate_landscape,
                        add_blank,
                        size_mode
                    )

                    if success:
                        processed_count += 1
                        total_rotated_pages += rotated
                        total_scaled_pages += scaled
                        if info['is_odd']:
                            blank_added_count += 1
                        print(f"  &#9989; 处理完成 (旋转{rotated}页, 调整尺寸{scaled}页, 添加空白页: {info['is_odd']})")
                    else:
                        print(f"  &#10060; 处理失败")

            print("\n" + "=" * 60)

            self.status_var.set(f"&#9989; 完成!共处理了 {processed_count} 个PDF文件")

            summary = f"""&#127881; 处理完成!

&#128202; 处理统计:
  &#128193; 处理文件: {processed_count} 个
  &#128260; 旋转横版页: {total_rotated_pages} 页
  &#128207; 调整尺寸页: {total_scaled_pages} 页 (目标: {size_name})
  &#128196; 添加空白页: {blank_added_count} 个 (统一{size_name if size_mode != 'none' else 'A4'}竖版)

&#128193; 输出目录: {output_dir}

&#128161; 所有横版页面已智能旋转,奇数页已补充竖版空白页!
"""
            messagebox.showinfo("完成", summary)
            print(summary)

        except Exception as e:
            self.status_var.set(f"&#10060; 错误: {str(e)}")
            messagebox.showerror("错误", f"处理过程中发生错误:\n{str(e)}")
            import traceback
            traceback.print_exc()
        finally:
            self.progress.stop()
            self.process_btn.config(state='normal')


def main():
    """主函数"""
    root = tk.Tk()
    app = PDFToolGUI(root)
    root.mainloop()


if __name__ == "__main__":
    try:
        import PyPDF2
    except ImportError:
        print("错误:未安装 PyPDF2 库!")
        print("请运行以下命令安装:")
        print("  pip install PyPDF2")
        input("\n按 Enter 键退出...")
        exit()

    main()
3#
75233a20260721 发表于 2026-7-24 05:15
4#
DSWuKong 发表于 2026-7-24 06:34
5#
lekit 发表于 2026-7-24 07:30
感谢分享,,,
6#
wnjshan 发表于 2026-7-24 07:34
楼主有心了,感谢分享
7#
hybcrp 发表于 2026-7-24 07:58
我正好需要这样的工具,感谢楼主
8#
hjk 发表于 2026-7-24 08:01
我做完了 而且变成 1打2,一张4页
9#
jerryhonghu 发表于 2026-7-24 08:08
感谢楼主的分享
10#
1h2yd3 发表于 2026-7-24 08:09
这个好!感谢分享,PDF批量合并真的很重要!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-24 22:35

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表