import
os
os.environ[
'KMP_DUPLICATE_LIB_OK'
]
=
'TRUE'
import
tkinter as tk
from
tkinter
import
filedialog, messagebox, ttk
from
PIL
import
Image, ImageTk
import
requests
import
base64
import
hashlib
import
hmac
import
time
import
json
from
threading
import
Thread
import
logging
import
configparser
from
pathlib
import
Path
import
subprocess
from
enum
import
Enum, auto
try
:
from
paddleocr
import
PaddleOCR
PADDLEOCR_AVAILABLE
=
True
except
ImportError:
PADDLEOCR_AVAILABLE
=
False
try
:
from
tkinterdnd2
import
DND_FILES, TkinterDnD
TKDND_AVAILABLE
=
True
except
ImportError:
TKDND_AVAILABLE
=
False
class
OcrMode(Enum):
TENCENT
=
auto()
PADDLE
=
auto()
class
ImageTextRenamerApp:
def
__init__(
self
, root):
self
.root
=
root
self
.root.title(
"图片文字识别重命名工具"
)
self
.root.geometry(
"1000x850"
)
self
.version
=
"1.3.0"
self
.release_date
=
"2025-4-05"
self
.image_files
=
[]
self
.current_index
=
0
self
.secret_id
=
""
self
.secret_key
=
""
self
.keywords
=
"编号,名称,日期"
self
.processing
=
False
self
.config_file
=
Path.home()
/
".ocr_renamer.ini"
self
.ocr_mode
=
OcrMode.TENCENT
self
.paddle_ocr
=
None
self
.replace_files_list
=
[]
self
.replace_current_index
=
0
logging.basicConfig(level
=
logging.INFO)
self
.logger
=
logging.getLogger(__name__)
self
.load_config()
if
self
.ocr_mode
=
=
OcrMode.PADDLE
and
PADDLEOCR_AVAILABLE:
self
.initialize_paddle_ocr()
if
TKDND_AVAILABLE:
self
.root.drop_target_register(DND_FILES)
self
.root.dnd_bind(
'<<Drop>>'
,
self
.handle_drop)
self
.drop_label
=
ttk.Label(
self
.root, text
=
"拖放图片到此处"
)
self
.drop_label.place(relx
=
0.5
, rely
=
0.5
, anchor
=
'center'
)
else
:
self
.logger.warning(
"tkinterdnd2未安装,拖放功能不可用"
)
self
.create_widgets()
self
.root.protocol(
"WM_DELETE_WINDOW"
,
self
.on_closing)
def
create_menu(
self
):
menubar
=
tk.Menu(
self
.root)
self
.root.config(menu
=
menubar)
file_menu
=
tk.Menu(menubar, tearoff
=
0
)
file_menu.add_command(label
=
"选择图片"
, command
=
self
.select_images)
file_menu.add_separator()
file_menu.add_command(label
=
"退出"
, command
=
self
.on_closing)
menubar.add_cascade(label
=
"文件"
, menu
=
file_menu)
settings_menu
=
tk.Menu(menubar, tearoff
=
0
)
settings_menu.add_command(label
=
"OCR配置"
, command
=
self
.show_ocr_config)
settings_menu.add_command(label
=
"关键词设置"
, command
=
self
.show_keywords_config)
menubar.add_cascade(label
=
"设置"
, menu
=
settings_menu)
help_menu
=
tk.Menu(menubar, tearoff
=
0
)
help_menu.add_command(label
=
"使用帮助"
, command
=
self
.show_help)
help_menu.add_command(label
=
"检查更新"
, command
=
self
.check_update)
help_menu.add_separator()
help_menu.add_command(label
=
"关于"
, command
=
self
.show_about)
menubar.add_cascade(label
=
"帮助"
, menu
=
help_menu)
def
show_ocr_config(
self
):
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"OCR配置"
)
dialog.resizable(
False
,
False
)
dialog.geometry(f
"500x400+{self.root.winfo_x()+100}+{self.root.winfo_y()+100}"
)
main_frame
=
tk.Frame(dialog, padx
=
20
, pady
=
20
)
main_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
mode_frame
=
tk.LabelFrame(main_frame, text
=
"OCR模式选择"
, padx
=
10
, pady
=
10
)
mode_frame.pack(fill
=
tk.X, pady
=
(
0
,
15
))
ocr_mode_var
=
tk.IntVar(value
=
self
.ocr_mode.value)
tk.Radiobutton(
mode_frame,
text
=
"腾讯云OCR (在线)"
,
variable
=
ocr_mode_var,
value
=
OcrMode.TENCENT.value
).pack(anchor
=
tk.W, pady
=
2
)
paddle_state
=
tk.NORMAL
if
PADDLEOCR_AVAILABLE
else
tk.DISABLED
paddle_text
=
"PaddleOCR (本地)"
if
PADDLEOCR_AVAILABLE
else
"PaddleOCR (本地, 不可用)"
paddle_radio
=
tk.Radiobutton(
mode_frame,
text
=
paddle_text,
variable
=
ocr_mode_var,
value
=
OcrMode.PADDLE.value,
state
=
paddle_state
)
paddle_radio.pack(anchor
=
tk.W, pady
=
2
)
api_frame
=
tk.LabelFrame(main_frame, text
=
"腾讯云OCR配置"
, padx
=
10
, pady
=
10
)
api_frame.pack(fill
=
tk.X, pady
=
(
0
,
15
))
tk.Label(api_frame, text
=
"SecretId:"
).pack(anchor
=
tk.W, pady
=
(
0
,
2
))
secret_id_entry
=
tk.Entry(api_frame)
secret_id_entry.insert(
0
,
self
.secret_id)
secret_id_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
tk.Label(api_frame, text
=
"SecretKey:"
).pack(anchor
=
tk.W, pady
=
(
0
,
2
))
secret_key_entry
=
tk.Entry(api_frame, show
=
"*"
)
secret_key_entry.insert(
0
,
self
.secret_key)
secret_key_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
btn_frame
=
tk.Frame(main_frame, pady
=
15
)
btn_frame.pack(fill
=
tk.X)
save_btn
=
tk.Button(btn_frame, text
=
"保存"
,
command
=
lambda
:
self
.save_ocr_config(
ocr_mode_var.get(),
secret_id_entry.get(),
secret_key_entry.get(),
dialog
))
save_btn.pack(side
=
tk.RIGHT, padx
=
5
)
cancel_btn
=
tk.Button(btn_frame, text
=
"取消"
, command
=
dialog.destroy)
cancel_btn.pack(side
=
tk.RIGHT, padx
=
5
)
dialog.transient(
self
.root)
dialog.grab_set()
def
show_keywords_config(
self
):
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"关键词设置"
)
dialog.resizable(
False
,
False
)
dialog.geometry(f
"500x300+{self.root.winfo_x()+100}+{self.root.winfo_y()+100}"
)
main_frame
=
tk.Frame(dialog, padx
=
20
, pady
=
20
)
main_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
keywords_frame
=
tk.LabelFrame(main_frame, text
=
"关键词设置(按顺序组合)"
, padx
=
10
, pady
=
10
)
keywords_frame.pack(fill
=
tk.X, pady
=
(
0
,
15
))
tk.Label(keywords_frame, text
=
"用逗号分隔,如: 编号,名称,日期"
).pack(anchor
=
tk.W, pady
=
(
0
,
5
))
keywords_entry
=
tk.Entry(keywords_frame)
keywords_entry.insert(
0
,
self
.keywords)
keywords_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
btn_frame
=
tk.Frame(main_frame, pady
=
15
)
btn_frame.pack(fill
=
tk.X)
save_btn
=
tk.Button(btn_frame, text
=
"保存"
,
command
=
lambda
:
self
.save_keywords_config(
keywords_entry.get(),
dialog
))
save_btn.pack(side
=
tk.RIGHT, padx
=
5
)
cancel_btn
=
tk.Button(btn_frame, text
=
"取消"
, command
=
dialog.destroy)
cancel_btn.pack(side
=
tk.RIGHT, padx
=
5
)
dialog.transient(
self
.root)
dialog.grab_set()
def
create_widgets(
self
):
self
.notebook
=
ttk.Notebook(
self
.root)
self
.notebook.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
self
.ocr_frame
=
tk.Frame(
self
.notebook)
self
.notebook.add(
self
.ocr_frame, text
=
"OCR识别"
)
self
.replace_frame
=
tk.Frame(
self
.notebook)
self
.notebook.add(
self
.replace_frame, text
=
"重命名"
)
self
.create_ocr_widgets()
self
.create_replace_widgets()
def
create_ocr_widgets(
self
):
self
.left_panel
=
tk.Frame(
self
.ocr_frame, width
=
600
, height
=
550
)
self
.left_panel.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
self
.left_panel.pack_propagate(
False
)
self
.image_title
=
tk.Label(
self
.left_panel, text
=
"图片预览"
)
self
.image_title.pack(fill
=
tk.X, pady
=
(
0
,
5
))
self
.image_container
=
tk.Frame(
self
.left_panel, highlightbackground
=
"#cccccc"
, highlightthickness
=
1
)
self
.image_container.pack(fill
=
tk.BOTH, expand
=
True
)
self
.image_label
=
tk.Label(
self
.image_container)
self
.image_label.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
5
, pady
=
5
)
self
.right_panel
=
tk.Frame(
self
.ocr_frame, width
=
350
, height
=
550
)
self
.right_panel.pack(side
=
tk.RIGHT, fill
=
tk.Y, padx
=
10
, pady
=
10
)
self
.right_panel.pack_propagate(
False
)
self
.ocr_mode_frame
=
tk.LabelFrame(
self
.right_panel, text
=
"OCR模式选择"
, padx
=
10
, pady
=
10
)
self
.ocr_mode_frame.pack(fill
=
tk.X, pady
=
5
)
self
.ocr_mode_var
=
tk.IntVar(value
=
self
.ocr_mode.value)
tk.Radiobutton(
self
.ocr_mode_frame,
text
=
"腾讯云OCR (在线)"
,
variable
=
self
.ocr_mode_var,
value
=
OcrMode.TENCENT.value,
command
=
self
.on_ocr_mode_change
).pack(anchor
=
tk.W, pady
=
2
)
paddle_state
=
tk.NORMAL
if
PADDLEOCR_AVAILABLE
else
tk.DISABLED
paddle_text
=
"PaddleOCR (本地)"
if
PADDLEOCR_AVAILABLE
else
"PaddleOCR (本地, 不可用)"
self
.paddle_radio
=
tk.Radiobutton(
self
.ocr_mode_frame,
text
=
paddle_text,
variable
=
self
.ocr_mode_var,
value
=
OcrMode.PADDLE.value,
command
=
self
.on_ocr_mode_change,
state
=
paddle_state
)
self
.paddle_radio.pack(anchor
=
tk.W, pady
=
2
)
self
.api_frame
=
tk.LabelFrame(
self
.right_panel, text
=
"腾讯云OCR配置"
, padx
=
10
, pady
=
10
)
self
.api_frame.pack(fill
=
tk.X, pady
=
5
)
tk.Label(
self
.api_frame, text
=
"SecretId:"
).pack(anchor
=
tk.W, pady
=
(
0
,
2
))
self
.secret_id_entry
=
tk.Entry(
self
.api_frame)
self
.secret_id_entry.insert(
0
,
self
.secret_id)
self
.secret_id_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
tk.Label(
self
.api_frame, text
=
"SecretKey:"
).pack(anchor
=
tk.W, pady
=
(
0
,
2
))
self
.secret_key_entry
=
tk.Entry(
self
.api_frame, show
=
"*"
)
self
.secret_key_entry.insert(
0
,
self
.secret_key)
self
.secret_key_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
self
.select_btn
=
tk.Button(
self
.right_panel, text
=
"选择图片"
, command
=
self
.select_images)
self
.select_btn.pack(fill
=
tk.X, pady
=
10
, padx
=
5
)
self
.options_frame
=
tk.LabelFrame(
self
.right_panel, text
=
"识别选项"
, padx
=
10
, pady
=
10
)
self
.options_frame.pack(fill
=
tk.X, pady
=
5
, padx
=
5
)
self
.rename_var
=
tk.IntVar(value
=
1
)
tk.Checkbutton(
self
.options_frame, text
=
"自动重命名文件"
, variable
=
self
.rename_var).pack(anchor
=
tk.W, pady
=
2
)
self
.suffix_var
=
tk.IntVar(value
=
0
)
tk.Checkbutton(
self
.options_frame, text
=
"添加序号后缀"
, variable
=
self
.suffix_var).pack(anchor
=
tk.W, pady
=
2
)
self
.keywords_frame
=
tk.LabelFrame(
self
.right_panel, text
=
"关键字设置(按顺序组合)"
, padx
=
10
, pady
=
10
)
self
.keywords_frame.pack(fill
=
tk.X, pady
=
5
, padx
=
5
)
tk.Label(
self
.keywords_frame, text
=
"用逗号分隔,如: 编号,名称,日期"
).pack(anchor
=
tk.W, pady
=
(
0
,
5
))
self
.keywords_entry
=
tk.Entry(
self
.keywords_frame)
self
.keywords_entry.insert(
0
,
self
.keywords)
self
.keywords_entry.pack(fill
=
tk.X, pady
=
(
0
,
5
))
self
.progress
=
ttk.Progressbar(
self
.right_panel, orient
=
tk.HORIZONTAL, mode
=
'determinate'
)
self
.progress.pack(fill
=
tk.X, pady
=
10
, padx
=
5
)
self
.process_btn
=
tk.Button(
self
.right_panel, text
=
"识别并重命名"
, command
=
self
.start_processing)
self
.process_btn.pack(fill
=
tk.X, pady
=
10
, padx
=
5
)
self
.result_frame
=
tk.LabelFrame(
self
.right_panel, text
=
"识别结果"
, padx
=
10
, pady
=
10
)
self
.result_frame.pack(fill
=
tk.BOTH, expand
=
True
, pady
=
5
, padx
=
5
)
self
.result_text
=
tk.Text(
self
.result_frame, height
=
10
, wrap
=
tk.WORD)
scrollbar
=
tk.Scrollbar(
self
.result_frame)
scrollbar.pack(side
=
tk.RIGHT, fill
=
tk.Y)
self
.result_text.pack(fill
=
tk.BOTH, expand
=
True
)
self
.result_text.config(yscrollcommand
=
scrollbar.
set
)
scrollbar.config(command
=
self
.result_text.yview)
self
.nav_frame
=
tk.Frame(
self
.right_panel)
self
.nav_frame.pack(fill
=
tk.X, pady
=
10
, padx
=
5
)
self
.prev_btn
=
tk.Button(
self
.nav_frame, text
=
"上一张"
, command
=
self
.prev_image)
self
.prev_btn.pack(side
=
tk.LEFT, expand
=
True
, padx
=
2
)
self
.next_btn
=
tk.Button(
self
.nav_frame, text
=
"下一张"
, command
=
self
.next_image)
self
.next_btn.pack(side
=
tk.RIGHT, expand
=
True
, padx
=
2
)
self
.status_var
=
tk.StringVar()
self
.status_var.
set
(
"就绪"
)
self
.status_bar
=
tk.Label(
self
.root, textvariable
=
self
.status_var, bd
=
1
, relief
=
tk.SUNKEN, anchor
=
tk.W)
self
.status_bar.pack(side
=
tk.BOTTOM, fill
=
tk.X)
def
create_replace_widgets(
self
):
control_frame
=
tk.Frame(
self
.replace_frame, padx
=
15
, pady
=
15
)
control_frame.pack(fill
=
tk.X)
self
.replace_select_btn
=
tk.Button(control_frame, text
=
"选择文件"
, command
=
self
.select_replace_files)
self
.replace_select_btn.pack(side
=
tk.LEFT, padx
=
5
)
tk.Label(control_frame, text
=
"关键词:"
).pack(side
=
tk.LEFT, padx
=
5
)
self
.keyword_entry
=
tk.Entry(control_frame, width
=
20
)
self
.keyword_entry.pack(side
=
tk.LEFT, padx
=
5
)
self
.keyword_entry.insert(
0
,
"名称"
)
tk.Label(control_frame, text
=
"新名称:"
).pack(side
=
tk.LEFT, padx
=
5
)
self
.new_name_entry
=
tk.Entry(control_frame, width
=
30
)
self
.new_name_entry.pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_number_var
=
tk.IntVar(value
=
1
)
tk.Checkbutton(control_frame, text
=
"添加编号"
, variable
=
self
.replace_number_var).pack(side
=
tk.LEFT, padx
=
5
)
tk.Label(control_frame, text
=
"起始:"
).pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_start_num
=
tk.Spinbox(control_frame, from_
=
1
, to
=
9999
, width
=
5
)
self
.replace_start_num.pack(side
=
tk.LEFT, padx
=
5
)
tk.Label(control_frame, text
=
"位数:"
).pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_digits_num
=
tk.Spinbox(control_frame, from_
=
1
, to
=
5
, width
=
3
)
self
.replace_digits_num.pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_digits_num.delete(
0
, tk.END)
self
.replace_digits_num.insert(
0
,
"3"
)
self
.replace_btn
=
tk.Button(control_frame, text
=
"批量重命名"
, command
=
self
.start_replace_renaming)
self
.replace_btn.pack(side
=
tk.LEFT, padx
=
5
)
display_frame
=
tk.Frame(
self
.replace_frame)
display_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
15
, pady
=
10
)
self
.replace_image_frame
=
tk.LabelFrame(display_frame, text
=
"图片预览"
, width
=
400
, height
=
400
)
self
.replace_image_frame.pack_propagate(
False
)
self
.replace_image_frame.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
, padx
=
(
0
,
10
))
self
.replace_image_label
=
tk.Label(
self
.replace_image_frame)
self
.replace_image_label.pack(fill
=
tk.BOTH, expand
=
True
)
list_frame
=
tk.LabelFrame(display_frame, text
=
"文件列表 (共0个文件)"
, width
=
300
)
list_frame.pack_propagate(
False
)
list_frame.pack(side
=
tk.RIGHT, fill
=
tk.BOTH)
self
.replace_file_listbox
=
tk.Listbox(list_frame, selectmode
=
tk.SINGLE)
self
.replace_file_listbox.pack(fill
=
tk.BOTH, expand
=
True
)
self
.replace_file_listbox.bind(
'<<ListboxSelect>>'
,
self
.on_replace_file_select)
scrollbar
=
tk.Scrollbar(
self
.replace_file_listbox)
scrollbar.pack(side
=
tk.RIGHT, fill
=
tk.Y)
self
.replace_file_listbox.config(yscrollcommand
=
scrollbar.
set
)
scrollbar.config(command
=
self
.replace_file_listbox.yview)
nav_frame
=
tk.Frame(
self
.replace_frame, pady
=
10
)
nav_frame.pack(fill
=
tk.X, padx
=
15
)
self
.replace_prev_btn
=
tk.Button(nav_frame, text
=
"上一个"
, command
=
self
.replace_prev_file)
self
.replace_prev_btn.pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_next_btn
=
tk.Button(nav_frame, text
=
"下一个"
, command
=
self
.replace_next_file)
self
.replace_next_btn.pack(side
=
tk.LEFT, padx
=
5
)
self
.replace_progress
=
ttk.Progressbar(
self
.replace_frame, orient
=
tk.HORIZONTAL, mode
=
'determinate'
)
self
.replace_progress.pack(fill
=
tk.X, padx
=
15
, pady
=
10
)
self
.replace_status_var
=
tk.StringVar()
self
.replace_status_var.
set
(
"就绪"
)
status_bar
=
tk.Label(
self
.replace_frame, textvariable
=
self
.replace_status_var, bd
=
1
, relief
=
tk.SUNKEN, anchor
=
tk.W)
status_bar.pack(fill
=
tk.X, padx
=
15
, pady
=
5
)
def
on_ocr_mode_change(
self
):
selected_mode
=
OcrMode(
self
.ocr_mode_var.get())
if
selected_mode !
=
self
.ocr_mode:
self
.ocr_mode
=
selected_mode
self
.save_config()
if
self
.ocr_mode
=
=
OcrMode.PADDLE
and
self
.paddle_ocr
is
None
and
PADDLEOCR_AVAILABLE:
self
.initialize_paddle_ocr()
def
select_images(
self
):
if
self
.processing:
messagebox.showwarning(
"警告"
,
"正在处理中,请稍后再选择图片"
)
return
files
=
filedialog.askopenfilenames(
title
=
"选择图片文件"
,
filetypes
=
[(
"图片文件"
,
"*.jpg *.jpeg *.png *.bmp *.gif"
), (
"所有文件"
,
"*.*"
)]
)
if
files:
self
.image_files
=
list
(files)
self
.current_index
=
0
self
.show_current_image()
self
.status_var.
set
(f
"已选择 {len(self.image_files)} 张图片"
)
self
.result_text.delete(
1.0
, tk.END)
def
show_current_image(
self
):
if
not
self
.image_files:
return
try
:
image_path
=
self
.image_files[
self
.current_index]
img
=
Image.
open
(image_path)
max_size
=
(
550
,
500
)
img.thumbnail(max_size, Image.LANCZOS)
photo
=
ImageTk.PhotoImage(img)
self
.image_label.config(image
=
photo)
self
.image_label.image
=
photo
self
.status_var.
set
(f
"图片 {self.current_index + 1}/{len(self.image_files)}: {os.path.basename(image_path)}"
)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"无法加载图片: {str(e)}"
)
def
prev_image(
self
):
if
self
.processing:
return
if
self
.image_files
and
self
.current_index >
0
:
self
.current_index
-
=
1
self
.show_current_image()
def
next_image(
self
):
if
self
.processing:
return
if
self
.image_files
and
self
.current_index <
len
(
self
.image_files)
-
1
:
self
.current_index
+
=
1
self
.show_current_image()
def
start_processing(
self
):
if
self
.processing:
return
if
not
self
.image_files:
messagebox.showwarning(
"警告"
,
"请先选择图片文件"
)
return
self
.secret_id
=
self
.secret_id_entry.get().strip()
self
.secret_key
=
self
.secret_key_entry.get().strip()
self
.keywords
=
self
.keywords_entry.get().strip()
if
self
.ocr_mode
=
=
OcrMode.TENCENT:
if
not
self
.secret_id
or
not
self
.secret_key:
messagebox.showwarning(
"警告"
,
"请输入腾讯云SecretId和SecretKey"
)
return
if
not
self
.keywords:
messagebox.showwarning(
"警告"
,
"请输入至少一个关键字"
)
return
self
.processing
=
True
self
.select_btn.config(state
=
tk.DISABLED)
self
.process_btn.config(state
=
tk.DISABLED)
self
.prev_btn.config(state
=
tk.DISABLED)
self
.next_btn.config(state
=
tk.DISABLED)
self
.progress[
"value"
]
=
0
self
.progress[
"maximum"
]
=
len
(
self
.image_files)
self
.result_text.delete(
1.0
, tk.END)
Thread(target
=
self
.process_images, daemon
=
True
).start()
def
recognize_text(
self
, image_path):
if
self
.ocr_mode
=
=
OcrMode.TENCENT:
return
self
.recognize_text_with_tencent_ocr(image_path)
elif
self
.ocr_mode
=
=
OcrMode.PADDLE
and
self
.paddle_ocr
is
not
None
:
return
self
.recognize_text_with_paddle_ocr(image_path)
else
:
raise
Exception(
"当前OCR模式不可用"
)
def
recognize_text_with_paddle_ocr(
self
, image_path):
try
:
result
=
self
.paddle_ocr.ocr(image_path,
cls
=
True
)
recognized_text
=
""
if
result
is
not
None
:
for
line
in
result:
if
line:
for
detection
in
line:
if
detection
and
detection[
1
]:
recognized_text
+
=
detection[
1
][
0
]
+
"\n"
return
recognized_text.strip()
except
Exception as e:
raise
Exception(f
"PaddleOCR识别失败: {str(e)}"
)
def
recognize_text_with_tencent_ocr(
self
, image_path):
try
:
with
open
(image_path,
"rb"
) as image_file:
image_data
=
image_file.read()
image_base64
=
base64.b64encode(image_data).decode(
'utf-8'
)
action
=
"GeneralBasicOCR"
region
=
"ap-guangzhou"
endpoint
=
"ocr.tencentcloudapi.com"
service
=
"ocr"
version
=
"2018-11-19"
algorithm
=
"TC3-HMAC-SHA256"
timestamp
=
int
(time.time())
date
=
time.strftime(
"%Y-%m-%d"
, time.gmtime(timestamp))
http_request_method
=
"POST"
canonical_uri
=
"/"
canonical_querystring
=
""
canonical_headers
=
"content-type:application/json; charset=utf-8\n"
+
f
"host:{endpoint}\n"
signed_headers
=
"content-type;host"
payload
=
{
"ImageBase64"
: image_base64,
"LanguageType"
:
"auto"
}
payload_str
=
json.dumps(payload)
hashed_request_payload
=
hashlib.sha256(payload_str.encode(
'utf-8'
)).hexdigest()
canonical_request
=
(http_request_method
+
"\n"
+
canonical_uri
+
"\n"
+
canonical_querystring
+
"\n"
+
canonical_headers
+
"\n"
+
signed_headers
+
"\n"
+
hashed_request_payload)
credential_scope
=
date
+
"/"
+
service
+
"/"
+
"tc3_request"
hashed_canonical_request
=
hashlib.sha256(canonical_request.encode(
'utf-8'
)).hexdigest()
string_to_sign
=
(algorithm
+
"\n"
+
str
(timestamp)
+
"\n"
+
credential_scope
+
"\n"
+
hashed_canonical_request)
secret_date
=
hmac.new((
"TC3"
+
self
.secret_key).encode(
'utf-8'
),
date.encode(
'utf-8'
), hashlib.sha256).digest()
secret_service
=
hmac.new(secret_date, service.encode(
'utf-8'
), hashlib.sha256).digest()
secret_signing
=
hmac.new(secret_service,
"tc3_request"
.encode(
'utf-8'
), hashlib.sha256).digest()
signature
=
hmac.new(secret_signing, string_to_sign.encode(
'utf-8'
), hashlib.sha256).hexdigest()
authorization
=
(algorithm
+
" "
+
"Credential="
+
self
.secret_id
+
"/"
+
credential_scope
+
", "
+
"SignedHeaders="
+
signed_headers
+
", "
+
"Signature="
+
signature)
headers
=
{
"Authorization"
: authorization,
"Content-Type"
:
"application/json; charset=utf-8"
,
"Host"
: endpoint,
"X-TC-Action"
: action,
"X-TC-Version"
: version,
"X-TC-Timestamp"
:
str
(timestamp),
"X-TC-Region"
: region
}
response
=
requests.post(f
"https://{endpoint}"
, headers
=
headers, data
=
payload_str, timeout
=
30
)
if
response.status_code !
=
200
:
error_data
=
response.json()
error_msg
=
error_data.get(
"Response"
, {}).get(
"Error"
, {}).get(
"Message"
, f
"HTTP {response.status_code} 错误"
)
raise
Exception(f
"腾讯OCR API请求错误: {error_msg}"
)
result
=
response.json()
text_detections
=
result.get(
"Response"
, {}).get(
"TextDetections"
, [])
recognized_text
=
""
for
detection
in
text_detections:
recognized_text
+
=
detection.get(
"DetectedText"
, "
") + "
\n"
return
recognized_text.strip()
except
requests.exceptions.RequestException as e:
error_msg
=
str
(e)
if
hasattr
(e,
"response"
)
and
e.response
is
not
None
:
try
:
error_data
=
e.response.json()
error_msg
=
error_data.get(
"Response"
, {}).get(
"Error"
, {}).get(
"Message"
, error_msg)
except
:
pass
raise
Exception(f
"API请求失败: {error_msg}"
)
except
Exception as e:
raise
Exception(f
"识别过程中出错: {str(e)}"
)
def
extract_keyword_contents(
self
, text):
keywords
=
[kw.strip()
for
kw
in
self
.keywords.split(
','
)
if
kw.strip()]
extracted_contents
=
[]
for
keyword
in
keywords:
index
=
text.find(keyword)
if
index !
=
-
1
:
content_start
=
index
+
len
(keyword)
next_pos
=
None
for
kw
in
keywords:
pos
=
text.find(kw, content_start)
if
pos !
=
-
1
and
(next_pos
is
None
or
pos < next_pos):
next_pos
=
pos
content
=
text[content_start:next_pos].strip()
if
next_pos
else
text[content_start:].strip()
for
sep
in
[
":"
,
":"
,
" "
]:
if
content.startswith(sep):
content
=
content[
len
(sep):].strip()
break
extracted_contents.append(content.split(
'\n'
)[
0
].strip())
else
:
extracted_contents.append("")
return
extracted_contents
def
process_images(
self
):
try
:
for
i, image_path
in
enumerate
(
self
.image_files):
self
.root.after(
0
,
lambda
v
=
i
+
1
:
self
.progress.configure(value
=
v))
self
.root.after(
0
,
self
.status_var.
set
,
f
"正在处理 {i+1}/{len(self.image_files)}: {os.path.basename(image_path)}"
)
try
:
recognized_text
=
self
.recognize_text(image_path)
if
not
recognized_text:
recognized_text
=
"未识别到文字"
self
.root.after(
0
,
self
.result_text.insert, tk.END,
f
"{os.path.basename(image_path)}:\n{recognized_text}\n\n"
)
if
self
.rename_var.get():
keyword_contents
=
self
.extract_keyword_contents(recognized_text)
filtered_contents
=
[c
for
c
in
keyword_contents
if
c]
new_name
=
"_"
.join(filtered_contents)
if
filtered_contents
else
"未找到关键字内容"
if
new_name:
new_name
=
self
.generate_valid_filename(new_name)
if
self
.suffix_var.get():
new_name
=
f
"{new_name}_{i+1:03d}"
new_path
=
self
.rename_file(image_path, new_name)
self
.image_files[i]
=
new_path
self
.root.after(
0
,
self
.result_text.insert, tk.END,
f
"已重命名为: {os.path.basename(new_path)}\n\n"
)
except
Exception as e:
self
.logger.error(f
"处理图片时出错: {str(e)}"
)
self
.root.after(
0
,
self
.result_text.insert, tk.END,
f
"处理 {os.path.basename(image_path)} 时出错: {str(e)}\n\n"
)
if
i
=
=
self
.current_index:
self
.root.after(
0
,
self
.show_current_image)
self
.root.after(
0
,
self
.status_var.
set
,
"处理完成"
)
if
self
.image_files:
folder_path
=
os.path.dirname(
self
.image_files[
0
])
def
show_completion_and_open_folder():
result
=
messagebox.showinfo(
"处理完成"
, f
"已处理 {len(self.image_files)} 个文件\n点击确定打开文件夹"
)
self
.open_folder(folder_path)
self
.root.after(
0
, show_completion_and_open_folder)
else
:
self
.root.after(
0
, messagebox.showinfo,
"处理完成"
, f
"已处理 {len(self.image_files)} 个文件"
)
except
Exception as e:
self
.logger.error(f
"处理过程中发生错误: {str(e)}"
)
self
.root.after(
0
, messagebox.showerror,
"错误"
, f
"处理过程中发生错误: {str(e)}"
)
finally
:
self
.processing
=
False
self
.root.after(
0
,
self
.select_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.process_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.prev_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.next_btn.config, {
'state'
: tk.NORMAL})
def
generate_valid_filename(
self
, text, max_length
=
100
):
if
not
text:
return
None
invalid_chars
=
'<>:"/\\|?*\n\r\t'
for
char
in
invalid_chars:
text
=
text.replace(char, '')
text
=
text.replace(
' '
,
'_'
)
text
=
text.strip()
if
len
(text) > max_length:
text
=
text[:max_length]
return
text
if
text
else
"unnamed"
def
rename_file(
self
, old_path, new_name):
dir_name
=
os.path.dirname(old_path)
ext
=
os.path.splitext(old_path)[
1
]
counter
=
1
base_new_name
=
new_name
while
True
:
new_path
=
os.path.join(dir_name, f
"{base_new_name}{ext}"
)
if
not
os.path.exists(new_path):
break
base_new_name
=
f
"{new_name}_{counter}"
counter
+
=
1
os.rename(old_path, new_path)
return
new_path
def
select_replace_files(
self
):
if
self
.processing:
messagebox.showwarning(
"警告"
,
"正在处理中,请稍后再选择文件"
)
return
files
=
filedialog.askopenfilenames(title
=
"选择要重命名的文件"
)
if
files:
self
.replace_files_list
=
list
(files)
self
.replace_current_index
=
0
self
.update_replace_file_list()
self
.show_replace_current_file()
self
.replace_status_var.
set
(f
"已选择 {len(self.replace_files_list)} 个文件"
)
def
update_replace_file_list(
self
):
self
.replace_file_listbox.delete(
0
, tk.END)
for
file
in
self
.replace_files_list:
self
.replace_file_listbox.insert(tk.END, os.path.basename(
file
))
for
child
in
self
.replace_frame.winfo_children():
if
isinstance
(child, tk.LabelFrame)
and
child.cget(
"text"
).startswith(
"文件列表"
):
child.config(text
=
f
"文件列表 (共{len(self.replace_files_list)}个文件)"
)
break
def
show_replace_current_file(
self
):
if
not
self
.replace_files_list:
return
file_path
=
self
.replace_files_list[
self
.replace_current_index]
if
file_path.lower().endswith((
'.png'
,
'.jpg'
,
'.jpeg'
,
'.bmp'
,
'.gif'
)):
try
:
img
=
Image.
open
(file_path)
img.thumbnail((
400
,
400
))
photo
=
ImageTk.PhotoImage(img)
self
.replace_image_label.config(image
=
photo)
self
.replace_image_label.image
=
photo
except
Exception as e:
self
.replace_image_label.config(image
=
None
)
self
.replace_image_label.image
=
None
else
:
self
.replace_image_label.config(image
=
None
)
self
.replace_image_label.image
=
None
self
.replace_file_listbox.selection_clear(
0
, tk.END)
self
.replace_file_listbox.selection_set(
self
.replace_current_index)
self
.replace_file_listbox.see(
self
.replace_current_index)
self
.replace_status_var.
set
(f
"文件 {self.replace_current_index + 1}/{len(self.replace_files_list)}: {os.path.basename(file_path)}"
)
def
on_replace_file_select(
self
, event):
if
not
self
.replace_files_list
or
self
.processing:
return
selection
=
self
.replace_file_listbox.curselection()
if
selection:
self
.replace_current_index
=
selection[
0
]
self
.show_replace_current_file()
def
replace_prev_file(
self
):
if
self
.processing:
return
if
self
.replace_files_list
and
self
.replace_current_index >
0
:
self
.replace_current_index
-
=
1
self
.show_replace_current_file()
def
replace_next_file(
self
):
if
self
.processing:
return
if
self
.replace_files_list
and
self
.replace_current_index <
len
(
self
.replace_files_list)
-
1
:
self
.replace_current_index
+
=
1
self
.show_replace_current_file()
def
start_replace_renaming(
self
):
if
self
.processing:
return
if
not
self
.replace_files_list:
messagebox.showwarning(
"警告"
,
"请先选择文件"
)
return
keyword
=
self
.keyword_entry.get().strip()
new_name
=
self
.new_name_entry.get().strip()
if
not
new_name:
messagebox.showwarning(
"警告"
,
"请输入新名称"
)
return
self
.processing
=
True
self
.replace_select_btn.config(state
=
tk.DISABLED)
self
.replace_btn.config(state
=
tk.DISABLED)
self
.replace_prev_btn.config(state
=
tk.DISABLED)
self
.replace_next_btn.config(state
=
tk.DISABLED)
self
.replace_progress[
"value"
]
=
0
self
.replace_progress[
"maximum"
]
=
len
(
self
.replace_files_list)
Thread(
target
=
self
.replace_rename_files,
args
=
(keyword, new_name),
daemon
=
True
).start()
def
replace_rename_files(
self
, keyword, new_name):
try
:
new_files
=
[]
counter
=
int
(
self
.replace_start_num.get())
digits
=
int
(
self
.replace_digits_num.get())
add_number
=
self
.replace_number_var.get()
for
i, file_path
in
enumerate
(
self
.replace_files_list):
self
.root.after(
0
,
lambda
v
=
i
+
1
:
self
.replace_progress.configure(value
=
v))
self
.root.after(
0
,
self
.replace_status_var.
set
,
f
"正在处理 {i+1}/{len(self.replace_files_list)}: {os.path.basename(file_path)}"
)
dir_name
=
os.path.dirname(file_path)
file_name, file_ext
=
os.path.splitext(os.path.basename(file_path))
if
keyword:
if
keyword
in
file_name:
new_file_name
=
file_name.replace(keyword, new_name)
else
:
new_file_name
=
f
"{new_name}_{i+1}"
else
:
new_file_name
=
new_name
if
add_number:
new_file_name
=
f
"{new_file_name}_{counter:0{digits}d}"
counter
+
=
1
unique_counter
=
1
base_new_name
=
new_file_name
while
True
:
new_path
=
os.path.join(dir_name, f
"{base_new_name}{file_ext}"
)
if
not
os.path.exists(new_path):
break
base_new_name
=
f
"{new_file_name}_{unique_counter}"
unique_counter
+
=
1
os.rename(file_path, new_path)
new_files.append(new_path)
self
.root.after(
0
,
self
.replace_file_listbox.delete, i)
self
.root.after(
0
,
self
.replace_file_listbox.insert, i, os.path.basename(new_path))
if
i
=
=
self
.replace_current_index:
self
.root.after(
0
,
lambda
:
self
.replace_files_list.__setitem__(
self
.replace_current_index, new_path))
self
.root.after(
0
,
self
.show_replace_current_file)
self
.replace_files_list
=
new_files
self
.root.after(
0
,
self
.replace_status_var.
set
, f
"重命名完成,共处理 {len(self.replace_files_list)} 个文件"
)
if
self
.replace_files_list:
folder_path
=
os.path.dirname(
self
.replace_files_list[
0
])
def
show_completion_and_open_folder():
result
=
messagebox.showinfo(
"重命名完成"
, f
"已处理 {len(self.replace_files_list)} 个文件\n点击确定打开文件夹"
)
self
.open_folder(folder_path)
self
.root.after(
0
, show_completion_and_open_folder)
else
:
self
.root.after(
0
, messagebox.showinfo,
"重命名完成"
, f
"已处理 {len(self.replace_files_list)} 个文件"
)
except
Exception as e:
self
.root.after(
0
, messagebox.showerror,
"错误"
, f
"重命名过程中出错: {str(e)}"
)
self
.root.after(
0
,
self
.replace_status_var.
set
,
"重命名过程中出错"
)
finally
:
self
.processing
=
False
self
.root.after(
0
,
self
.replace_select_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.replace_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.replace_prev_btn.config, {
'state'
: tk.NORMAL})
self
.root.after(
0
,
self
.replace_next_btn.config, {
'state'
: tk.NORMAL})
def
open_folder(
self
, folder_path):
try
:
if
os.path.exists(folder_path):
if
os.name
=
=
'nt'
:
os.startfile(folder_path)
else
:
subprocess.call([
'open'
, folder_path])
else
:
messagebox.showerror(
"错误"
, f
"文件夹不存在: {folder_path}"
)
except
Exception as e:
self
.logger.error(f
"打开文件夹时出错: {str(e)}"
)
messagebox.showerror(
"错误"
, f
"打开文件夹时出错: {str(e)}"
)
def
bind_shortcuts(
self
):
self
.root.bind(
'<Control-o>'
,
lambda
e:
self
.select_image())
self
.root.bind(
'<Control-s>'
,
lambda
e:
self
.save_result())
self
.root.bind(
'<Control-r>'
,
lambda
e:
self
.recognize_text())
self
.root.bind(
'<Control-c>'
,
lambda
e:
self
.copy_text())
self
.root.bind(
'<Control-f>'
,
lambda
e:
self
.select_replace_files())
self
.root.bind(
'<Control-b>'
,
lambda
e:
self
.batch_rename())
self
.root.bind(
'<Control-1>'
,
lambda
e:
self
.notebook.select(
0
))
self
.root.bind(
'<Control-2>'
,
lambda
e:
self
.notebook.select(
1
))
self
.root.bind(
'<Control-p>'
,
lambda
e:
self
.show_ocr_config())
def
handle_drop(
self
, event):
file_path
=
event.data
file_path
=
file_path.strip(
'{}'
).strip(
'"'
)
if
file_path.lower().endswith((
'.png'
,
'.jpg'
,
'.jpeg'
,
'.bmp'
,
'.gif'
)):
self
.image_path
=
file_path
self
.display_image()
self
.drop_label.place_forget()
else
:
messagebox.showerror(
"错误"
,
"请拖放图片文件(支持 PNG、JPG、JPEG、BMP、GIF 格式)"
)
def
display_image(
self
):
if
self
.image_path:
try
:
image
=
Image.
open
(
self
.image_path)
display_size
=
(
400
,
400
)
image.thumbnail(display_size, Image.Resampling.LANCZOS)
photo
=
ImageTk.PhotoImage(image)
self
.image_label.configure(image
=
photo)
self
.image_label.image
=
photo
self
.filename_label.configure(text
=
f
"文件名: {os.path.basename(self.image_path)}"
)
self
.recognize_button.configure(state
=
'normal'
)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"无法加载图片: {str(e)}"
)
self
.image_path
=
None
self
.image_label.configure(image
=
'')
self
.filename_label.configure(text
=
"文件名: "
)
self
.recognize_button.configure(state
=
'disabled'
)
def
initialize_paddle_ocr(
self
):
if
PADDLEOCR_AVAILABLE:
try
:
self
.paddle_ocr
=
PaddleOCR(
use_angle_cls
=
True
,
lang
=
"ch"
,
det_model_dir
=
None
,
rec_model_dir
=
None
,
cls_model_dir
=
None
,
use_gpu
=
False
,
enable_mkldnn
=
True
,
det_db_thresh
=
0.3
,
det_db_box_thresh
=
0.5
,
det_db_unclip_ratio
=
1.6
,
max_batch_size
=
10
,
use_dilation
=
False
,
det_db_score_mode
=
"fast"
,
drop_score
=
0.5
,
rec_char_dict_path
=
None
,
show_log
=
False
)
self
.logger.info(
"PaddleOCR初始化成功"
)
except
Exception as e:
self
.logger.error(f
"PaddleOCR初始化失败: {str(e)}"
)
messagebox.showerror(
"错误"
, f
"PaddleOCR初始化失败: {str(e)}"
)
else
:
self
.logger.warning(
"PaddleOCR未安装"
)
messagebox.showwarning(
"警告"
,
"PaddleOCR未安装,请使用pip install paddlepaddle paddleocr安装"
)
def
save_ocr_config(
self
, ocr_mode_value, secret_id, secret_key, dialog):
new_mode
=
OcrMode(ocr_mode_value)
if
new_mode !
=
self
.ocr_mode:
self
.ocr_mode
=
new_mode
if
self
.ocr_mode
=
=
OcrMode.PADDLE
and
self
.paddle_ocr
is
None
and
PADDLEOCR_AVAILABLE:
self
.initialize_paddle_ocr()
self
.secret_id
=
secret_id
self
.secret_key
=
secret_key
self
.save_config()
self
.ocr_mode_var.
set
(
self
.ocr_mode.value)
dialog.destroy()
messagebox.showinfo(
"成功"
,
"OCR配置已保存"
)
def
save_keywords_config(
self
, keywords, dialog):
self
.keywords
=
keywords
self
.keywords_entry.delete(
0
, tk.END)
self
.keywords_entry.insert(
0
,
self
.keywords)
self
.save_config()
dialog.destroy()
messagebox.showinfo(
"成功"
,
"关键词设置已保存"
)
def
show_help(
self
):
help_text
=
f
self
.show_info_dialog(
"使用帮助"
, help_text)
def
check_update(
self
):
messagebox.showinfo(
"检查更新"
, f
"当前已是最新版本 ({self.version})"
)
def
show_about(
self
):
about_text
=
f
self
.show_info_dialog(
"关于"
, about_text)
def
show_info_dialog(
self
, title, message):
dialog
=
tk.Toplevel(
self
.root)
dialog.title(title)
dialog.resizable(
True
,
True
)
dialog.geometry(f
"650x500+{self.root.winfo_x()+100}+{self.root.winfo_y()+100}"
)
text_frame
=
tk.Frame(dialog, padx
=
15
, pady
=
15
)
text_frame.pack(fill
=
tk.BOTH, expand
=
True
, padx
=
10
, pady
=
10
)
text
=
tk.Text(text_frame, wrap
=
tk.WORD, padx
=
10
, pady
=
10
)
text.insert(tk.END, message)
text.config(state
=
tk.DISABLED)
text.pack(fill
=
tk.BOTH, expand
=
True
)
scrollbar
=
tk.Scrollbar(text_frame)
scrollbar.pack(side
=
tk.RIGHT, fill
=
tk.Y)
text.config(yscrollcommand
=
scrollbar.
set
)
scrollbar.config(command
=
text.yview)
btn_frame
=
tk.Frame(dialog, pady
=
10
)
btn_frame.pack(fill
=
tk.X, padx
=
10
)
close_btn
=
tk.Button(btn_frame, text
=
"关闭"
, command
=
dialog.destroy)
close_btn.pack()
dialog.transient(
self
.root)
dialog.grab_set()
self
.root.wait_window(dialog)
def
load_config(
self
):
config
=
configparser.ConfigParser()
if
self
.config_file.exists():
try
:
config.read(
self
.config_file)
self
.secret_id
=
config.get(
'TENCENT'
,
'SecretId'
, fallback
=
'')
self
.secret_key
=
config.get(
'TENCENT'
,
'SecretKey'
, fallback
=
'')
self
.keywords
=
config.get(
'SETTINGS'
,
'Keywords'
, fallback
=
'编号,名称,日期'
)
ocr_mode
=
config.get(
'SETTINGS'
,
'OcrMode'
, fallback
=
'tencent'
)
if
ocr_mode.lower()
=
=
'paddle'
and
PADDLEOCR_AVAILABLE:
self
.ocr_mode
=
OcrMode.PADDLE
else
:
self
.ocr_mode
=
OcrMode.TENCENT
except
Exception as e:
self
.logger.error(f
"加载配置文件失败: {str(e)}"
)
def
save_config(
self
):
config
=
configparser.ConfigParser()
config[
'TENCENT'
]
=
{
'SecretId'
:
self
.secret_id,
'SecretKey'
:
self
.secret_key
}
config[
'SETTINGS'
]
=
{
'Keywords'
:
self
.keywords,
'OcrMode'
:
'paddle'
if
self
.ocr_mode
=
=
OcrMode.PADDLE
else
'tencent'
}
try
:
with
open
(
self
.config_file,
'w'
) as f:
config.write(f)
except
Exception as e:
self
.logger.error(f
"保存配置文件失败: {str(e)}"
)
def
on_closing(
self
):
self
.save_config()
self
.root.destroy()
if
__name__
=
=
"__main__"
:
if
TKDND_AVAILABLE:
root
=
TkinterDnD.Tk()
else
:
root
=
tk.Tk()
messagebox.showwarning(
"警告"
,
"未检测到tkinterdnd2库,拖放功能将不可用。\n"
"如需使用拖放功能,请安装: pip install tkinterdnd2"
)
if
not
PADDLEOCR_AVAILABLE:
messagebox.showwarning(
"警告"
,
"未检测到PaddleOCR库,将仅支持腾讯OCR模式。\n"
"如需使用本地PaddleOCR,请安装: pip install paddlepaddle paddleocr"
)
app
=
ImageTextRenamerApp(root)
root.mainloop()