基于Python实现一个图片水印批量添加工具

 更新时间:2025年10月27日 08:55:13   作者:xcLeigh  
本文详细介绍了使用 Python开发 给图片加水印的工具,该工具基于 Pillow 和 tkinter 库构建,可解决单图处理耗时、专业软件操作复杂的问题,文中提供完整可运行代码,并给出参数校验、字体兼容、常见报错解决等实用内容,需要的朋友可以参考下

平时处理图片时,你是不是总遇到这样的麻烦:想给一批图片加水印,单张处理太费时间,用专业软件又觉得小题大做?今天就带大家亲手做一个实用的Python小工具,既能选单个图片加水印,也能批量处理整个文件夹的图片,小白跟着步骤走也能搞定!

一、工具核心功能与准备工作

先明确下这个工具能帮我们解决什么问题,以及动手前需要准备哪些东西。

1. 核心功能

咱们做的这个工具,主打“灵活”和“高效”,具体能实现这3个功能:

  • 单图处理:选一张图片,手动调整水印位置、透明度,预览效果后再保存
  • 批量处理:选一个文件夹,一键给里面所有图片(支持jpg、png、jpeg格式)加统一水印
  • 自定义设置:自己改水印文字、字体大小、颜色,还能调水印的透明度,避免遮挡图片内容

2. 环境准备

做这个工具不用复杂的环境,只要装2个Python库就行,新手直接按步骤来:

  1. 先确认电脑装了Python(建议3.7及以上版本,没装的话去官网下载,记得勾“Add Python to PATH”)
  2. 打开命令提示符(Windows按Win+R,输cmd;Mac打开终端),输入下面两行命令,安装需要的库:
    • 安装处理图片的库:pip install pillow
    • 安装做图形界面的库(让工具更直观):pip install tkinter
      (注:tkinter在Python3里通常是自带的,要是安装报错,直接跳过这步试试,大概率能正常用)

二、代码拆解与实现(附完整代码)

我把整个工具的代码分成了3个部分,每部分都标了注释,大家可以跟着理解,也能直接复制用。

1. 导入需要的库

先把后面要用到的工具包导进来,就像做饭前把调料准备好一样:

from PIL import Image, ImageDraw, ImageFont  # 处理图片和添加文字水印
import tkinter as tk  # 做图形界面
from tkinter import filedialog, messagebox, ttk  # 界面里的文件选择、提示框等组件
import os  # 处理文件夹和文件路径

2. 核心功能函数(加水印的关键)

这部分是工具的“心脏”,负责实现图片读取、水印添加、保存等核心操作,我拆成了3个函数,每个函数干一件事,逻辑更清楚:

(1)单个图片加水印函数

