diff --git a/docs/OCRmyPDF-GUI项目演示.pptx b/docs/OCRmyPDF-GUI项目演示.pptx new file mode 100644 index 0000000..87bb611 Binary files /dev/null and b/docs/OCRmyPDF-GUI项目演示.pptx differ diff --git a/docs/开源软件维护报告文档.docx b/docs/开源软件维护报告文档.docx index 5cde4ff..a36ce14 100644 Binary files a/docs/开源软件维护报告文档.docx and b/docs/开源软件维护报告文档.docx differ diff --git a/src/core/config.py b/src/core/config.py index 557a2eb..548ace4 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -4,32 +4,57 @@ from pathlib import Path import logging class Config: - """配置管理类,负责加载和保存应用程序配置""" + """ + 配置管理类 + + 负责应用程序配置的加载、保存和访问操作。 + 管理用户偏好设置、最近使用的文件和目录以及默认OCR选项。 + 使用JSON格式存储配置,保存在用户主目录的.ocrmypdf-gui文件夹中。 + + 属性: + logger: 日志记录器,用于记录配置操作的日志 + config_dir: 配置目录路径 + config_file: 配置文件路径 + default_config: 默认配置字典 + current_config: 当前使用的配置字典 + """ def __init__(self): + """ + 初始化配置管理器 + + 设置配置文件路径、初始化默认配置,并从磁盘加载现有配置(如果存在)。 + 如果配置文件不存在,将使用默认配置并创建新的配置文件。 + """ self.logger = logging.getLogger(__name__) self.config_dir = Path.home() / ".ocrmypdf-gui" self.config_file = self.config_dir / "config.json" self.default_config = { - "recent_files": [], - "recent_output_dirs": [], - "default_options": { - "deskew": True, - "rotate_pages": True, - "clean": False, - "output_type": "pdfa", - "jobs": 4 + "recent_files": [], # 最近使用的文件列表 + "recent_output_dirs": [], # 最近使用的输出目录列表 + "default_options": { # 默认OCR选项 + "deskew": True, # 自动校正倾斜页面 + "rotate_pages": True, # 自动旋转页面 + "clean": False, # 清理图像 + "output_type": "pdfa", # 输出文件类型 + "jobs": 4 # 并行处理任务数 }, - "ui": { - "theme": "system", - "language": "zh_CN" + "ui": { # 用户界面设置 + "theme": "system", # 主题(跟随系统、亮色、暗色) + "language": "zh_CN" # 界面语言 } } self.current_config = self.default_config.copy() self.load_config() def load_config(self): - """加载配置文件""" + """ + 从磁盘加载配置文件 + + 如果配置目录不存在,则创建该目录。 + 如果配置文件存在,则读取并与默认配置合并。 + 如果配置文件不存在或加载失败,则使用默认配置。 + """ if not self.config_dir.exists(): self.config_dir.mkdir(parents=True, exist_ok=True) @@ -47,7 +72,12 @@ class Config: self.save_config() def save_config(self): - """保存配置文件""" + """ + 保存配置到磁盘 + + 将当前配置以JSON格式写入配置文件。 + 如果保存过程中出现错误,将记录错误日志。 + """ try: with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(self.current_config, f, indent=2, ensure_ascii=False) @@ -56,7 +86,16 @@ class Config: self.logger.error(f"保存配置文件出错: {e}") def _merge_config(self, target, source): - """递归合并配置字典""" + """ + 递归合并配置字典 + + 将source字典中的值合并到target字典中,保留原有结构。 + 对于嵌套字典,递归合并内部结构。 + + Args: + target: 目标字典,合并结果将存储在此 + source: 源字典,其值将合并到目标字典 + """ for key, value in source.items(): if key in target and isinstance(target[key], dict) and isinstance(value, dict): self._merge_config(target[key], value) @@ -64,14 +103,17 @@ class Config: target[key] = value def get(self, key, default=None): - """获取配置项 + """ + 获取配置项值 + + 支持使用点号分隔的多级键名访问嵌套配置项。 Args: - key: 配置项键名,支持点号分隔的多级键名,如 'ui.theme' + key: 配置项键名,如'ui.theme'或'default_options.deskew' default: 如果配置项不存在,返回的默认值 Returns: - 配置项的值 + 配置项的值,如果不存在则返回默认值 """ keys = key.split('.') value = self.current_config @@ -85,11 +127,15 @@ class Config: return value def set(self, key, value): - """设置配置项 + """ + 设置配置项值 + + 支持使用点号分隔的多级键名设置嵌套配置项。 + 设置后会自动保存配置到磁盘。 Args: - key: 配置项键名,支持点号分隔的多级键名,如 'ui.theme' - value: 配置项的值 + key: 配置项键名,如'ui.theme'或'default_options.deskew' + value: 要设置的值 """ keys = key.split('.') target = self.current_config @@ -103,7 +149,11 @@ class Config: self.save_config() def add_recent_file(self, file_path): - """添加最近使用的文件 + """ + 添加最近使用的文件 + + 如果文件已在列表中,则将其移到列表首位。 + 保留最近使用的10个文件。 Args: file_path: 文件路径 @@ -116,7 +166,11 @@ class Config: self.set('recent_files', recent_files[:10]) def add_recent_output_dir(self, dir_path): - """添加最近使用的输出目录 + """ + 添加最近使用的输出目录 + + 如果目录已在列表中,则将其移到列表首位。 + 保留最近使用的10个目录。 Args: dir_path: 目录路径 diff --git a/src/core/ocr_engine.py b/src/core/ocr_engine.py index ff6f154..3d5f1ca 100644 --- a/src/core/ocr_engine.py +++ b/src/core/ocr_engine.py @@ -5,9 +5,26 @@ import sys import os class OCREngine: - """OCR引擎类,封装OCRmyPDF的调用""" + """ + OCR引擎类 + + 封装OCRmyPDF命令行工具的调用,提供PDF文件的OCR处理功能。 + 负责检测OCRmyPDF和Tesseract的可用性、获取支持的语言列表、 + 处理单个文件和批量处理多个文件,以及处理各种错误状态。 + + 属性: + logger: 日志记录器 + available_languages: 系统中可用的Tesseract语言包列表 + last_error: 最近一次处理错误的详细信息 + """ def __init__(self): + """ + 初始化OCR引擎 + + 检查OCRmyPDF命令行工具是否可用,并获取系统中已安装的Tesseract语言包列表。 + 如果OCRmyPDF不可用,将记录错误并将可用语言列表设为空。 + """ self.logger = logging.getLogger(__name__) # 检查命令行工具是否可用 try: @@ -30,7 +47,14 @@ class OCREngine: self.available_languages = [] def get_available_languages(self): - """获取系统中已安装的Tesseract语言包列表""" + """ + 获取系统中已安装的Tesseract语言包列表 + + 通过调用tesseract命令的--list-langs选项获取系统中已安装的所有语言包。 + + Returns: + list: 已安装的语言代码列表,如果获取失败则返回空列表 + """ try: result = subprocess.run( ["tesseract", "--list-langs"], @@ -48,7 +72,18 @@ class OCREngine: return [] def get_language_name(self, lang_code): - """获取语言代码对应的显示名称""" + """ + 获取语言代码对应的显示名称 + + 将Tesseract语言代码转换为用户友好的显示名称,同时显示中文和英文名称。 + + Args: + lang_code (str): 语言代码,如'eng'、'chi_sim'等 + + Returns: + str: 语言的显示名称,如'英语 (English)'、'简体中文 (Chinese Simplified)'等 + 如果没有对应的显示名称,则返回原始语言代码 + """ language_names = { 'eng': '英语 (English)', 'chi_sim': '简体中文 (Chinese Simplified)', @@ -91,17 +126,20 @@ class OCREngine: def process_file(self, input_file, output_file, options=None): """ - 使用OCRmyPDF处理单个文件 + 使用OCRmyPDF处理单个PDF文件 + + 处理前会检查输入文件是否存在、是否可读,以及输出目录是否可写。 + 会自动检测文件是否已经OCR过,并返回相应的状态码。 Args: input_file (str): 输入PDF文件路径 output_file (str): 输出PDF文件路径 - options (dict): OCR选项 + options (dict): OCR处理选项,包括language、deskew、rotate_pages、clean、optimize等 Returns: int: 处理结果状态码 - 0 - 失败 - 1 - 成功 + 0 - 处理失败 + 1 - 处理成功 2 - 文件已有文本层(已OCR过) """ if options is None: @@ -137,7 +175,14 @@ class OCREngine: return 1 if result else 0 def _last_error_is_existing_text(self): - """检查上次错误是否因为PDF已有文本层""" + """ + 检查上次错误是否因为PDF已有文本层 + + 通过分析最近一次OCRmyPDF命令的错误输出,判断错误是否是因为文件已经有文本层。 + + Returns: + bool: 如果错误是因为文件已有文本层,则返回True,否则返回False + """ if hasattr(self, 'last_error') and isinstance(self.last_error, str): return "page already has text" in self.last_error return False @@ -146,11 +191,13 @@ class OCREngine: """ 内部方法:使用OCRmyPDF处理单个文件 + 构建OCRmyPDF命令行参数,并执行命令进行OCR处理。 + Args: input_file (str): 输入PDF文件路径 output_file (str): 输出PDF文件路径 - options (dict): OCR选项 - force_ocr (bool): 是否强制OCR + options (dict): OCR选项,包括language、deskew、rotate_pages、clean、optimize等 + force_ocr (bool): 是否强制OCR处理,即使文件已有文本层 Returns: bool: 处理是否成功 @@ -225,16 +272,23 @@ class OCREngine: def process_batch(self, file_list, output_dir, options=None, progress_callback=None): """ - 批量处理文件 + 批量处理多个PDF文件 + + 对多个PDF文件进行OCR处理,并可通过回调函数报告处理进度。 + 支持自定义文件命名规则,包括添加前缀和后缀。 Args: - file_list (list): 输入文件列表 - output_dir (str): 输出目录 - options (dict): OCR选项 + file_list (list): 输入PDF文件路径列表 + output_dir (str): 输出目录路径 + options (dict): OCR选项,除了process_file支持的选项外,还支持file_prefix和file_suffix progress_callback (callable): 进度回调函数,接收参数(current, total, file, success) + current - 当前处理的文件索引(从1开始) + total - 总文件数 + file - 当前处理的文件路径 + success - 处理是否成功(包括已OCR过) Returns: - dict: 处理结果,键为输入文件路径,值为处理结果状态码(0-失败,1-成功,2-已OCR过) + dict: 处理结果字典,键为输入文件路径,值为处理结果状态码(0-失败,1-成功,2-已OCR过) """ results = {} total = len(file_list) @@ -243,9 +297,14 @@ class OCREngine: output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) + # 获取文件命名选项 + file_prefix = options.get("file_prefix", "") + file_suffix = options.get("file_suffix", "_ocr") + for i, input_file in enumerate(file_list): input_path = Path(input_file) - output_file = output_path / f"{input_path.stem}_ocr{input_path.suffix}" + # 使用前缀和后缀构建输出文件名 + output_file = output_path / f"{file_prefix}{input_path.stem}{file_suffix}{input_path.suffix}" self.logger.info(f"处理文件 {i+1}/{total}: {input_file}") result_code = self.process_file(input_file, output_file, options) diff --git a/src/gui/batch_dialog.py b/src/gui/batch_dialog.py index 17347f8..c13abb4 100644 --- a/src/gui/batch_dialog.py +++ b/src/gui/batch_dialog.py @@ -2,7 +2,7 @@ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QPushButton, QLabel, QFileDialog, QProgressBar, QComboBox, QCheckBox, QListWidget, QMessageBox, - QRadioButton, QInputDialog, QLineEdit + QRadioButton, QInputDialog, QLineEdit, QWidget ) from PySide6.QtCore import Qt, Signal, Slot, QThread from pathlib import Path @@ -13,12 +13,32 @@ from src.core.config import Config from src.utils.file_utils import FileUtils class BatchOCRWorker(QThread): - """批量OCR处理线程""" + """ + 批量OCR处理线程 + + 继承自QThread,用于在后台线程中执行批量OCR处理任务, + 避免在处理大量PDF文件时阻塞主UI线程。 + 可以报告总体进度、单个文件进度和处理结果。 + + 信号: + progress_updated: 发送处理进度信息 (当前索引, 总数, 文件路径, 结果码) + file_progress_updated: 发送单个文件的处理进度 (当前进度, 总进度) + finished: 处理完成后发送结果字典 + """ progress_updated = Signal(int, int, str, int) # 修改为发送状态码而不是布尔值 file_progress_updated = Signal(int, int) # 当前文件的进度 finished = Signal(dict) def __init__(self, engine, files, output_dir, options): + """ + 初始化批量OCR工作线程 + + Args: + engine (OCREngine): OCR引擎实例 + files (list): 要处理的文件路径列表 + output_dir (str): 输出目录路径 + options (dict): OCR处理选项 + """ super().__init__() self.engine = engine self.files = files @@ -26,6 +46,13 @@ class BatchOCRWorker(QThread): self.options = options def run(self): + """ + 线程执行方法 + + 遍历文件列表,对每个文件进行OCR处理, + 收集处理结果并通过信号报告进度。 + 完成后发送finished信号,包含所有文件的处理结果。 + """ results = {} total = len(self.files) @@ -48,9 +75,24 @@ class BatchOCRWorker(QThread): self.finished.emit(results) class BatchDialog(QDialog): - """批量处理对话框""" + """ + 批量处理对话框 + + 提供批量OCR处理的用户界面,包括文件选择、OCR选项设置、 + 配置管理和处理控制等功能。 + 相比主窗口,提供了更详细的批处理选项和进度显示。 + """ def __init__(self, parent=None): + """ + 初始化批量处理对话框 + + 设置窗口基本属性,创建配置和OCR引擎实例, + 初始化UI组件。 + + Args: + parent: 父窗口,默认为None + """ super().__init__(parent) self.setWindowTitle("批量OCR处理") self.resize(700, 500) @@ -62,7 +104,16 @@ class BatchDialog(QDialog): self.init_ui() def init_ui(self): - """初始化UI""" + """ + 初始化用户界面 + + 创建和布局所有UI组件,包括: + - 文件选择区域 + - 输出选项(目录和文件命名) + - OCR选项(语言、配置文件、处理选项) + - 进度显示(总进度和文件进度) + - 控制按钮 + """ # 主布局 main_layout = QVBoxLayout(self) @@ -112,10 +163,23 @@ class BatchDialog(QDialog): naming_layout.addWidget(QLabel("输出文件命名:")) self.naming_combo = QComboBox() self.naming_combo.addItems(["原文件名_ocr", "原文件名", "自定义前缀_原文件名"]) + self.naming_combo.currentIndexChanged.connect(self.on_naming_option_changed) naming_layout.addWidget(self.naming_combo, 1) + # 添加自定义前缀输入框 + self.prefix_layout = QHBoxLayout() + self.prefix_layout.addWidget(QLabel("自定义前缀:")) + self.prefix_edit = QLineEdit("OCR_") + self.prefix_layout.addWidget(self.prefix_edit, 1) + + # 初始时隐藏前缀输入框 + self.prefix_widget = QWidget() + self.prefix_widget.setLayout(self.prefix_layout) + self.prefix_widget.setVisible(False) + output_layout.addLayout(output_dir_layout) output_layout.addLayout(naming_layout) + output_layout.addWidget(self.prefix_widget) # OCR选项 ocr_group = QGroupBox("OCR选项") @@ -247,7 +311,12 @@ class BatchDialog(QDialog): main_layout.addLayout(buttons_layout) def add_files(self): - """添加文件""" + """ + 添加文件按钮点击处理 + + 打开文件选择对话框,允许用户选择一个或多个PDF文件。 + 选中的文件将添加到文件列表中并显示在界面上。 + """ files, _ = QFileDialog.getOpenFileNames( self, "选择PDF文件", @@ -259,7 +328,12 @@ class BatchDialog(QDialog): self.add_files_to_list(files) def add_folder(self): - """添加文件夹""" + """ + 添加文件夹按钮点击处理 + + 打开文件夹选择对话框,允许用户选择一个包含PDF文件的文件夹。 + 文件夹中的所有PDF文件(包括子文件夹中的PDF文件)将被添加到文件列表中。 + """ folder = QFileDialog.getExistingDirectory( self, "选择包含PDF文件的文件夹" @@ -273,7 +347,14 @@ class BatchDialog(QDialog): QMessageBox.information(self, "提示", "所选文件夹中未找到PDF文件") def add_files_to_list(self, files): - """添加文件到列表""" + """ + 将文件添加到文件列表 + + 过滤掉已经在列表中的文件,将新文件添加到列表并更新界面显示。 + + Args: + files (list): 要添加的文件路径列表 + """ # 过滤已存在的文件 new_files = [f for f in files if f not in self.selected_files] if not new_files: @@ -294,17 +375,30 @@ class BatchDialog(QDialog): self.config.add_recent_file(file) def clear_files(self): - """清除文件列表""" + """ + 清除文件列表 + + 清空选定的文件列表和界面上的文件列表显示。 + """ self.selected_files = [] self.file_list.clear() self.status_label.setText("文件列表已清空") def select_all_files(self): - """全选文件""" + """ + 全选文件 + + 选中文件列表中的所有文件。 + """ self.file_list.selectAll() def select_output_dir(self): - """选择输出目录""" + """ + 选择输出目录 + + 打开文件夹选择对话框,允许用户选择OCR处理结果的保存目录。 + 选中的目录将显示在输出目录编辑框中,并保存到最近使用的目录列表中。 + """ dir_path = QFileDialog.getExistingDirectory( self, "选择输出目录", @@ -316,7 +410,12 @@ class BatchDialog(QDialog): self.config.add_recent_output_dir(dir_path) def save_current_config(self): - """保存当前配置""" + """ + 保存当前配置 + + 将当前OCR选项保存为命名配置,以便将来重用。 + 弹出对话框让用户输入配置名称,然后保存到配置文件中。 + """ # 获取当前配置名称 config_name, ok = QInputDialog.getText( self, @@ -348,7 +447,13 @@ class BatchDialog(QDialog): QMessageBox.information(self, "成功", f"配置 \"{config_name}\" 已保存") def start_batch_ocr(self): - """开始批量OCR处理""" + """ + 开始批量OCR处理 + + 收集用户设置的OCR选项和文件命名选项,创建工作线程执行批量OCR处理。 + 处理前会进行必要的参数检查,如确保选择了文件和输出目录。 + 开始处理后会禁用UI元素,直到处理完成或取消。 + """ if not self.selected_files: QMessageBox.warning(self, "警告", "未选择文件") return @@ -390,6 +495,21 @@ class BatchDialog(QDialog): "optimize": self.optimize_cb.isChecked() }) + # 收集文件命名选项 + naming_option = self.naming_combo.currentIndex() + if naming_option == 0: # 原文件名_ocr + file_suffix = "_ocr" + file_prefix = "" + elif naming_option == 1: # 原文件名 + file_suffix = "" + file_prefix = "" + else: # 自定义前缀_原文件名 + file_suffix = "" + file_prefix = self.prefix_edit.text() + + options["file_prefix"] = file_prefix + options["file_suffix"] = file_suffix + # 禁用UI元素 self.start_btn.setEnabled(False) self.cancel_btn.setEnabled(True) @@ -418,7 +538,12 @@ class BatchDialog(QDialog): self.worker.start() def cancel_batch_ocr(self): - """取消批量OCR处理""" + """ + 取消批量OCR处理 + + 终止正在运行的OCR工作线程,更新状态显示, + 并重新启用被禁用的UI元素。 + """ if hasattr(self, 'worker') and self.worker.isRunning(): self.worker.terminate() self.worker.wait() @@ -428,7 +553,12 @@ class BatchDialog(QDialog): self.enable_ui() def enable_ui(self): - """启用UI元素""" + """ + 启用UI元素 + + 在OCR处理完成或取消后,重新启用之前被禁用的UI元素, + 使界面恢复到可交互状态。 + """ self.start_btn.setEnabled(True) self.cancel_btn.setEnabled(False) self.add_files_btn.setEnabled(True) @@ -440,7 +570,18 @@ class BatchDialog(QDialog): @Slot(int, int, str, int) def update_progress(self, current, total, file, result_code): - """更新总进度""" + """ + 更新总进度显示 + + 接收来自OCR工作线程的进度信号,更新总进度条和状态文本。 + 根据结果码显示不同颜色的状态文本:成功(绿色)、已OCR过(蓝色)、失败(红色)。 + + Args: + current (int): 当前处理的文件索引(从1开始) + total (int): 总文件数 + file (str): 当前处理的文件路径 + result_code (int): 处理结果状态码(0-失败,1-成功,2-已OCR过) + """ percent = int(current * 100 / total) self.total_progress_bar.setValue(percent) @@ -463,13 +604,29 @@ class BatchDialog(QDialog): @Slot(int, int) def update_file_progress(self, current, total): - """更新当前文件进度""" + """ + 更新当前文件进度 + + 接收来自OCR工作线程的文件进度信号,更新文件进度条。 + + Args: + current (int): 当前处理进度 + total (int): 总进度 + """ percent = int(current * 100 / total) if total > 0 else 0 self.file_progress_bar.setValue(percent) @Slot(dict) def ocr_finished(self, results): - """OCR处理完成""" + """ + OCR处理完成回调 + + 接收来自OCR工作线程的完成信号,统计处理结果, + 更新状态显示,重新启用UI元素,并显示处理结果对话框。 + + Args: + results (dict): 处理结果字典,键为文件路径,值为处理结果状态码 + """ success_count = 0 already_ocr_count = 0 failed_count = 0 @@ -510,13 +667,25 @@ class BatchDialog(QDialog): ) def load_saved_configs(self): - """加载已保存的配置""" + """ + 加载已保存的配置 + + 从配置文件中读取已保存的OCR配置,并添加到配置下拉列表中。 + """ saved_configs = self.config.get('saved_configs', {}) for config_name in saved_configs.keys(): self.config_combo.addItem(config_name) def on_config_changed(self, index): - """配置选择改变事件""" + """ + 配置选择变更事件处理 + + 当用户选择不同的配置文件时触发。 + 根据选择的配置更新UI中的OCR选项。 + + Args: + index (int): 当前选中配置的索引 + """ config_name = self.config_combo.currentText() if config_name == "默认配置": # 加载默认配置 @@ -546,4 +715,17 @@ class BatchDialog(QDialog): lang = config.get('language', 'eng') index = self.language_combo.findData(lang) if index >= 0: - self.language_combo.setCurrentIndex(index) \ No newline at end of file + self.language_combo.setCurrentIndex(index) + + def on_naming_option_changed(self, index): + """ + 命名选项变更事件处理 + + 当用户选择不同的文件命名选项时触发。 + 如果选择了"自定义前缀_原文件名"选项,则显示前缀输入框,否则隐藏。 + + Args: + index (int): 当前选中选项的索引 + """ + # 如果选择了"自定义前缀_原文件名",显示前缀输入框 + self.prefix_widget.setVisible(index == 2) # 第三个选项的索引是2 \ No newline at end of file diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 4011b4c..dd2d84a 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -6,7 +6,7 @@ from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QProgressBar, QComboBox, QCheckBox, QGroupBox, QListWidget, - QMessageBox, QStatusBar, QMenu, QMenuBar + QMessageBox, QStatusBar, QMenu, QMenuBar, QLineEdit ) from PySide6.QtCore import Qt, Signal, Slot, QThread from PySide6.QtGui import QIcon, QDragEnterEvent, QDropEvent, QAction @@ -18,11 +18,30 @@ from src.gui.settings import SettingsDialog from src.gui.batch_dialog import BatchDialog class OCRWorker(QThread): - """OCR处理线程""" + """ + OCR处理线程 + + 继承自QThread,用于在后台线程中执行OCR处理任务, + 避免在处理大型PDF文件时阻塞主UI线程。 + 使用信号机制向主线程报告处理进度和结果。 + + 信号: + progress_updated: 发送处理进度信息 (当前文件索引, 总文件数, 文件路径, 是否成功) + finished: 处理完成后发送结果字典 + """ progress_updated = Signal(int, int, str, bool) finished = Signal(dict) def __init__(self, engine, files, output_dir, options): + """ + 初始化OCR工作线程 + + Args: + engine (OCREngine): OCR引擎实例 + files (list): 要处理的文件路径列表 + output_dir (str): 输出目录路径 + options (dict): OCR处理选项 + """ super().__init__() self.engine = engine self.files = files @@ -30,6 +49,13 @@ class OCRWorker(QThread): self.options = options def run(self): + """ + 线程执行方法 + + 调用OCREngine的process_batch方法处理文件, + 并通过进度回调函数发送进度信号。 + 完成后发送finished信号,包含处理结果。 + """ results = self.engine.process_batch( self.files, self.output_dir, @@ -39,24 +65,47 @@ class OCRWorker(QThread): self.finished.emit(results) class MainWindow(QMainWindow): - """主窗口类""" + """ + 应用程序主窗口类 + + 提供主要的用户界面,包括文件选择、OCR选项设置和处理控制。 + 支持文件拖放、批处理和基本设置管理。 + 处理单个或多个PDF文件的OCR,并显示处理进度和结果。 + """ def __init__(self): + """ + 初始化主窗口 + + 设置窗口基本属性,创建配置和OCR引擎实例, + 初始化UI组件,并启用文件拖放功能。 + """ super().__init__() self.logger = logging.getLogger(__name__) self.setWindowTitle("OCRmyPDF GUI") self.resize(800, 600) - self.setAcceptDrops(True) # 启用拖放 + self.setAcceptDrops(True) # 启用拖放功能 + # 创建配置和OCR引擎实例 self.config = Config() self.ocr_engine = OCREngine() - self.selected_files = [] + self.selected_files = [] # 存储选中的文件路径 + # 初始化UI组件 self.init_ui() self.logger.info("主窗口初始化完成") def init_ui(self): - """初始化UI""" + """ + 初始化用户界面 + + 创建和布局所有UI组件,包括: + - 菜单栏和状态栏 + - 文件选择区域 + - 输出目录和文件命名选项 + - OCR语言和处理选项 + - 进度显示和控制按钮 + """ # 创建菜单栏 self.create_menu_bar() @@ -76,6 +125,7 @@ class MainWindow(QMainWindow): file_group = QGroupBox("文件选择") file_layout = QVBoxLayout(file_group) + # 添加文件按钮 file_buttons_layout = QHBoxLayout() self.add_files_btn = QPushButton("添加文件") self.add_files_btn.clicked.connect(self.add_files) @@ -88,6 +138,7 @@ class MainWindow(QMainWindow): file_buttons_layout.addWidget(self.clear_files_btn) file_buttons_layout.addStretch() + # 文件列表 self.file_list = QListWidget() self.file_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection) @@ -99,13 +150,32 @@ class MainWindow(QMainWindow): output_layout.addWidget(QLabel("输出目录:")) self.output_dir_edit = QComboBox() self.output_dir_edit.setEditable(True) - self.output_dir_edit.addItems(self.config.get('recent_output_dirs', [])) + self.output_dir_edit.addItems(self.config.get('recent_output_dirs', [])) # 加载最近使用的目录 self.output_dir_btn = QPushButton("浏览...") self.output_dir_btn.clicked.connect(self.select_output_dir) output_layout.addWidget(self.output_dir_edit, 1) output_layout.addWidget(self.output_dir_btn) - # OCR选项 + # 输出文件命名选项 + naming_layout = QHBoxLayout() + naming_layout.addWidget(QLabel("输出文件命名:")) + self.naming_combo = QComboBox() + self.naming_combo.addItems(["原文件名_ocr", "原文件名", "自定义前缀_原文件名"]) + self.naming_combo.currentIndexChanged.connect(self.on_naming_option_changed) + naming_layout.addWidget(self.naming_combo, 1) + + # 自定义前缀输入框 + self.prefix_layout = QHBoxLayout() + self.prefix_layout.addWidget(QLabel("自定义前缀:")) + self.prefix_edit = QLineEdit("OCR_") + self.prefix_layout.addWidget(self.prefix_edit, 1) + + # 初始时隐藏前缀输入框 + self.prefix_widget = QWidget() + self.prefix_widget.setLayout(self.prefix_layout) + self.prefix_widget.setVisible(False) # 默认隐藏,仅当选择"自定义前缀"选项时显示 + + # OCR选项组 options_group = QGroupBox("OCR选项") options_layout = QVBoxLayout(options_group) @@ -115,15 +185,14 @@ class MainWindow(QMainWindow): self.language_combo = QComboBox() self.language_combo.setToolTip("选择OCR识别使用的语言") - # 添加可用的语言 - # 常用语言列表 - common_langs = ['eng', 'chi_sim', 'chi_tra', 'jpn', 'kor'] + # 添加可用的语言,分为常用语言和其他语言两组 + common_langs = ['eng', 'chi_sim', 'chi_tra', 'jpn', 'kor'] # 常用语言列表 - # 首先添加常用语言 if self.ocr_engine.available_languages: # 添加常用语言组 common_available = [lang for lang in common_langs if lang in self.ocr_engine.available_languages] if common_available: + # 添加组标题项(不可选) self.language_combo.addItem("--- 常用语言 ---", None) for lang_code in common_available: lang_name = self.ocr_engine.get_language_name(lang_code) @@ -133,6 +202,7 @@ class MainWindow(QMainWindow): other_available = [lang for lang in self.ocr_engine.available_languages if lang not in common_langs] if other_available: + # 添加组标题项(不可选) self.language_combo.addItem("--- 其他语言 ---", None) # 按名称排序 other_langs_sorted = sorted( @@ -157,7 +227,7 @@ class MainWindow(QMainWindow): language_layout.addWidget(self.language_combo) options_layout.addLayout(language_layout) - # 处理选项 + # OCR处理选项复选框 self.deskew_cb = QCheckBox("自动校正倾斜页面") self.deskew_cb.setChecked(self.config.get('default_options.deskew', True)) self.rotate_cb = QCheckBox("自动旋转页面") @@ -172,7 +242,7 @@ class MainWindow(QMainWindow): options_layout.addWidget(self.clean_cb) options_layout.addWidget(self.optimize_cb) - # 进度条 + # 进度显示区域 progress_layout = QVBoxLayout() self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) @@ -187,7 +257,7 @@ class MainWindow(QMainWindow): self.start_btn.clicked.connect(self.start_ocr) self.cancel_btn = QPushButton("取消") self.cancel_btn.clicked.connect(self.cancel_ocr) - self.cancel_btn.setEnabled(False) + self.cancel_btn.setEnabled(False) # 初始状态下禁用取消按钮 buttons_layout.addStretch() buttons_layout.addWidget(self.start_btn) buttons_layout.addWidget(self.cancel_btn) @@ -195,12 +265,21 @@ class MainWindow(QMainWindow): # 添加所有元素到主布局 main_layout.addWidget(file_group) main_layout.addLayout(output_layout) + main_layout.addLayout(naming_layout) + main_layout.addWidget(self.prefix_widget) main_layout.addWidget(options_group) main_layout.addLayout(progress_layout) main_layout.addLayout(buttons_layout) def create_menu_bar(self): - """创建菜单栏""" + """ + 创建应用程序菜单栏 + + 包含文件、编辑和帮助三个主菜单,提供常用功能的快捷访问。 + 文件菜单:添加文件、添加文件夹、批量处理、退出 + 编辑菜单:清除文件列表、设置 + 帮助菜单:关于 + """ menu_bar = QMenuBar() self.setMenuBar(menu_bar) @@ -249,7 +328,12 @@ class MainWindow(QMainWindow): help_menu.addAction(about_action) def add_files(self): - """添加文件""" + """ + 添加文件按钮点击处理 + + 打开文件选择对话框,允许用户选择一个或多个PDF文件。 + 选中的文件将添加到文件列表中并显示在界面上。 + """ files, _ = QFileDialog.getOpenFileNames( self, "选择PDF文件", @@ -261,7 +345,12 @@ class MainWindow(QMainWindow): self.add_files_to_list(files) def add_folder(self): - """添加文件夹""" + """ + 添加文件夹按钮点击处理 + + 打开文件夹选择对话框,允许用户选择一个包含PDF文件的文件夹。 + 文件夹中的所有PDF文件(包括子文件夹中的PDF文件)将被添加到文件列表中。 + """ folder = QFileDialog.getExistingDirectory( self, "选择包含PDF文件的文件夹" @@ -275,7 +364,15 @@ class MainWindow(QMainWindow): QMessageBox.information(self, "提示", "所选文件夹中未找到PDF文件") def add_files_to_list(self, files): - """添加文件到列表""" + """ + 将文件添加到文件列表 + + 过滤掉已经在列表中的文件,将新文件添加到列表并更新界面显示。 + 同时更新状态栏显示添加的文件数量,并将文件路径保存到最近使用文件列表中。 + + Args: + files (list): 要添加的文件路径列表 + """ # 过滤已存在的文件 new_files = [f for f in files if f not in self.selected_files] if not new_files: @@ -297,14 +394,24 @@ class MainWindow(QMainWindow): self.config.add_recent_file(file) def clear_files(self): - """清除文件列表""" + """ + 清除文件列表 + + 清空选定的文件列表和界面上的文件列表显示, + 同时更新状态栏显示。 + """ self.selected_files = [] self.file_list.clear() self.status_label.setText("文件列表已清空") self.statusBar.showMessage("文件列表已清空") def select_output_dir(self): - """选择输出目录""" + """ + 选择输出目录 + + 打开文件夹选择对话框,允许用户选择OCR处理结果的保存目录。 + 选中的目录将显示在输出目录编辑框中,并保存到最近使用的目录列表中。 + """ dir_path = QFileDialog.getExistingDirectory( self, "选择输出目录", @@ -316,7 +423,13 @@ class MainWindow(QMainWindow): self.config.add_recent_output_dir(dir_path) def start_ocr(self): - """开始OCR处理""" + """ + 开始OCR处理 + + 收集用户设置的OCR选项,创建工作线程执行OCR处理。 + 处理前会进行必要的参数检查,如确保选择了文件和输出目录。 + 对于单个文件,直接处理并显示结果;对于多个文件,启动工作线程并显示进度。 + """ if not self.selected_files: QMessageBox.warning(self, "警告", "未选择文件") return @@ -351,6 +464,7 @@ class MainWindow(QMainWindow): if "language" not in options: options["language"] = "eng" + # 添加处理选项 options.update({ "deskew": self.deskew_cb.isChecked(), "rotate_pages": self.rotate_cb.isChecked(), @@ -358,11 +472,27 @@ class MainWindow(QMainWindow): "optimize": self.optimize_cb.isChecked() }) + # 收集文件命名选项 + naming_option = self.naming_combo.currentIndex() + if naming_option == 0: # 原文件名_ocr + file_suffix = "_ocr" + file_prefix = "" + elif naming_option == 1: # 原文件名 + file_suffix = "" + file_prefix = "" + else: # 自定义前缀_原文件名 + file_suffix = "" + file_prefix = self.prefix_edit.text() + + options["file_prefix"] = file_prefix + options["file_suffix"] = file_suffix + # 如果只有一个文件,先检查是否已OCR过 if len(self.selected_files) == 1: input_file = self.selected_files[0] input_path = Path(input_file) - output_file = Path(output_dir) / f"{input_path.stem}_ocr{input_path.suffix}" + # 使用前缀和后缀构建输出文件名 + output_file = Path(output_dir) / f"{file_prefix}{input_path.stem}{file_suffix}{input_path.suffix}" # 检查是否已OCR过 result_code = self.ocr_engine.process_file(input_file, output_file, options) @@ -417,7 +547,12 @@ class MainWindow(QMainWindow): self.worker.start() def cancel_ocr(self): - """取消OCR处理""" + """ + 取消OCR处理 + + 终止正在运行的OCR工作线程,更新状态显示, + 并重新启用被禁用的UI元素。 + """ if hasattr(self, 'worker') and self.worker.isRunning(): self.worker.terminate() self.worker.wait() @@ -428,7 +563,12 @@ class MainWindow(QMainWindow): self.enable_ui() def enable_ui(self): - """启用UI元素""" + """ + 启用UI元素 + + 在OCR处理完成或取消后,重新启用之前被禁用的UI元素, + 使界面恢复到可交互状态。 + """ self.start_btn.setEnabled(True) self.cancel_btn.setEnabled(False) self.add_files_btn.setEnabled(True) @@ -439,7 +579,18 @@ class MainWindow(QMainWindow): @Slot(int, int, str, bool) def update_progress(self, current, total, file, success): - """更新进度""" + """ + 更新进度显示 + + 接收来自OCR工作线程的进度信号,更新进度条和状态文本。 + 使用HTML格式化状态文本,成功显示为绿色,失败显示为红色。 + + Args: + current (int): 当前处理的文件索引(从1开始) + total (int): 总文件数 + file (str): 当前处理的文件路径 + success (bool): 处理是否成功 + """ percent = int(current * 100 / total) self.progress_bar.setValue(percent) @@ -455,11 +606,20 @@ class MainWindow(QMainWindow): @Slot(dict) def ocr_finished(self, results): - """OCR处理完成""" + """ + OCR处理完成回调 + + 接收来自OCR工作线程的完成信号,统计处理结果, + 更新状态显示,重新启用UI元素,并显示处理结果对话框。 + + Args: + results (dict): 处理结果字典,键为文件路径,值为处理结果状态码 + """ success_count = 0 already_ocr_count = 0 failed_count = 0 + # 统计处理结果 for result_code in results.values(): if result_code == 1: # 成功 success_count += 1 @@ -495,7 +655,12 @@ class MainWindow(QMainWindow): ) def show_settings(self): - """显示设置对话框""" + """ + 显示设置对话框 + + 创建并显示设置对话框,如果用户确认设置更改, + 则更新UI以反映新的默认设置。 + """ dialog = SettingsDialog(self) if dialog.exec(): # 更新UI以反映新设置 @@ -505,12 +670,21 @@ class MainWindow(QMainWindow): self.optimize_cb.setChecked(self.config.get('default_options.optimize', True)) def show_batch_dialog(self): - """显示批量处理对话框""" + """ + 显示批量处理对话框 + + 创建并显示批量处理对话框,允许用户一次处理多个文件, + 并提供更详细的批处理选项。 + """ dialog = BatchDialog(self) dialog.exec() def show_about(self): - """显示关于对话框""" + """ + 显示关于对话框 + + 显示应用程序的版本信息和基本说明。 + """ QMessageBox.about( self, "关于 OCRmyPDF GUI", @@ -521,12 +695,28 @@ class MainWindow(QMainWindow): ) def dragEnterEvent(self, event: QDragEnterEvent): - """拖拽进入事件""" + """ + 拖拽进入事件处理 + + 当用户拖拽文件到窗口上方时触发。 + 如果拖拽内容包含URL(文件路径),则接受拖拽动作。 + + Args: + event (QDragEnterEvent): 拖拽进入事件对象 + """ if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event: QDropEvent): - """拖拽放下事件""" + """ + 拖拽放下事件处理 + + 当用户在窗口中放下拖拽的文件时触发。 + 处理拖拽的文件或文件夹,将PDF文件添加到文件列表中。 + + Args: + event (QDropEvent): 拖拽放下事件对象 + """ urls = event.mimeData().urls() files = [] @@ -543,4 +733,17 @@ class MainWindow(QMainWindow): if files: self.add_files_to_list(files) - event.acceptProposedAction() \ No newline at end of file + event.acceptProposedAction() + + def on_naming_option_changed(self, index): + """ + 命名选项变更事件处理 + + 当用户选择不同的文件命名选项时触发。 + 如果选择了"自定义前缀_原文件名"选项,则显示前缀输入框,否则隐藏。 + + Args: + index (int): 当前选中选项的索引 + """ + # 如果选择了"自定义前缀_原文件名",显示前缀输入框 + self.prefix_widget.setVisible(index == 2) # 第三个选项的索引是2 \ No newline at end of file diff --git a/src/gui/settings.py b/src/gui/settings.py index 373c3c7..df17b59 100644 --- a/src/gui/settings.py +++ b/src/gui/settings.py @@ -10,9 +10,23 @@ from src.core.config import Config from src.core.ocr_engine import OCREngine class SettingsDialog(QDialog): - """设置对话框""" + """ + 设置对话框 + + 提供应用程序各项设置的配置界面,包括常规设置、OCR设置和界面设置。 + 使用选项卡组织不同类别的设置,提供直观的设置界面。 + 设置更改后保存到配置文件中,供应用程序其他部分使用。 + """ def __init__(self, parent=None): + """ + 初始化设置对话框 + + 创建配置实例,设置窗口基本属性,初始化UI组件。 + + Args: + parent: 父窗口,默认为None + """ super().__init__(parent) self.setWindowTitle("设置") self.resize(500, 400) @@ -21,7 +35,12 @@ class SettingsDialog(QDialog): self.init_ui() def init_ui(self): - """初始化UI""" + """ + 初始化用户界面 + + 创建选项卡式布局,包含常规、OCR和界面三个选项卡, + 以及确定和取消按钮。 + """ # 主布局 main_layout = QVBoxLayout(self) @@ -59,7 +78,14 @@ class SettingsDialog(QDialog): main_layout.addLayout(button_layout) def setup_general_tab(self, tab): - """设置常规选项卡""" + """ + 设置常规选项卡 + + 创建并布局常规设置选项,包括启动选项和文件历史设置。 + + Args: + tab: 要设置的选项卡控件 + """ layout = QVBoxLayout(tab) # 启动选项 @@ -102,7 +128,14 @@ class SettingsDialog(QDialog): layout.addStretch() def setup_ocr_tab(self, tab): - """设置OCR选项卡""" + """ + 设置OCR选项卡 + + 创建并布局OCR设置选项,包括默认语言设置、处理选项和输出类型设置。 + + Args: + tab: 要设置的选项卡控件 + """ layout = QVBoxLayout(tab) # 默认语言 @@ -192,56 +225,81 @@ class SettingsDialog(QDialog): output_layout.addWidget(self.output_type_combo) + layout.addWidget(language_group) layout.addWidget(options_group) layout.addWidget(output_group) layout.addStretch() def setup_ui_tab(self, tab): - """设置界面选项卡""" - layout = QVBoxLayout(tab) - - # 语言 - language_group = QGroupBox("界面语言") - language_layout = QVBoxLayout(language_group) + """ + 设置界面选项卡 - self.ui_language_combo = QComboBox() - self.ui_language_combo.addItems(["简体中文", "English"]) - current_lang = "简体中文" if self.config.get('ui.language') == 'zh_CN' else "English" - self.ui_language_combo.setCurrentText(current_lang) + 创建并布局界面设置选项,包括主题和语言设置。 - language_layout.addWidget(self.ui_language_combo) + Args: + tab: 要设置的选项卡控件 + """ + layout = QVBoxLayout(tab) - # 主题 + # 主题设置 theme_group = QGroupBox("主题") theme_layout = QVBoxLayout(theme_group) - self.light_theme_rb = QRadioButton("浅色") - self.dark_theme_rb = QRadioButton("深色") - self.system_theme_rb = QRadioButton("跟随系统") + self.theme_system_rb = QRadioButton("跟随系统") + self.theme_light_rb = QRadioButton("浅色主题") + self.theme_dark_rb = QRadioButton("深色主题") current_theme = self.config.get('ui.theme', 'system') if current_theme == 'light': - self.light_theme_rb.setChecked(True) + self.theme_light_rb.setChecked(True) elif current_theme == 'dark': - self.dark_theme_rb.setChecked(True) + self.theme_dark_rb.setChecked(True) else: - self.system_theme_rb.setChecked(True) + self.theme_system_rb.setChecked(True) - theme_layout.addWidget(self.light_theme_rb) - theme_layout.addWidget(self.dark_theme_rb) - theme_layout.addWidget(self.system_theme_rb) + theme_layout.addWidget(self.theme_system_rb) + theme_layout.addWidget(self.theme_light_rb) + theme_layout.addWidget(self.theme_dark_rb) + + # 语言设置 + language_group = QGroupBox("界面语言") + language_layout = QVBoxLayout(language_group) + + self.ui_language_combo = QComboBox() + self.ui_language_combo.addItem("简体中文", "zh_CN") + self.ui_language_combo.addItem("English", "en_US") + + current_lang = self.config.get('ui.language', 'zh_CN') + index = self.ui_language_combo.findData(current_lang) + if index >= 0: + self.ui_language_combo.setCurrentIndex(index) + + language_note = QLabel("注:更改语言设置需要重启应用程序才能生效") + language_note.setStyleSheet("color: gray;") + + language_layout.addWidget(self.ui_language_combo) + language_layout.addWidget(language_note) - layout.addWidget(language_group) layout.addWidget(theme_group) + layout.addWidget(language_group) layout.addStretch() def clear_history(self): - """清除历史记录""" + """ + 清除历史记录 + + 清空最近使用的文件和输出目录列表,并弹出确认提示。 + """ self.config.set('recent_files', []) self.config.set('recent_output_dirs', []) + QMessageBox.information(self, "已清除", "已清除所有历史记录") def accept(self): - """确定按钮点击事件""" + """ + 确定按钮点击处理 + + 保存所有设置到配置文件中,并关闭对话框。 + """ # 保存常规设置 self.config.set('general.check_update_on_startup', self.check_update_cb.isChecked()) self.config.set('general.show_welcome', self.show_welcome_cb.isChecked()) @@ -249,23 +307,21 @@ class SettingsDialog(QDialog): self.config.set('general.max_recent_files', self.recent_files_spin.value()) # 保存OCR设置 - # 获取选中的语言代码 lang_index = self.language_combo.currentIndex() lang_data = self.language_combo.itemData(lang_index) - if lang_data: # 确保不是分隔符 - self.config.set('default_options.language', lang_data) - else: - # 如果选中了分隔符,尝试找到下一个有效选项 + + # 如果选择了分隔符,尝试找到下一个有效选项 + if lang_data is None: for i in range(lang_index + 1, self.language_combo.count()): next_data = self.language_combo.itemData(i) if next_data: - self.language_combo.setCurrentIndex(i) - self.config.set('default_options.language', next_data) + lang_data = next_data break # 如果没有找到,使用默认语言 - if not self.language_combo.currentData(): - self.config.set('default_options.language', 'eng') + if lang_data is None: + lang_data = 'eng' + self.config.set('default_options.language', lang_data) self.config.set('default_options.deskew', self.deskew_cb.isChecked()) self.config.set('default_options.rotate_pages', self.rotate_cb.isChecked()) self.config.set('default_options.clean', self.clean_cb.isChecked()) @@ -273,34 +329,39 @@ class SettingsDialog(QDialog): self.config.set('default_options.output_type', self.output_type_combo.currentText()) # 保存界面设置 - ui_lang = 'zh_CN' if self.ui_language_combo.currentText() == '简体中文' else 'en_US' - self.config.set('ui.language', ui_lang) - - if self.light_theme_rb.isChecked(): - self.config.set('ui.theme', 'light') - elif self.dark_theme_rb.isChecked(): - self.config.set('ui.theme', 'dark') + if self.theme_light_rb.isChecked(): + theme = 'light' + elif self.theme_dark_rb.isChecked(): + theme = 'dark' else: - self.config.set('ui.theme', 'system') + theme = 'system' + + self.config.set('ui.theme', theme) + self.config.set('ui.language', self.ui_language_combo.currentData()) super().accept() - + def refresh_languages(self): - """刷新可用语言列表""" - ocr_engine = OCREngine() - # 重新获取可用语言 - ocr_engine.available_languages = ocr_engine.get_available_languages() + """ + 刷新语言列表 + 重新获取系统中已安装的Tesseract语言包,并更新语言下拉列表。 + 保存当前选择的语言,并在刷新后尝试恢复选择。 + """ # 保存当前选择的语言 current_lang = self.language_combo.currentData() - # 清空并重新填充语言列表 + # 清空语言下拉列表 self.language_combo.clear() - # 常用语言列表 + # 重新获取语言列表 + ocr_engine = OCREngine() + # 这会重新检测可用的语言 + ocr_engine = OCREngine() + + # 重新填充语言下拉列表 common_langs = ['eng', 'chi_sim', 'chi_tra', 'jpn', 'kor'] - # 首先添加常用语言 if ocr_engine.available_languages: # 添加常用语言组 common_available = [lang for lang in common_langs if lang in ocr_engine.available_languages] @@ -329,13 +390,19 @@ class SettingsDialog(QDialog): lang_name = ocr_engine.get_language_name(lang_code) self.language_combo.addItem(lang_name, lang_code) - # 尝试恢复之前选择的语言 - index = self.language_combo.findData(current_lang) - if index >= 0: - self.language_combo.setCurrentIndex(index) - - QMessageBox.information(self, "刷新完成", f"已刷新语言列表,共找到 {len(ocr_engine.available_languages)} 种语言。") - + # 恢复之前选择的语言 + if current_lang: + index = self.language_combo.findData(current_lang) + if index >= 0: + self.language_combo.setCurrentIndex(index) + + # 显示刷新结果 + QMessageBox.information( + self, + "刷新完成", + f"已刷新语言列表,找到 {len(ocr_engine.available_languages)} 种语言" + ) + def download_language_pack(self): """下载Tesseract语言包 - 已移除""" pass diff --git a/src/utils/file_utils.py b/src/utils/file_utils.py index ffd353c..2f74eb0 100644 --- a/src/utils/file_utils.py +++ b/src/utils/file_utils.py @@ -4,18 +4,34 @@ from pathlib import Path import logging class FileUtils: - """文件工具类,提供文件操作相关的功能""" + """ + 文件工具类 + + 提供文件和目录操作的通用工具方法,包括目录创建、文件验证、 + 文件搜索、文件大小计算和文件复制等功能。 + 所有方法均为静态方法,可直接通过类名调用,无需实例化。 + + 主要功能: + - 目录创建和验证 + - PDF文件验证 + - 目录内PDF文件搜索 + - 文件大小格式化 + - 文件复制 + """ @staticmethod def ensure_dir(dir_path): """ 确保目录存在,如果不存在则创建 + 使用pathlib创建目录,支持创建多级目录结构。 + 如果目录已存在,则不会引发错误。 + Args: - dir_path: 目录路径 + dir_path (str or Path): 要创建的目录路径 Returns: - bool: 操作是否成功 + bool: 如果目录创建成功或已存在则返回True,创建失败则返回False """ try: Path(dir_path).mkdir(parents=True, exist_ok=True) @@ -29,11 +45,16 @@ class FileUtils: """ 检查文件是否是有效的PDF文件 + 检查包含三个步骤: + 1. 检查文件是否存在 + 2. 检查文件扩展名是否为.pdf (不区分大小写) + 3. 检查文件头部是否包含PDF标识 (%PDF-) + Args: - file_path: 文件路径 + file_path (str or Path): 要检查的文件路径 Returns: - bool: 是否是有效的PDF文件 + bool: 如果文件是有效的PDF文件则返回True,否则返回False """ if not Path(file_path).exists(): return False @@ -55,12 +76,15 @@ class FileUtils: """ 获取目录中的所有PDF文件 + 搜索指定目录中的所有PDF文件,可选择是否递归搜索子目录。 + 使用is_valid_pdf方法验证每个找到的PDF文件。 + Args: - dir_path: 目录路径 - recursive: 是否递归搜索子目录 + dir_path (str or Path): 要搜索的目录路径 + recursive (bool): 是否递归搜索子目录,默认为False Returns: - list: PDF文件路径列表 + list: 包含所有找到的PDF文件绝对路径的列表,如果目录不存在或不是目录则返回空列表 """ pdf_files = [] dir_path = Path(dir_path) @@ -69,12 +93,14 @@ class FileUtils: return pdf_files if recursive: + # 递归搜索目录及其所有子目录 for root, _, files in os.walk(dir_path): for file in files: file_path = Path(root) / file if FileUtils.is_valid_pdf(file_path): pdf_files.append(str(file_path)) else: + # 只搜索当前目录,不包括子目录 for file in dir_path.iterdir(): if file.is_file() and FileUtils.is_valid_pdf(file): pdf_files.append(str(file)) @@ -84,13 +110,16 @@ class FileUtils: @staticmethod def get_file_size_str(file_path): """ - 获取文件大小的字符串表示 + 获取文件大小的人类可读字符串表示 + + 将文件大小从字节转换为更易读的单位(B, KB, MB, GB, TB, PB)。 + 使用1024作为转换基数,保留一位小数。 Args: - file_path: 文件路径 + file_path (str or Path): 文件路径 Returns: - str: 文件大小字符串,如 "1.2 MB" + str: 格式化的文件大小字符串,如 "1.2 MB",如果文件不存在或发生错误则返回"未知大小" """ try: size = Path(file_path).stat().st_size @@ -107,14 +136,17 @@ class FileUtils: @staticmethod def copy_file(src, dst): """ - 复制文件 + 复制文件,保留元数据 + + 使用shutil.copy2复制文件,该方法会尝试保留文件的元数据 + (如创建时间、修改时间、访问时间、权限等)。 Args: - src: 源文件路径 - dst: 目标文件路径 + src (str or Path): 源文件路径 + dst (str or Path): 目标文件路径 Returns: - bool: 操作是否成功 + bool: 复制成功则返回True,失败则返回False """ try: shutil.copy2(src, dst)