[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, colorchooser
from PIL import Image, ImageDraw, ImageFont, ImageTk
import os
import json
from datetime import datetime
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
except ImportError:
TkinterDnD = None
CONFIG_PATH = os.path.join(os.path.expanduser("~"), "watermark_config.json")
def save_config():
try:
config = {
"water_x": water_x.get(),
"water_y": water_y.get(),
"font_size": font_size_var.get(),
"font_family": font_family_var.get(),
"font_color": font_color_var.get(),
"font_alpha": font_alpha_var.get(),
"scale_enable": scale_enable.get(),
"scale_w": scale_w.get(),
"scale_h": scale_h.get(),
"time_pre": entry_time_pre.get(),
"time_val": entry_time_val.get(),
"loc_pre": entry_loc_pre.get(),
"loc_val": entry_loc_val.get(),
"lon_pre": entry_lon_pre.get(),
"lon_val": entry_lon_val.get(),
"lat_pre": entry_lat_pre.get(),
"lat_val": entry_lat_val.get(),
"remark_pre": entry_remark_pre.get(),
"remark_val": entry_remark_val.get(),
"show_time": var_time.get(),
"show_loc": var_loc.get(),
"show_lon": var_lon.get(),
"show_lat": var_lat.get(),
"show_remark": var_remark.get()
}
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception:
pass
def load_config():
if not os.path.exists(CONFIG_PATH):
return
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
c = json.load(f)
water_x.set(c.get("water_x", 50))
water_y.set(c.get("water_y", 60))
font_size_var.set(c.get("font_size", 42))
font_family_var.set(c.get("font_family", "SimHei"))
font_color_var.set(c.get("font_color", "#FFFFFF"))
font_alpha_var.set(c.get("font_alpha", 240))
scale_enable.set(c.get("scale_enable", False))
scale_w.set(c.get("scale_w", 1200))
scale_h.set(c.get("scale_h", 1600))
entry_time_pre.delete(0, tk.END)
entry_time_pre.insert(0, c.get("time_pre", "时间:"))
entry_time_val.delete(0, tk.END)
entry_time_val.insert(0, c.get("time_val", ""))
entry_loc_pre.delete(0, tk.END)
entry_loc_pre.insert(0, c.get("loc_pre", "地点:"))
entry_loc_val.delete(0, tk.END)
entry_loc_val.insert(0, c.get("loc_val", ""))
entry_lon_pre.delete(0, tk.END)
entry_lon_pre.insert(0, c.get("lon_pre", "经度:"))
entry_lon_val.delete(0, tk.END)
entry_lon_val.insert(0, c.get("lon_val", ""))
entry_lat_pre.delete(0, tk.END)
entry_lat_pre.insert(0, c.get("lat_pre", "纬度:"))
entry_lat_val.delete(0, tk.END)
entry_lat_val.insert(0, c.get("lat_val", ""))
entry_remark_pre.delete(0, tk.END)
entry_remark_pre.insert(0, c.get("remark_pre", "备注:"))
entry_remark_val.delete(0, tk.END)
entry_remark_val.insert(0, c.get("remark_val", ""))
var_time.set(c.get("show_time", True))
var_loc.set(c.get("show_loc", True))
var_lon.set(c.get("show_lon", True))
var_lat.set(c.get("show_lat", True))
var_remark.set(c.get("show_remark", True))
except Exception:
pass
if TkinterDnD:
root = TkinterDnD.Tk()
else:
root = tk.Tk()
root.title("水印批量工具")
root.geometry("1000x720")
root.resizable(False, False)
selected_paths = []
current_index = 0
drag_data = {"sx":0,"sy":0,"ox":0,"oy":0}
KEY_STEP = 1
sys_font_list = ["SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong", "Arial"]
water_x = tk.IntVar(value=50)
water_y = tk.IntVar(value=60)
font_size_var = tk.IntVar(value=42)
font_family_var = tk.StringVar(value="SimHei")
font_color_var = tk.StringVar(value="#FFFFFF")
font_alpha_var = tk.IntVar(value=240)
scale_enable = tk.BooleanVar(value=False)
scale_w = tk.IntVar(value=1200)
scale_h = tk.IntVar(value=1600)
var_time = tk.BooleanVar(value=True)
var_loc = tk.BooleanVar(value=True)
var_lon = tk.BooleanVar(value=True)
var_lat = tk.BooleanVar(value=True)
var_remark = tk.BooleanVar(value=True)
tip = tk.StringVar(value="未选择图片")
def get_font(size):
try:
return ImageFont.truetype(font_family_var.get(), size)
except:
try:
return ImageFont.truetype("simhei.ttf", size)
except:
return ImageFont.load_default()
def choose_color():
color = colorchooser.askcolor(title="选择水印颜色")[1]
if color:
font_color_var.set(color)
refresh_preview()
def make_watermark_img(img_path):
x = water_x.get()
y = water_y.get()
fs = font_size_var.get()
color = font_color_var.get()
alpha = font_alpha_var.get()
img = Image.open(img_path).convert("RGB")
if scale_enable.get():
img = img.resize((scale_w.get(), scale_h.get()), Image.Resampling.LANCZOS)
lines = []
if var_time.get():
lines.append(f"{entry_time_pre.get()}{entry_time_val.get()}")
if var_loc.get():
lines.append(f"{entry_loc_pre.get()}{entry_loc_val.get()}")
if var_lon.get():
lines.append(f"{entry_lon_pre.get()}{entry_lon_val.get()}")
if var_lat.get():
lines.append(f"{entry_lat_pre.get()}{entry_lat_val.get()}")
if var_remark.get():
lines.append(f"{entry_remark_pre.get()}{entry_remark_val.get()}")
rgba = img.convert("RGBA")
layer = Image.new("RGBA", rgba.size, (0,0,0,0))
draw = ImageDraw.Draw(layer)
if lines:
fnt = get_font(fs)
cy = y
r = int(color[1:3],16)
g = int(color[3:5],16)
b = int(color[5:7],16)
for txt in lines:
draw.text((x-1, cy-1), txt, font=fnt, fill=(0,0,0,alpha))
draw.text((x, cy), txt, font=fnt, fill=(r,g,b,alpha))
cy += int(fs * 1.25)
return Image.alpha_composite(rgba, layer).convert("RGB")
def show_preview(img):
max_w, max_h = 600, 500
w, h = img.size
scale = min(max_w/w, max_h/h)
nw, nh = int(w*scale), int(h*scale)
preview_img = img.resize((nw, nh), Image.Resampling.LANCZOS)
canvas = Image.new("RGB", (max_w, max_h), "#CCCCCC")
px = (max_w - nw)//2
py = (max_h - nh)//2
canvas.paste(preview_img,(px,py))
tk_img = ImageTk.PhotoImage(canvas)
lab_preview.config(image=tk_img)
lab_preview.image = tk_img
def refresh_preview(*args):
save_config()
if not selected_paths:
tip.set("未选择图片")
return
try:
src_img = make_watermark_img(selected_paths[current_index])
show_preview(src_img)
# 修复:每次刷新强制更新计数
tip.set(f"当前:{current_index+1}/{len(selected_paths)} 张")
except Exception as e:
print(e)
def on_mouse_down(e):
drag_data["sx"] = e.x
drag_data["sy"] = e.y
drag_data["ox"] = water_x.get()
drag_data["oy"] = water_y.get()
def on_mouse_drag(e):
dx = e.x - drag_data["sx"]
dy = e.y - drag_data["sy"]
water_x.set(drag_data["ox"]+dx)
water_y.set(drag_data["oy"]+dy)
refresh_preview()
def key_move(event):
step = KEY_STEP
if event.keysym == "Up":
water_y.set(water_y.get() - step)
elif event.keysym == "Down":
water_y.set(water_y.get() + step)
elif event.keysym == "Left":
water_x.set(water_x.get() - step)
elif event.keysym == "Right":
water_x.set(water_x.get() + step)
else:
return
refresh_preview()
root.bind("<Up>", key_move)
root.bind("<Down>", key_move)
root.bind("<Left>", key_move)
root.bind("<Right>", key_move)
def drop_files(e):
global selected_paths,current_index
try:
paths = list(root.tk.splitlist(e.data))
except:
return
exts = (".jpg",".jpeg",".png",".bmp")
valid = [p for p in paths if os.path.isfile(p) and p.lower().endswith(exts)]
if valid:
selected_paths = valid
current_index = 0
refresh_preview()
def select_files():
global selected_paths,current_index
fs = filedialog.askopenfilenames(filetypes=[("图片","*.jpg;*.jpeg;*.png;*.bmp")])
if fs:
selected_paths = list(fs)
current_index = 0
refresh_preview()
def prev():
global current_index
if not selected_paths:return
current_index = (current_index-1)%len(selected_paths)
refresh_preview()
def next():
global current_index
if not selected_paths:return
current_index = (current_index+1)%len(selected_paths)
refresh_preview()
def refresh_time():
entry_time_val.delete(0,tk.END)
entry_time_val.insert(0,datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
refresh_preview()
def export_all():
if not selected_paths:
messagebox.showwarning("提示","先导入图片!")
return
out_dir = filedialog.askdirectory(title="选择导出保存文件夹")
if not out_dir:
return
success = 0
fail = 0
for path in selected_paths:
try:
out_img = make_watermark_img(path)
filename = os.path.basename(path)
save_path = os.path.join(out_dir, filename)
out_img.save(save_path, quality=95)
success += 1
except Exception as err:
print("保存失败:",path,err)
fail += 1
messagebox.showinfo("导出结果",f"成功:{success}张\n失败:{fail}张")
# -------------------- 界面布局 --------------------
left_frame = ttk.Frame(root)
left_frame.pack(side=tk.LEFT, padx=10, fill=tk.BOTH, expand=True)
ttk.Label(left_frame, text="预览区|鼠标拖动|方向键微调水印", font=("",13,"bold")).pack(pady=5)
lab_preview = ttk.Label(left_frame, relief="solid")
lab_preview.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
lab_preview.bind("<Button-1>", on_mouse_down)
lab_preview.bind("<B1-Motion>", on_mouse_drag)
if TkinterDnD:
lab_preview.drop_target_register(DND_FILES)
lab_preview.dnd_bind("<<Drop>>", drop_files)
btn_row = ttk.Frame(left_frame)
btn_row.pack(fill=tk.X, pady=6)
ttk.Button(btn_row, text="上一张", command=prev).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=3)
ttk.Button(btn_row, text="下一张", command=next).pack(side=tk.RIGHT, expand=True, fill=tk.X, padx=3)
# 底部计数标签
ttk.Label(left_frame, textvariable=tip, font=("",11)).pack(pady=2)
# 右侧整体下移对齐
right_frame = ttk.Frame(root)
right_frame.pack(side=tk.RIGHT, padx=10, pady=38, fill=tk.Y)
ttk.Button(right_frame, text="选择图片", command=select_files).pack(fill=tk.X, pady=3)
ttk.Button(right_frame, text="刷新预览", command=refresh_preview).pack(fill=tk.X, pady=3)
ttk.Button(right_frame, text="批量导出", command=export_all).pack(fill=tk.X, pady=3)
pos_frame = ttk.LabelFrame(right_frame, text="水印位置&字号")
pos_frame.pack(fill=tk.X, pady=5)
ttk.Label(pos_frame, text="X").grid(row=0,column=0)
ttk.Entry(pos_frame, textvariable=water_x, width=5).grid(row=0,column=1,padx=2)
ttk.Label(pos_frame, text="Y").grid(row=0,column=2)
ttk.Entry(pos_frame, textvariable=water_y, width=5).grid(row=0,column=3,padx=2)
ttk.Label(pos_frame, text="字号").grid(row=1,column=0)
ttk.Spinbox(pos_frame, from_=10, to=150, textvariable=font_size_var, width=6, command=refresh_preview).grid(row=1,column=1,columnspan=3,pady=3)
font_frame = ttk.LabelFrame(right_frame, text="字体样式")
font_frame.pack(fill=tk.X, pady=5)
ttk.Label(font_frame, text="字体:").grid(row=0,column=0,sticky="w")
cb_font = ttk.Combobox(font_frame, values=sys_font_list, textvariable=font_family_var, state="readonly")
cb_font.grid(row=0,column=1,columnspan=2,sticky="ew",padx=2)
cb_font.bind("<<ComboboxSelected>>", refresh_preview)
ttk.Label(font_frame, text="颜色:").grid(row=1,column=0,sticky="w")
ttk.Entry(font_frame, textvariable=font_color_var, width=12).grid(row=1,column=1,padx=2)
ttk.Button(font_frame, text="选色", command=choose_color).grid(row=1,column=2)
ttk.Label(font_frame, text="透明度:").grid(row=2,column=0,sticky="w")
ttk.Scale(font_frame, from_=0, to=255, variable=font_alpha_var, orient=tk.HORIZONTAL, command=lambda v:refresh_preview()).grid(row=2,column=1,columnspan=2,sticky="ew")
size_frame = ttk.LabelFrame(right_frame, text="批量统一尺寸")
size_frame.pack(fill=tk.X, pady=6)
ttk.Checkbutton(size_frame, text="启用", variable=scale_enable, command=refresh_preview).pack(anchor="w")
sz_fr = ttk.Frame(size_frame)
sz_fr.pack(fill=tk.X,pady=2)
ttk.Label(sz_fr, text="宽").grid(row=0,column=0)
ttk.Entry(sz_fr, textvariable=scale_w, width=6).grid(row=0,column=1,padx=2)
ttk.Label(sz_fr, text="高").grid(row=0,column=2)
ttk.Entry(sz_fr, textvariable=scale_h, width=6).grid(row=0,column=3,padx=2)
content_frame = ttk.LabelFrame(right_frame, text="水印内容")
content_frame.pack(fill=tk.X, pady=6)
def make_line(f,txt,var):
fr = ttk.Frame(f)
fr.pack(fill=tk.X,pady=1)
ttk.Checkbutton(fr,variable=var,command=refresh_preview).pack(side=tk.LEFT)
pre = ttk.Entry(fr,width=7)
pre.insert(0,txt)
pre.pack(side=tk.LEFT,padx=2)
val = ttk.Entry(fr)
val.pack(fill=tk.X,expand=True,padx=2)
return pre, val
entry_time_pre, entry_time_val = make_line(content_frame,"时间:",var_time)
entry_loc_pre, entry_loc_val = make_line(content_frame,"地点:",var_loc)
entry_lon_pre, entry_lon_val = make_line(content_frame,"经度:",var_lon)
entry_lat_pre, entry_lat_val = make_line(content_frame,"纬度:",var_lat)
entry_remark_pre, entry_remark_val = make_line(content_frame,"备注:",var_remark)
ttk.Button(right_frame, text="刷新当前时间", command=refresh_time).pack(fill=tk.X, pady=4)
load_config()
refresh_preview()
root.mainloop()