def add_watermark_to_single_image():
    # 1. 让用户选择要处理的单个图片
    img_path = filedialog.askopenfilename(
        title="选一张要加水印的图片",
        filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
    )
    if not img_path:  # 要是用户没选图片,直接退出函数
        return

    # 2. 获取用户设置的水印参数(文字、大小、颜色、透明度)
    watermark_text = entry_text.get().strip()
    if not watermark_text:  # 没填水印文字的话,弹出提示
        messagebox.showwarning("提示", "请先输入水印文字哦!")
        return

    try:
        font_size = int(entry_font_size.get().strip())
        opacity = int(entry_opacity.get().strip())
        # 简单判断参数是否合理,避免出错
        if font_size <= 0 or opacity < 0 or opacity > 100:
            raise ValueError
    except ValueError:
        messagebox.showerror("错误", "字体大小要填正整数,透明度要在0-100之间哦!")
        return

    # 3. 读取图片,处理PNG透明格式(避免透明图片加水印后背景变黑色)
    img = Image.open(img_path).convert("RGBA")
    # 创建一个和图片一样大的透明图层,用来放水印
    watermark_layer = Image.new("RGBA", img.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(watermark_layer)

    # 4. 加载字体(这里用系统默认字体,Windows和Mac路径不一样,做了兼容)
    try:
        if os.name == "nt":  # Windows系统
            font = ImageFont.truetype("arial.ttf", font_size)
        else:  # Mac或Linux系统
            font = ImageFont.truetype("/Library/Fonts/Arial.ttf", font_size)
    except IOError:
        # 要是没找到Arial字体,就用默认字体,虽然丑点但不影响用
        font = ImageFont.load_default(size=font_size)

    # 5. 计算水印位置(默认放在右下角,距离边缘20像素,也可以自己改这里的数值)
    text_width, text_height = draw.textbbox((0, 0), watermark_text, font=font)[2:]
    img_width, img_height = img.size
    x = img_width - text_width - 20  # 右边距20
    y = img_height - text_height - 20  # 下边距20

    # 6. 添加水印(处理透明度)
    # 把用户输入的0-100透明度,转成PIL需要的0-255范围
    alpha = int(255 * (opacity / 100))
    # 这里默认水印是黑色,想改颜色的话,把(0,0,0,alpha)里的前三个数换成RGB值
    draw.text((x, y), watermark_text, font=font, fill=(0, 0, 0, alpha))

    # 7. 合并图片和水印图层,保存结果
    result = Image.alpha_composite(img, watermark_layer)
    # 处理PNG转JPG的情况(JPG不支持透明,要加白色背景)
    if img_path.lower().endswith(('.jpg', '.jpeg')):
        result = result.convert("RGB")
    
    # 让用户选保存路径
    save_path = filedialog.asksaveasfilename(
        defaultextension=os.path.splitext(img_path)[1],
        filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")],
        title="保存加水印后的图片"
    )
    if save_path:
        result.save(save_path)
        messagebox.showinfo("成功", f"图片已保存到:\n{save_path}")

(2)批量处理文件夹图片函数

批量处理和单图逻辑差不多,主要多了“遍历文件夹”的步骤:

def batch_add_watermark():
    # 1. 让用户选要批量处理的文件夹
    folder_path = filedialog.askdirectory(title="选要批量加水印的文件夹")
    if not folder_path:
        return

    # 2. 获取用户设置的水印参数(和单图函数一样,避免重复写代码)
    watermark_text = entry_text.get().strip()
    if not watermark_text:
        messagebox.showwarning("提示", "请先输入水印文字哦!")
        return

    try:
        font_size = int(entry_font_size.get().strip())
        opacity = int(entry_opacity.get().strip())
        if font_size <= 0 or opacity < 0 or opacity > 100:
            raise ValueError
    except ValueError:
        messagebox.showerror("错误", "字体大小要填正整数,透明度要在0-100之间哦!")
        return

    # 3. 遍历文件夹里的所有文件,只处理图片
    # 先定义支持的图片格式,避免处理非图片文件
    supported_formats = ('.jpg', '.jpeg', '.png')
    # 统计成功处理的图片数量
    success_count = 0

    # 加载字体(和单图函数一样,做了系统兼容)
    try:
        if os.name == "nt":
            font = ImageFont.truetype("arial.ttf", font_size)
        else:
            font = ImageFont.truetype("/Library/Fonts/Arial.ttf", font_size)
    except IOError:
        font = ImageFont.load_default(size=font_size)

    # 4. 逐个处理图片
    for filename in os.listdir(folder_path):
        # 只处理支持格式的文件
        if filename.lower().endswith(supported_formats):
            img_path = os.path.join(folder_path, filename)
            try:
                # 读取图片,和单图处理逻辑一致
                img = Image.open(img_path).convert("RGBA")
                watermark_layer = Image.new("RGBA", img.size, (255, 255, 255, 0))
                draw = ImageDraw.Draw(watermark_layer)

                # 计算水印位置(同样默认右下角)
                text_width, text_height = draw.textbbox((0, 0), watermark_text, font=font)[2:]
                img_width, img_height = img.size
                x = img_width - text_width - 20
                y = img_height - text_height - 20

                # 添加水印
                alpha = int(255 * (opacity / 100))
                draw.text((x, y), watermark_text, font=font, fill=(0, 0, 0, alpha))

                # 合并图层,保存图片
                result = Image.alpha_composite(img, watermark_layer)
                if filename.lower().endswith(('.jpg', '.jpeg')):
                    result = result.convert("RGB")
                
                # 保存路径:在原文件夹下加“_watermarked”后缀
                name, ext = os.path.splitext(filename)
                save_path = os.path.join(folder_path, f"{name}_watermarked{ext}")
                result.save(save_path)
                success_count += 1
            except Exception as e:
                # 遇到错误不崩溃,只是提示哪个文件处理失败
                messagebox.showerror("处理失败", f"文件 {filename} 处理出错:\n{str(e)}")

    # 批量处理结束后,提示结果
    messagebox.showinfo("批量处理完成", f"总共处理了 {success_count} 张图片\n结果保存在原文件夹,文件名带“_watermarked”后缀")

(3)创建图形界面函数

有了核心功能,再做个简单的界面,不用记命令,点鼠标就能操作:

def create_gui():
    # 1. 初始化窗口
    root = tk.Tk()
    root.title("Python图片水印工具")
    root.geometry("500x300")  # 窗口大小,也可以自己改
    root.resizable(True, True)  # 允许拉伸窗口

    # 2. 创建标签和输入框(按网格布局,整齐好看)
    # 水印文字设置
    ttk.Label(root, text="水印文字:").grid(row=0, column=0, padx=10, pady=15, sticky="w")
    global entry_text
    entry_text = ttk.Entry(root, width=40)
    entry_text.grid(row=0, column=1, padx=10, pady=15)
    entry_text.insert(0, "我的图片")  # 默认文字,可修改

    # 字体大小设置
    ttk.Label(root, text="字体大小:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
    global entry_font_size
    entry_font_size = ttk.Entry(root, width=40)
    entry_font_size.grid(row=1, column=1, padx=10, pady=5)
    entry_font_size.insert(0, "20")  # 默认20号字,可修改

    # 透明度设置
    ttk.Label(root, text="水印透明度(0-100):").grid(row=2, column=0, padx=10, pady=5, sticky="w")
    global entry_opacity
    entry_opacity = ttk.Entry(root, width=40)
    entry_opacity.grid(row=2, column=1, padx=10, pady=5)
    entry_opacity.insert(0, "50")  # 默认半透明,可修改

    # 3. 创建功能按钮
    # 单图处理按钮
    btn_single = ttk.Button(root, text="处理单个图片", command=add_watermark_to_single_image)
    btn_single.grid(row=3, column=0, padx=20, pady=20, sticky="ew")

    # 批量处理按钮
    btn_batch = ttk.Button(root, text="批量处理文件夹", command=batch_add_watermark)
    btn_batch.grid(row=3, column=1, padx=20, pady=20, sticky="ew")

    # 4. 运行窗口
    root.mainloop()

3. 主程序入口

最后加一句代码,让脚本运行时自动打开界面:

if __name__ == "__main__":
    create_gui()

三、升级版图片水印工具:支持多参数自定义配置

在前一版工具的基础上,我们新增了文字颜色选择、水印位置自由切换、重复水印次数设置等功能,让水印效果更灵活可控。以下是完整的升级版代码,包含所有参数配置功能。

3.1 功能升级说明

  • 新增文字颜色选择:支持通过颜色选择器自定义水印文字颜色
  • 水印位置可选:提供9个常用位置(如左上角、中心、右下角等)
  • 重复水印次数:可设置水印在图片中重复出现的行数和列数(如3x3网格分布)
  • 保留原功能:兼容单图/批量处理、字体大小/透明度调整

3.2 完整代码实现(含参数配置)

from PIL import Image, ImageDraw, ImageFont
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, colorchooser
import os
import math


class WatermarkTool:
    def __init__(self, root):
        # 初始化主窗口
        self.root = root
        self.root.title("图片水印批量工具")
        self.root.geometry("700x550")
        self.root.resizable(True, True)
        self.root.configure(bg="#f0f0f0")  # 浅灰背景

        # 初始化参数
        self.current_color = (0, 0, 0)  # 默认黑色
        self.fullscreen_mode = False  # 全屏水印模式开关

        # 设置全局样式
        self.setup_styles()

        # 创建界面
        self.create_widgets()

    def setup_styles(self):
        """配置ttk样式,统一美化界面"""
        style = ttk.Style()
        style.theme_use("clam")  # 使用clam主题增强跨平台一致性

        # 标题样式
        style.configure("Title.TLabel", 
                       font=("微软雅黑", 12, "bold"),
                       background="#f0f0f0",
                       foreground="#333333")

        # 标签样式
        style.configure("Label.TLabel",
                       font=("微软雅黑", 10),
                       background="#f0f0f0",
                       foreground="#555555",
                       padding=5)

        # 输入框样式
        style.configure("TEntry",
                       font=("微软雅黑", 10),
                       padding=5,
                       fieldbackground="#ffffff",
                       borderwidth=1,
                       focusthickness=2,
                       focuscolor="#4a90d9")

        # 按钮样式
        style.configure("TButton",
                       font=("微软雅黑", 10, "bold"),
                       padding=8,
                       background="#4a90d9",
                       foreground="#ffffff")
        style.map("TButton",
                 background=[("active", "#357abd"), ("pressed", "#2a6099")])

        # 颜色预览样式(不依赖height,用padding控制大小)
        self.color_style = ttk.Style()
        self.color_style.configure("Color.TLabel", 
                                  background="#000000",  # 默认黑色
                                  borderwidth=1,
                                  relief="solid",
                                  padding=(10, 5))  # 用内边距控制宽高(水平10,垂直5)

        # 分组框标题样式(解决-font错误)
        style.configure("TLabelFrame.Label",
                       font=("微软雅黑", 10, "bold"),
                       foreground="#333333")

    def create_widgets(self):
        """创建界面组件,采用卡片式布局"""
        # 主容器(带边距)
        main_frame = ttk.Frame(self.root, padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 参数配置卡片(白色背景,阴影效果)
        config_card = ttk.LabelFrame(main_frame, text="水印参数配置", padding=15)
        config_card.pack(fill=tk.X, pady=(0, 20))

        # 1. 水印文字
        ttk.Label(config_card, text="水印文字:", style="Label.TLabel").grid(
            row=0, column=0, padx=10, pady=10, sticky="w")
        self.entry_text = ttk.Entry(config_card, width=50)
        self.entry_text.grid(row=0, column=1, columnspan=3, padx=10, pady=10, sticky="ew")
        self.entry_text.insert(0, "我的图片")

        # 2. 字体大小 + 透明度(横向排列)
        ttk.Label(config_card, text="字体大小:", style="Label.TLabel").grid(
            row=1, column=0, padx=10, pady=10, sticky="w")
        self.entry_font_size = ttk.Entry(config_card, width=10)
        self.entry_font_size.grid(row=1, column=1, padx=(10, 30), pady=10, sticky="w")
        self.entry_font_size.insert(0, "20")

        ttk.Label(config_card, text="透明度(0-100):", style="Label.TLabel").grid(
            row=1, column=2, padx=10, pady=10, sticky="w")
        self.entry_opacity = ttk.Entry(config_card, width=10)
        self.entry_opacity.grid(row=1, column=3, padx=10, pady=10, sticky="w")
        self.entry_opacity.insert(0, "50")

        # 3. 文字颜色选择(修复-height错误:用padding控制预览框大小)
        ttk.Label(config_card, text="文字颜色:", style="Label.TLabel").grid(
            row=2, column=0, padx=10, pady=10, sticky="w")
        color_frame = ttk.Frame(config_card)
        color_frame.grid(row=2, column=1, columnspan=3, padx=10, pady=10, sticky="w")
        
        # 移除-height参数,依赖样式中的padding控制大小
        self.color_label = ttk.Label(color_frame, style="Color.TLabel")
        self.color_label.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(color_frame, text="选择颜色", command=self.choose_color).pack(side=tk.LEFT)

        # 4. 水印位置选择
        ttk.Label(config_card, text="水印位置:", style="Label.TLabel").grid(
            row=3, column=0, padx=10, pady=10, sticky="w")
        self.position_var = tk.StringVar(value="右下")
        positions = ["左上", "中上", "右上", "左中", "中心", "右中", "左下", "中下", "右下"]
        self.position_combo = ttk.Combobox(
            config_card, textvariable=self.position_var, values=positions, width=10, state="readonly"
        )
        self.position_combo.grid(row=3, column=1, padx=10, pady=10, sticky="w")

        # 5. 重复分布(行数+列数)
        ttk.Label(config_card, text="重复分布:", style="Label.TLabel").grid(
            row=4, column=0, padx=10, pady=10, sticky="w")
        
        ttk.Label(config_card, text="行数:", style="Label.TLabel").grid(
            row=4, column=1, padx=(10, 5), pady=10, sticky="e")
        self.entry_rows = ttk.Entry(config_card, width=5)
        self.entry_rows.grid(row=4, column=2, padx=(0, 20), pady=10, sticky="w")
        self.entry_rows.insert(0, "1")

        ttk.Label(config_card, text="列数:", style="Label.TLabel").grid(
            row=4, column=2, padx=(20, 5), pady=10, sticky="e")
        self.entry_cols = ttk.Entry(config_card, width=5)
        self.entry_cols.grid(row=4, column=3, padx=10, pady=10, sticky="w")
        self.entry_cols.insert(0, "1")

        # 6. 全屏水印模式(新增功能)
        fullscreen_frame = ttk.Frame(config_card)
        fullscreen_frame.grid(row=5, column=0, columnspan=4, padx=10, pady=10, sticky="w")
        
        self.fullscreen_check = ttk.Checkbutton(
            fullscreen_frame, text="全屏水印模式(忽略位置和行列设置)",
            command=self.toggle_fullscreen_mode
        )
        self.fullscreen_check.pack(side=tk.LEFT)

        # 7. 全屏水印参数(仅在全屏模式下启用)
        self.fullscreen_params_frame = ttk.Frame(config_card)
        self.fullscreen_params_frame.grid(row=6, column=0, columnspan=4, padx=10, pady=5, sticky="w")
        self.disable_frame(self.fullscreen_params_frame)  # 默认禁用(自定义禁用方法)

        ttk.Label(self.fullscreen_params_frame, text="水印旋转角度:", style="Label.TLabel").pack(side=tk.LEFT, padx=5)
        self.entry_rotation = ttk.Entry(self.fullscreen_params_frame, width=5)
        self.entry_rotation.pack(side=tk.LEFT, padx=5)
        self.entry_rotation.insert(0, "30")  # 默认旋转30度

        ttk.Label(self.fullscreen_params_frame, text="水平间距(像素):", style="Label.TLabel").pack(side=tk.LEFT, padx=5)
        self.entry_h_spacing = ttk.Entry(self.fullscreen_params_frame, width=5)
        self.entry_h_spacing.pack(side=tk.LEFT, padx=5)
        self.entry_h_spacing.insert(0, "100")

        ttk.Label(self.fullscreen_params_frame, text="垂直间距(像素):", style="Label.TLabel").pack(side=tk.LEFT, padx=5)
        self.entry_v_spacing = ttk.Entry(self.fullscreen_params_frame, width=5)
        self.entry_v_spacing.pack(side=tk.LEFT, padx=5)
        self.entry_v_spacing.insert(0, "80")

        # 按钮区域(底部居中)
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=10)

        ttk.Button(
            btn_frame, text="处理单个图片", command=self.process_single_image, width=20
        ).pack(side=tk.LEFT, padx=(0, 30), pady=10, anchor=tk.CENTER)

        ttk.Button(
            btn_frame, text="批量处理文件夹", command=self.batch_process_images, width=20
        ).pack(side=tk.LEFT, padx=30, pady=10, anchor=tk.CENTER)

        # 状态标签(底部显示提示信息)
        self.status_label = ttk.Label(
            main_frame, text="请配置参数后选择图片处理", style="Label.TLabel", anchor=tk.CENTER
        )
        self.status_label.pack(fill=tk.X, pady=10)

        # 让列自适应宽度
        config_card.columnconfigure(1, weight=1)
        config_card.columnconfigure(3, weight=1)

    def disable_frame(self, frame):
        """自定义禁用框架内所有组件的方法(替代state=disabled)"""
        for widget in frame.winfo_children():
            widget.config(state="disabled")

    def enable_frame(self, frame):
        """自定义启用框架内所有组件的方法"""
        for widget in frame.winfo_children():
            widget.config(state="normal")

    def toggle_fullscreen_mode(self):
        """切换全屏水印模式,启用/禁用相关参数"""
        self.fullscreen_mode = not self.fullscreen_mode
        if self.fullscreen_mode:
            self.enable_frame(self.fullscreen_params_frame)
            self.update_status("已启用全屏水印模式")
        else:
            self.disable_frame(self.fullscreen_params_frame)
            self.update_status("已禁用全屏水印模式")

    def choose_color(self):
        """选择水印颜色并更新预览"""
        color = colorchooser.askcolor(title="选择水印颜色")[0]
        if color:
            self.current_color = (int(color[0]), int(color[1]), int(color[2]))
            hex_color = f"#{int(color[0]):02x}{int(color[1]):02x}{int(color[2]):02x}"
            self.color_style.configure("Color.TLabel", background=hex_color)
            self.update_status(f"已选择颜色:{hex_color}")

    def get_watermark_params(self):
        """获取并校验水印参数"""
        watermark_text = self.entry_text.get().strip()
        if not watermark_text:
            messagebox.showwarning("提示", "请输入水印文字")
            return None

        try:
            font_size = int(self.entry_font_size.get().strip())
            opacity = int(self.entry_opacity.get().strip())
            rows = int(self.entry_rows.get().strip())
            cols = int(self.entry_cols.get().strip())
            
            if not (font_size > 0 and 0 <= opacity <= 100 and rows > 0 and cols > 0):
                raise ValueError
        except ValueError:
            messagebox.showerror("参数错误", "请检查参数:\n- 字体大小:正整数\n- 透明度:0-100\n- 行数/列数:正整数")
            return None

        # 全屏模式参数校验
        fullscreen_params = None
        if self.fullscreen_mode:
            try:
                rotation = int(self.entry_rotation.get().strip())
                h_spacing = int(self.entry_h_spacing.get().strip())
                v_spacing = int(self.entry_v_spacing.get().strip())
                if not (h_spacing > 0 and v_spacing > 0):
                    raise ValueError
                fullscreen_params = (rotation, h_spacing, v_spacing)
            except ValueError:
                messagebox.showerror("参数错误", "全屏模式参数需为正整数")
                return None

        return (
            watermark_text, font_size, opacity,
            self.current_color, self.position_var.get(),
            rows, cols, fullscreen_params
        )

    def load_font(self, size):
        """加载字体(兼容中文)"""
        try:
            if os.name == "nt":  # Windows系统
                return ImageFont.truetype("simsun.ttc", size)  # 宋体支持中文
            else:  # Mac/Linux
                return ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", size)  # 苹方字体
        except IOError:
            self.update_status("未找到指定字体,使用默认字体")
            return ImageFont.load_default(size=size)

    def calculate_position(self, position, img_w, img_h, text_w, text_h):
        """计算水印位置坐标"""
        margin = 20  # 边缘距离
        pos_map = {
            "左上": (margin, margin),
            "中上": ((img_w - text_w) // 2, margin),
            "右上": (img_w - text_w - margin, margin),
            "左中": (margin, (img_h - text_h) // 2),
            "中心": ((img_w - text_w) // 2, (img_h - text_h) // 2),
            "右中": (img_w - text_w - margin, (img_h - text_h) // 2),
            "左下": (margin, img_h - text_h - margin),
            "中下": ((img_w - text_w) // 2, img_h - text_h - margin),
            "右下": (img_w - text_w - margin, img_h - text_h - margin)
        }
        return pos_map.get(position, (margin, margin))  # 默认左上

    def draw_fullscreen_watermark(self, draw, watermark_text, font, img_width, img_height, fill_color):
        """绘制全屏水印(优化旋转逻辑)"""
        # 获取全屏模式参数
        params = self.get_watermark_params()
        if not params:
            return
        fullscreen_params = params[-1]
        rotation, h_spacing, v_spacing = fullscreen_params
        
        # 计算文字基础尺寸
        text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
        text_width = text_bbox[2] - text_bbox[0]
        text_height = text_bbox[3] - text_bbox[1]
        
        # 计算网格覆盖范围(避免边缘空白)
        grid_width = img_width + text_width * 2
        grid_height = img_height + text_height * 2
        
        # 遍历网格绘制旋转水印
        for x in range(-text_width, grid_width, h_spacing):
            for y in range(-text_height, grid_height, v_spacing):
                # 创建临时图层(尺寸足够容纳旋转后的文字)
                temp_size = max(text_width, text_height) * 2
                temp_layer = Image.new("RGBA", (temp_size, temp_size), (255, 255, 255, 0))
                temp_draw = ImageDraw.Draw(temp_layer)
                
                # 在临时图层中心绘制文字
                temp_draw.text(
                    (temp_size//2 - text_width//2, temp_size//2 - text_height//2),
                    watermark_text,
                    font=font,
                    fill=fill_color
                )
                
                # 旋转临时图层(expand=True确保完整显示)
                rotated_layer = temp_layer.rotate(rotation, expand=True)
                rotated_w, rotated_h = rotated_layer.size
                
                # 计算最终绘制位置(文字中心对齐网格点)
                draw_x = x - rotated_w // 2
                draw_y = y - rotated_h // 2
                
                # 绘制旋转后的水印(用paste替代bitmap,避免透明度问题)
                draw._image.paste(rotated_layer, (draw_x, draw_y), rotated_layer)

    def process_single_image(self):
        """处理单个图片"""
        img_path = filedialog.askopenfilename(
            title="选择需要加水印的图片",
            filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
        )
        if not img_path:
            self.update_status("已取消选择图片")
            return

        # 获取参数
        params = self.get_watermark_params()
        if not params:
            return
        watermark_text, font_size, opacity, color, position, rows, cols, fullscreen_params = params

        try:
            self.update_status(f"正在处理图片:{os.path.basename(img_path)}")
            
            # 处理图片
            img = Image.open(img_path).convert("RGBA")
            watermark_layer = Image.new("RGBA", img.size, (255, 255, 255, 0))
            draw = ImageDraw.Draw(watermark_layer)
            font = self.load_font(font_size)
            alpha = int(255 * (opacity / 100))
            fill_color = (*color, alpha)

            # 全屏水印模式
            if self.fullscreen_mode and fullscreen_params:
                self.draw_fullscreen_watermark(draw, watermark_text, font, img.width, img.height, fill_color)
            else:
                # 普通模式:计算文字尺寸和分布
                text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
                text_width = text_bbox[2] - text_bbox[0]
                text_height = text_bbox[3] - text_bbox[1]
                x_step = math.floor(img.width / (cols - 1)) if cols > 1 else 0
                y_step = math.floor(img.height / (rows - 1)) if rows > 1 else 0

                # 绘制水印
                for i in range(rows):
                    for j in range(cols):
                        base_x, base_y = self.calculate_position(
                            position, img.width, img.height, text_width, text_height
                        )
                        x = base_x + (j * x_step) if cols > 1 else base_x
                        y = base_y + (i * y_step) if rows > 1 else base_y
                        x = max(0, min(x, img.width - text_width))
                        y = max(0, min(y, img.height - text_height))
                        draw.text((x, y), watermark_text, font=font, fill=fill_color)

            # 合并保存
            result = Image.alpha_composite(img, watermark_layer)
            if img_path.lower().endswith(('.jpg', '.jpeg')):
                result = result.convert("RGB")

            save_path = filedialog.asksaveasfilename(
                defaultextension=os.path.splitext(img_path)[1],
                filetypes=[("图片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")],
                title="保存加水印后的图片"
            )
            if save_path:
                result.save(save_path)
                messagebox.showinfo("成功", f"图片已保存到:\n{save_path}")
                self.update_status(f"处理完成:{os.path.basename(save_path)}")

        except Exception as e:
            messagebox.showerror("处理失败", f"错误信息:\n{str(e)}")
            self.update_status("处理失败,请检查图片是否正常")

    def batch_process_images(self):
        """批量处理文件夹图片"""
        folder_path = filedialog.askdirectory(title="选择批量处理的文件夹")
        if not folder_path:
            self.update_status("已取消选择文件夹")
            return

        params = self.get_watermark_params()
        if not params:
            return
        watermark_text, font_size, opacity, color, position, rows, cols, fullscreen_params = params

        supported_formats = ('.jpg', '.jpeg', '.png')
        success_count = 0
        font = self.load_font(font_size)
        alpha = int(255 * (opacity / 100))
        fill_color = (*color, alpha)

        self.update_status(f"开始批量处理:{os.path.basename(folder_path)}")

        for filename in os.listdir(folder_path):
            if filename.lower().endswith(supported_formats):
                img_path = os.path.join(folder_path, filename)
                try:
                    img = Image.open(img_path).convert("RGBA")
                    watermark_layer = Image.new("RGBA", img.size, (255, 255, 255, 0))
                    draw = ImageDraw.Draw(watermark_layer)

                    # 全屏水印模式
                    if self.fullscreen_mode and fullscreen_params:
                        self.draw_fullscreen_watermark(draw, watermark_text, font, img.width, img.height, fill_color)
                    else:
                        # 普通模式绘制
                        text_bbox = draw.textbbox((0, 0), watermark_text, font=font)
                        text_width = text_bbox[2] - text_bbox[0]
                        text_height = text_bbox[3] - text_bbox[1]
                        x_step = math.floor(img.width / (cols - 1)) if cols > 1 else 0
                        y_step = math.floor(img.height / (rows - 1)) if rows > 1 else 0

                        for i in range(rows):
                            for j in range(cols):
                                base_x, base_y = self.calculate_position(
                                    position, img.width, img.height, text_width, text_height
                                )
                                x = base_x + (j * x_step) if cols > 1 else base_x
                                y = base_y + (i * y_step) if rows > 1 else base_y
                                x = max(0, min(x, img.width - text_width))
                                y = max(0, min(y, img.height - text_height))
                                draw.text((x, y), watermark_text, font=font, fill=fill_color)

                    # 保存处理结果
                    result = Image.alpha_composite(img, watermark_layer)
                    if filename.lower().endswith(('.jpg', '.jpeg')):
                        result = result.convert("RGB")

                    name, ext = os.path.splitext(filename)
                    save_path = os.path.join(folder_path, f"{name}_watermarked{ext}")
                    result.save(save_path)
                    success_count += 1
                    self.update_status(f"已处理:{filename}({success_count}张)")

                except Exception as e:
                    messagebox.showerror("单个文件失败", f"文件 {filename} 处理出错:\n{str(e)}")

        messagebox.showinfo("批量完成", f"批量处理结束!\n共成功处理 {success_count} 张图片")
        self.update_status(f"批量处理完成,成功 {success_count} 张")

    def update_status(self, text):
        """更新底部状态提示"""
        self.status_label.config(text=text)
        self.root.update_idletasks()  # 立即刷新界面


if __name__ == "__main__":
    root = tk.Tk()
    app = WatermarkTool(root)
    root.mainloop()

3.3 参数配置说明

3.3.1. 基础参数

  • 水印文字:输入需要添加的水印内容(支持中文)
  • 字体大小:设置文字大小(建议10-50之间,根据图片尺寸调整)
  • 透明度:0-100的数值(0为完全透明,100为完全不透明)

3.3.2. 高级参数

  • 文字颜色:点击"选择颜色"打开调色板,可自定义任意颜色(默认黑色)
  • 水印位置:下拉选择9个常用位置(如"中心"会将水印放在图片正中间)
  • 重复分布:通过"行数"和"列数"设置水印重复次数:
    • 1x1:只显示一个水印(默认)
    • 2x2:显示4个水印(2行2列均匀分布)
    • 3x3:显示9个水印(适合大图片全屏水印)

3.4 使用示例和效果演示

  1. 制作版权水印:文字"© 2025 xcLeigh工作室",白色半透明(透明度30),右下角1x1分布
  2. 制作全屏水印:文字"内部资料",灰色(RGB 128,128,128),透明度20,3x3中心分布

批量生成效果

生成水印前的图片

生成水印后的图片

四、工具使用步骤(小白友好版)

代码写好后,怎么用呢?跟着这4步来,保证能成功:

1. 保存代码

把上面所有代码复制下来,粘贴到记事本里,然后点“文件-另存为”,注意2个细节:

  • 文件名:结尾必须是.py,比如watermark_tool.py
  • 保存类型:选“所有文件”,编码选“UTF-8”(避免中文乱码)

2. 运行工具

找到保存好的watermark_tool.py文件,双击它就能打开工具(前提是已经装了Python)。打开后会看到这样的界面:

  • 上面三个输入框,分别填“水印文字”“字体大小”“透明度”
  • 下面两个按钮,选“处理单个图片”或“批量处理文件夹”

3. 单图处理操作

  1. 在输入框里填好参数(比如水印文字填“我的旅行照”,字体20,透明度50)
  2. 点“处理单个图片”,在弹出的窗口里选要加水印的图片(比如桌面上的photo.jpg
  3. 选好后,会弹出“保存”窗口,选个保存位置(比如还是桌面),点“保存”
  4. 提示“成功”后,去保存位置找图片,就能看到右下角多了水印

4. 批量处理操作

  1. 同样先填好参数(批量处理时,所有图片会用同一个参数)
  2. 点“批量处理文件夹”,选要处理的文件夹(比如“D盘-图片集”)
  3. 工具会自动处理文件夹里所有jpg、png图片,不用手动等
  4. 处理完会提示“完成”,去原文件夹看,每张图片都会多一个带“_watermarked”后缀的副本,比如photo_watermarked.jpg

五、常见问题解决(避坑指南)

用的时候可能会遇到小问题,这里整理了3个常见情况,教你怎么解决:

1. 双击脚本没反应/报错“no module named xxx”

原因:没装需要的库,或者Python没添加到环境变量。
解决办法:

  • 按前面“准备工作”的步骤,重新打开cmd/终端,输入pip install pillowpip install tkinter
  • 要是还没反应,右键点击脚本,选“打开方式”,手动选Python.exe(通常在C:\Users\你的用户名\AppData\Local\Programs\Python\PythonXX文件夹里)

2. 处理PNG图片后,背景变黑色

原因:PNG图片有透明背景,保存成JPG时不支持透明,默认会填黑色。
解决办法:

  • 处理PNG图片时,保存的时候选“保存类型”为PNG,不要选JPG
  • 要是必须存JPG,可以在代码里改“填充颜色”:把fill=(0,0,0,alpha)改成fill=(255,255,255,alpha),水印会变成白色,背景也会变成白色

3. 水印文字显示乱码/方块

原因:系统里没有Arial字体,导致字体加载失败。
解决办法:

  • 手动换个系统有的字体,比如Windows里的“微软雅黑”,把代码里加载字体的行改成:
    font = ImageFont.truetype("msyh.ttc", font_size)
  • 或者直接用默认字体,虽然不好看,但不会乱码,代码里保留font = ImageFont.load_default(size=font_size)就行

六、功能扩展建议(进阶玩法)

要是你觉得基础功能不够用,还能给工具加这些小功能,难度也不大:

  1. 加水印位置选择:现在默认右下角,可以加个下拉框,让用户选“左上角”“中间”“左下角”等位置
  2. 加图片水印:不光能加文字,还能加图片水印(比如自己的logo),用Image.open("logo.png")读取logo,再贴到原图上
  3. 批量修改保存路径:现在批量处理只能存在原文件夹,可加个“选择输出文件夹”按钮,把结果统一存到新文件夹里
  4. 预览功能:加水印前先显示预览图,用户确认没问题再保存,避免反复修改

七、总结

这个Python水印工具虽然简单,但特别实用,不管是处理日常照片,还是工作中给素材加水印,都能省不少时间。关键是代码全在上面,自己能改,想加什么功能就加什么。

新手不用怕,跟着步骤一步步来,先跑通基础版本,再慢慢改细节,既能学会Python实用技能,又能做出自己能用的工具,这不比单纯看教程有意思多了?

以上就是基于Python实现一个图片水印批量添加工具的详细内容,更多关于Python图片水印批量添加的资料请关注脚本之家其它相关文章!

相关文章

  • python爬虫 2019中国好声音评论爬取过程解析

    python爬虫 2019中国好声音评论爬取过程解析

    这篇文章主要介绍了python爬虫 2019中国好声音评论爬取过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • python判断字符串是否是回文的方法小结

    python判断字符串是否是回文的方法小结

    回文,英文叫 palindrome,意思是一个字符串,从前往后读 和 从后往前读 是一模一样的,本文给大家介绍了python判断字符串是否是回文的方法小结,需要的朋友可以参考下
    2025-08-08
  • 使用keras2.0 将Merge层改为函数式

    使用keras2.0 将Merge层改为函数式

    这篇文章主要介绍了使用keras2.0 将Merge层改为函数式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-05-05
  • pandas.DataFrame.from_dict直接从字典构建DataFrame的方法

    pandas.DataFrame.from_dict直接从字典构建DataFrame的方法

    本文主要介绍了pandas.DataFrame.from_dict直接从字典构建DataFrame的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • python 实现定时任务的四种方式

    python 实现定时任务的四种方式

    这篇文章主要介绍了python 实现定时任务的四种方式,帮助大家更好的理解和学习使用python,感兴趣的朋友可以了解下
    2021-04-04
  • Python+smtplib库实现邮件发送功能

    Python+smtplib库实现邮件发送功能

    这篇文章主要为大家详细介绍了Python如何通过smtplib库实现简单的邮件发送功能,文中的示例代码借鉴一下,有需要的小伙伴可以参考一下
    2025-02-02
  • Python Django Vue 项目创建过程详解

    Python Django Vue 项目创建过程详解

    这篇文章主要介绍了Python Django Vue 项目创建过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-07-07
  • Python datetime模块高效处理日期和时间的方法

    Python datetime模块高效处理日期和时间的方法

    Python内置的datetime模块提供了强大的工具来处理这些需求,本文将全面介绍datetime模块的使用方法,从基础操作到高级技巧,帮助你掌握Python中的日期时间处理,感兴趣的朋友一起看看吧
    2025-05-05
  • Python的列表推导式你了解吗

    Python的列表推导式你了解吗

    这篇文章主要为大家详细介绍了Python的列表推导式,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • pandas 两列时间相减换算为秒的方法

    pandas 两列时间相减换算为秒的方法

    下面小编就为大家分享一篇pandas 两列时间相减换算为秒的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-04-04

最新评论