吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1172|回复: 4
收起左侧

[Windows] 自定义水印批量增加小程序

  [复制链接]
wangbutou 发表于 2026-6-4 09:47
本帖最后由 wangbutou 于 2026-6-4 09:49 编辑

工作需要,写了这个小程序,此软件和使用说明由豆包生成,源码放在下面。
打包好的exe程序 放在: https://wwbdk.lanzouv.com/ixu9b3r39kxg
QQ20260604-094051.jpg
[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()

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
hxx3399 + 1 + 1 谢谢@Thanks!

查看全部评分

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

 楼主| wangbutou 发表于 2026-6-5 08:04
现在AI是真方便啊

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
Ameli + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

caoyuan5637895 发表于 2026-6-6 08:46
Ameli 发表于 2026-7-28 21:32
可以呀,有点厉害啊,大神我有个微信小程序有的很不自在,你会逆向复制一个小程序优化一下吗
redTuna 发表于 2026-7-30 15:38
谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-9-2 19:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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