‌Python中使用PIL(Pillow)库给图片添加文字水印(第4节)


在很多场合中,我们可能需要给图片添加水印,比如为了保护图片版权,或者是在社交媒体上标记自己的原创作品。水印通常是指具有透明度的灰色文字或者包含其它颜色的文字。在Python中给图片添加文字水印,通常可以通过PIL(Pillow)库来实现。本节教程将介绍如何使用Python的PIL(Pillow)库和Tkinter库创建一个应用程序,该程序能够在图片的指定位置添加文字水印

文字水印程序效果图如下所示:

给图片添加文字水印

(1)准备工作

文字水印应用程序主要使用Python的标准GUI库Tkinter,并通过Python的第三方库PIL库处理图像。首先,确保要安装PIL第三方库,如果还没有安装,可以通过pip进行安装:

pip install Pillow

(2)导入所需的库

import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk, ImageDraw, ImageFont
import time
from tkinter.colorchooser import askcolor

(3)文字水印制作核心代码

水印制作的函数如下所示:

# 文字水印制作函数
def add_watermark(open_image, text, color, font, output_folder_path, position):
    try:
        # 获取原始图片文件名(包含扩展名)
        open_image_name = os.path.basename(open_image)
        file_name, file_extension = os.path.splitext(open_image_name)

        # 水印完成后,创建输出文件名,在原始文件名后添加_时间戳后缀
        # 获取当前时间的时间元组
        current_time = time.localtime()
        # 将时间元组格式化为字符串,例如,将时间“2025年08月23日09时22分30秒”格式化为“20250823092230”
        time_str = time.strftime("%Y%m%d%H%M%S", current_time)
        output_image_name = file_name + "_" + time_str + file_extension

        # 构建完整的输出文件路径
        output_path = os.path.join(output_folder_path, output_image_name)

        # 打开原始图片
        image = Image.open(open_image)

        # 将图片转换为RGBA模式(一种包含红色、绿色、蓝色和透明度四个通道的颜色模式)
        watermark_image = image.convert('RGBA')

        # 获取水印字体和大小
        font = ImageFont.truetype(font_var.get(), current_font_size.get())

        # 获取文本水印内容
        text = default_value.get()

        # 创建空白图像
        blank_image_dims = (round(4 * watermark_image.width),
                            round(4 * watermark_image.height))
        text_image = Image.new('RGBA', blank_image_dims, (255, 255, 255, 0))
        my_watermark = ImageDraw.Draw(text_image)

        # 根据颜色选择和不透明度值创建RGBA元组
        opacity_tuple = ((round(current_opacity.get() * 2.55)),)
        color = text_color + opacity_tuple

        # 在空白图像上设置文本水印
        center_of_image = (round(text_image.width / 2), round(text_image.height / 2))
        my_watermark.text(center_of_image,
                        text,
                        font=font,
                        fill=color,
                        anchor="mm",)

        # 将文本图像修剪为仅围绕文本中心旋转的图像
        text_box_size = text_image.getbbox()
        text_image = text_image.crop(text_box_size)
        text_image = text_image.rotate(current_tilt.get(),
                                    center=(text_image.width / 2, text_image.height / 2),
                                    expand=True)
        # 创建新图像以粘贴文本图像
        new_image = Image.new('RGBA', watermark_image.size, (255, 255, 255, 0))
        position = ((int(current_width.get() / 2 - text_image.width / 2),
                        int(current_height.get() / 2 - text_image.height / 2)))
        Image.Image.paste(new_image, text_image, position)

        # 将原始图像与文本图像组合并保存图像
        combined = Image.alpha_composite(watermark_image, new_image).convert('RGB')
        # 保存结果图片,压缩质量为95%最高质量‌
        combined.save(output_path, quality=95)
        messagebox.showinfo("完成", "水印已成功添加!")
    except Exception as e:
        messagebox.showerror("错误", "发生错误: " + str(e))

(4)文字水印预览区域

文字水印预览区域的函数如下所示:

# 文字水印预览函数
def apply_watermark_preview():
    open_image = open_image_entry.get()
    watermark_path = watermark_input.get()
    opacity = current_opacity.get()
    opacity = int(opacity)
    if not open_image or not watermark_path:
        return

    try:
        # 打开原始图片
        image = Image.open(open_image)

        # 将图片转换为RGBA模式(一种包含红色、绿色、蓝色和透明度四个通道的颜色模式)
        watermark_image = image.convert('RGBA')

        # 获取水印字体和大小
        font = ImageFont.truetype(font_var.get(), current_font_size.get())

        # 获取文本水印内容
        text = default_value.get()

         # 创建空白图像
        blank_image_dims = (round(4 * watermark_image.width),
                            round(4 * watermark_image.height))
        text_image = Image.new('RGBA', blank_image_dims, (255, 255, 255, 0))
        my_watermark = ImageDraw.Draw(text_image)

        # 根据颜色选择和不透明度值创建RGBA元组
        opacity_tuple = ((round(current_opacity.get() * 2.55)),)
        color = text_color + opacity_tuple

        # 在空白图像上设置文本水印
        center_of_image = (round(text_image.width / 2), round(text_image.height / 2))
        my_watermark.text(center_of_image,
                        text,
                        font=font,
                        fill=color,
                        anchor="mm",)

        # 将文本图像修剪为仅围绕文本中心旋转的图像
        text_box_size = text_image.getbbox()
        text_image = text_image.crop(text_box_size)
        text_image = text_image.rotate(current_tilt.get(),
                                    center=(text_image.width / 2, text_image.height / 2),
                                    expand=True)

        # 创建新图像以粘贴文本图像
        new_image = Image.new('RGBA', watermark_image.size, (255, 255, 255, 0))
        position = ((int(current_width.get()  / 2 - text_image.width / 2),
                        int(current_height.get() / 2 - text_image.height / 2)))
        Image.Image.paste(new_image, text_image, position)

        # 将原始图像与文本图像组合并保存图像
        combined = Image.alpha_composite(watermark_image, new_image).convert('RGB')

        # 复制图片,创建带有水印的预览图像
        preview_image = combined.copy()

        # 设置固定的预览框尺寸
        target_width = 400
        target_height = 400

        # 获取原始图片宽高比
        width_ratio =  preview_image.width / target_width
        height_ratio =  preview_image.height / target_height

        # 根据宽高比计算缩放后的尺寸,保持图片比例不变
        if width_ratio > height_ratio:
            new_width = target_width
            new_height = int( preview_image.height / width_ratio)
        else:
            new_height = target_height
            new_width = int( preview_image.width / height_ratio)

        # 缩放图片
        preview_image =  preview_image.resize((new_width, new_height), Image.Resampling.LANCZOS)

        # 计算在预览框中居中显示的偏移量
        offset_x = (target_width - new_width) // 2
        offset_y = (target_height - new_height) // 2

        # 创建一个新的白色背景的图片(尺寸为预览框大小)
        final_preview_image = Image.new('RGB', (target_width, target_height), (255, 255, 255))
        final_preview_image.paste(preview_image, (offset_x, offset_y))

        # 转换为Tkinter可以显示的格式
        preview_photo = ImageTk.PhotoImage(final_preview_image)
        preview_canvas.create_image(0, 0, anchor='nw', image=preview_photo)
        preview_canvas.image = preview_photo

    except Exception as e:
        messagebox.showerror("预览错误", "无法显示水印预览: " + str(e))

给图片添加文字水印的完整代码如下所示:

动手练一练:

import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk, ImageDraw, ImageFont
import time
from tkinter.colorchooser import askcolor

# 文字水印颜色选择器
def change_text_color():
    colors = askcolor(title="水印颜色选择器")
    global text_color
    text_color = colors[0]      # colors[0]是RGB值
    color_selection.configure(bg=colors[1])     # colors[1]是十六进制值

# 显示RGB值颜色
def rgb_to_hex(rgb):
    return "#%02x%02x%02x" % rgb

# 移动“宽度”滑块时触发的函数
def width_slider_moved(event):
    width_label.configure(text=f"宽度: {current_width.get() - 1/2 * img_width}")

# 移动“高度”滑块时触发的函数
def height_slider_moved(event):
    height_label.configure(text=f"高度: {-1 * (current_height.get()  - 1/2 * img_height)}")

# 文字水印制作函数
def add_watermark(open_image, text, color, font, output_folder_path, position):
    try:
        # 获取原始图片文件名(包含扩展名)
        open_image_name = os.path.basename(open_image)
        file_name, file_extension = os.path.splitext(open_image_name)

        # 水印完成后,创建输出文件名,在原始文件名后添加_时间戳后缀
        # 获取当前时间的时间元组
        current_time = time.localtime()
        # 将时间元组格式化为字符串,例如,将时间“2025年08月23日09时22分30秒”格式化为“20250823092230”
        time_str = time.strftime("%Y%m%d%H%M%S", current_time)
        output_image_name = file_name + "_" + time_str + file_extension

        # 构建完整的输出文件路径
        output_path = os.path.join(output_folder_path, output_image_name)

        # 打开原始图片
        image = Image.open(open_image)

        # 将图片转换为RGBA模式(一种包含红色、绿色、蓝色和透明度四个通道的颜色模式)
        watermark_image = image.convert('RGBA')

        # 获取水印字体和大小
        font = ImageFont.truetype(font_var.get(), current_font_size.get())

        # 获取文本水印内容
        text = default_value.get()

        # 创建空白图像
        blank_image_dims = (round(4 * watermark_image.width),
                            round(4 * watermark_image.height))
        text_image = Image.new('RGBA', blank_image_dims, (255, 255, 255, 0))
        my_watermark = ImageDraw.Draw(text_image)

        # 根据颜色选择和不透明度值创建RGBA元组
        opacity_tuple = ((round(current_opacity.get() * 2.55)),)
        color = text_color + opacity_tuple

        # 在空白图像上设置文本水印
        center_of_image = (round(text_image.width / 2), round(text_image.height / 2))
        my_watermark.text(center_of_image,
                        text,
                        font=font,
                        fill=color,
                        anchor="mm",)

        # 将文本图像修剪为仅围绕文本中心旋转的图像
        text_box_size = text_image.getbbox()
        text_image = text_image.crop(text_box_size)
        text_image = text_image.rotate(current_tilt.get(),
                                    center=(text_image.width / 2, text_image.height / 2),
                                    expand=True)
        # 创建新图像以粘贴文本图像
        new_image = Image.new('RGBA', watermark_image.size, (255, 255, 255, 0))
        position = ((int(current_width.get() / 2 - text_image.width / 2),
                        int(current_height.get() / 2 - text_image.height / 2)))
        Image.Image.paste(new_image, text_image, position)

        # 将原始图像与文本图像组合并保存图像
        combined = Image.alpha_composite(watermark_image, new_image).convert('RGB')
        # 保存结果图片,压缩质量为95%最高质量‌
        combined.save(output_path, quality=95)
        messagebox.showinfo("完成", "水印已成功添加!")
    except Exception as e:
        messagebox.showerror("错误", "发生错误: " + str(e))

# 打开一个文件选择对话框,允许用户选择一个图片文件,并返回该文件的路径
def browse_open_image():
    file_selected = filedialog.askopenfilename()
    if file_selected:
        open_image_entry.delete(0, tk.END)
        open_image_entry.insert(0, file_selected)
        apply_watermark_preview()

# 打开一个文件选择对话框,让用户选择一个目录
def browse_output_image():
    folder_selected = filedialog.askdirectory()
    if folder_selected:
        output_entry.delete(0, tk.END)
        output_entry.insert(0, folder_selected)

# 添加水印进程
def add_watermark_process():
    open_image = open_image_entry.get()
    text = watermark_input.get()
    output_folder_path = output_entry.get()
    position = ((int(current_width.get() / 2),
                      int(current_height.get() / 2)))
    # 根据颜色选择和不透明度值创建RGBA元组
    opacity = ((round(current_opacity.get() * 2.55)),)
    color = text_color + opacity
    font = ImageFont.truetype(font_var.get(), current_font_size.get())
    # 判断是否为空
    if not open_image:
        messagebox.showwarning("警告", "“待加水印图片”不能为空!")
        return
    elif not text:
        messagebox.showwarning("警告", "“水印文本”不能为空!")
        return
    elif not output_folder_path:
        messagebox.showwarning("警告", "“输出文件夹”不能为空!")
        return
    add_watermark(open_image, text, color, font, output_folder_path, position)

# 文字水印预览函数
def apply_watermark_preview():
    open_image = open_image_entry.get()
    watermark_path = watermark_input.get()
    opacity = current_opacity.get()
    opacity = int(opacity)
    if not open_image or not watermark_path:
        return

    try:
        # 打开原始图片
        image = Image.open(open_image)

        # 将图片转换为RGBA模式(一种包含红色、绿色、蓝色和透明度四个通道的颜色模式)
        watermark_image = image.convert('RGBA')

        # 获取水印字体和大小
        font = ImageFont.truetype(font_var.get(), current_font_size.get())

        # 获取文本水印内容
        text = default_value.get()

         # 创建空白图像
        blank_image_dims = (round(4 * watermark_image.width),
                            round(4 * watermark_image.height))
        text_image = Image.new('RGBA', blank_image_dims, (255, 255, 255, 0))
        my_watermark = ImageDraw.Draw(text_image)

        # 根据颜色选择和不透明度值创建RGBA元组
        opacity_tuple = ((round(current_opacity.get() * 2.55)),)
        color = text_color + opacity_tuple

        # 在空白图像上设置文本水印
        center_of_image = (round(text_image.width / 2), round(text_image.height / 2))
        my_watermark.text(center_of_image,
                        text,
                        font=font,
                        fill=color,
                        anchor="mm",)

        # 将文本图像修剪为仅围绕文本中心旋转的图像
        text_box_size = text_image.getbbox()
        text_image = text_image.crop(text_box_size)
        text_image = text_image.rotate(current_tilt.get(),
                                    center=(text_image.width / 2, text_image.height / 2),
                                    expand=True)

        # 创建新图像以粘贴文本图像
        new_image = Image.new('RGBA', watermark_image.size, (255, 255, 255, 0))
        position = ((int(current_width.get()  / 2 - text_image.width / 2),
                        int(current_height.get() / 2 - text_image.height / 2)))
        Image.Image.paste(new_image, text_image, position)

        # 将原始图像与文本图像组合并保存图像
        combined = Image.alpha_composite(watermark_image, new_image).convert('RGB')

        # 复制图片,创建带有水印的预览图像
        preview_image = combined.copy()

        # 设置固定的预览框尺寸
        target_width = 400
        target_height = 400

        # 获取原始图片宽高比
        width_ratio =  preview_image.width / target_width
        height_ratio =  preview_image.height / target_height

        # 根据宽高比计算缩放后的尺寸,保持图片比例不变
        if width_ratio > height_ratio:
            new_width = target_width
            new_height = int( preview_image.height / width_ratio)
        else:
            new_height = target_height
            new_width = int( preview_image.width / height_ratio)

        # 缩放图片
        preview_image =  preview_image.resize((new_width, new_height), Image.Resampling.LANCZOS)

        # 计算在预览框中居中显示的偏移量
        offset_x = (target_width - new_width) // 2
        offset_y = (target_height - new_height) // 2

        # 创建一个新的白色背景的图片(尺寸为预览框大小)
        final_preview_image = Image.new('RGB', (target_width, target_height), (255, 255, 255))
        final_preview_image.paste(preview_image, (offset_x, offset_y))

        # 转换为Tkinter可以显示的格式
        preview_photo = ImageTk.PhotoImage(final_preview_image)
        preview_canvas.create_image(0, 0, anchor='nw', image=preview_photo)
        preview_canvas.image = preview_photo

    except Exception as e:
        messagebox.showerror("预览错误", "无法显示水印预览: " + str(e))

# 创建程序主窗口
app = tk.Tk()
app.title("给图片添加文字水印工具")
app.geometry("950x600")
app.resizable(False, False)

# 设置主窗口居中
app.update_idletasks()
window_width = app.winfo_width()
window_height = app.winfo_height()
screen_width = app.winfo_screenwidth()
screen_height = app.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
app.geometry(f"{window_width}x{window_height}+{x}+{y}")

# 添加水印预览区域
preview_frame = tk.Frame(app)
preview_frame.pack(side="left", pady=10, padx=20)

preview_label = tk.Label(preview_frame, text="水印图片预览:")
preview_label.grid(column=0, row=0, pady=5)

preview_canvas = tk.Canvas(preview_frame, width=400, height=400, bg="white")
preview_canvas.grid(column=0, row=1, pady=5)

# 更新水印图片预览按钮
confirm_button = tk.Button(preview_frame,
                        text="更新水印图片预览",
                        command=apply_watermark_preview)
confirm_button.grid(column=0, row=3, pady=5)

# 待加水印图片选择
edit_image_frame = tk.Frame(app)
edit_image_frame.pack(side="right", pady=10, padx=20)

open_image_label = tk.Label(edit_image_frame, text="待加水印图片:", width=12, anchor="w")
open_image_label.grid(column=0, row=0, pady=5, padx=2)

open_image_entry = tk.Entry(edit_image_frame, width=40)
open_image_entry.grid(column=1, row=0, pady=5, padx=2)

open_image_browse_button = tk.Button(edit_image_frame, text="浏览", command=browse_open_image)
open_image_browse_button.grid(column=2, row=0, pady=5, padx=2)

# 水印文本内容
watermark_label = tk.Label(edit_image_frame, text="水印文本: ", width=12, anchor="w")
watermark_label.grid(column=0, row=1, pady=5, padx=2)
default_value = tk.StringVar()
default_value.set("水印文本内容")
watermark_input = tk.Entry(edit_image_frame, textvariable=default_value, width=22)
watermark_input.grid(column=1, row=1, pady=5, padx=2, sticky="w")

# 水印字体
font_label = tk.Label(edit_image_frame, text="字体: ", width=12, anchor="w")
font_label.grid(column=0, row=2, pady=5, padx=2)
font_var = tk.StringVar()
drop_menu = ttk.Combobox(
    edit_image_frame,
    textvariable=font_var,
    values=["simhei.ttf", "simkai.ttf", "simfang.ttf", "arial.ttf"],
    width=10,
    state="readonly",
)
drop_menu.current(0)
drop_menu.grid(column=1, row=2, pady=5, padx=2, sticky="w")

# 水印字体大小
current_font_size = tk.IntVar(value=50)
font_size_label = tk.Label(edit_image_frame, text="字体大小: ", width=12, anchor="w")
font_size_label.grid(column=0, row=3, pady=5, padx=2)
font_size = tk.Spinbox(edit_image_frame,
                    from_=8,
                    to=250,
                    wrap=True,
                    textvariable=current_font_size,)
font_size.grid(column=1, row=3, pady=5, padx=2, sticky="w")

# 水印不透明度
current_opacity = tk.IntVar(value=100)
opacity_label = tk.Label(edit_image_frame, text="不透明度", width=12, anchor="w")
opacity_label.grid(column=0, row=4, pady=5, padx=2)
opacity_spinbox = tk.Spinbox(edit_image_frame,
                          from_=0,
                          to=100,
                          textvariable=current_opacity,
                          wrap=False,
                          )
opacity_spinbox.grid(column=1, row=4, pady=5, padx=2, sticky="w")

# 水印颜色
text_color = (0, 0, 0)
color_selection = tk.Canvas(edit_image_frame,
                         bg=rgb_to_hex(text_color),
                         width=80,
                         height=15)
color_selection.grid(column=0, row=5, pady=5, padx=2)
color_button = tk.Button(edit_image_frame,
                      text="更改颜色",
                      command=change_text_color,
                      width=18)
color_button.grid(column=1, row=5, columnspan=1, pady=5, padx=2, sticky="w")

# 水印角度
current_tilt = tk.IntVar(value=0)
tilt_label = tk.Label(edit_image_frame, text="水印角度: ", width=12, anchor="w")
tilt_label.grid(column=0, row=6, pady=5, padx=2)
tilt_spinbox = tk.Spinbox(edit_image_frame,
                       from_=0,
                       to=359,
                       wrap=True,
                       textvariable=current_tilt,
                       )
tilt_spinbox.grid(column=1, row=6, pady=5, padx=2, sticky="w")

# 创建一个带有边框的容器控件(LabelFrame),用于存放水印位置调整滑块。
group = tk.LabelFrame(edit_image_frame, text="水印位置调整", labelanchor = 'n',pady=2, padx=2)
group.grid(column=1, row=7, pady=5)

# 水印高度滑块
img_width, img_height = 600, 600
height_label = tk.Label(group, text="高度:", width=12, anchor="w")
height_label.grid(column=0, row=7, pady=5, padx=2)
current_height = tk.IntVar(value=int(img_height / 2))
height_position = tk.Scale(group,
                        from_=0,
                        to=2000,
                        variable=current_height,
                        orient=tk.VERTICAL,
                        command=height_slider_moved)
height_position.grid(column=1, row=7, pady=5, padx=2, sticky="w")

# 水印宽度滑块
width_label = tk.Label(group, text="宽度:", width=12, anchor="w")
width_label.grid(column=0, row=8, pady=5, padx=2)
current_width = tk.IntVar(value=int(img_height / 2))
width_position = tk.Scale(group,
                       from_=0,
                       to=2000,
                       variable=current_width,
                       orient=tk.HORIZONTAL,
                       command=width_slider_moved)
width_position.grid(column=1, row=8, pady=5, padx=2, sticky="w")

# 输出图片文件夹选择
output_label = tk.Label(edit_image_frame, text="图片输出文件夹:", width=12, anchor="w")
output_label.grid(column=0, row=9, pady=5, padx=2)

output_entry = tk.Entry(edit_image_frame, width=40)
output_entry.grid(column=1, row=9, pady=5, padx=2, sticky="w")

output_browse_button = tk.Button(edit_image_frame, text="浏览", command=browse_output_image)
output_browse_button.grid(column=2, row=9, pady=5, padx=2)

# 开始添加水印按钮
confirm_button = tk.Button(edit_image_frame,
                        text="保存",
                        command=add_watermark_process)
confirm_button.grid(column=1, row=10, pady=10, padx=2,)

# 当前模块直接被执行
if __name__ == "__main__":
    # 运行主循环
    app.mainloop()

程序的操作方式

  • 选择待加水印图片文件后,在左侧编辑区域填入相应的水印操作参数,包括水印文本内容、字体、字体大小、不透明度、颜色、水印倾斜角度和通过滑块控制水印在图片上的位置,最后记得选择水印保存后输出的文件夹。此时,在左侧的预览区域会立刻显示出水印制作的预览图片。

  • 如果对水印位置不满意或者想要重新修改水印的参数,可以在右侧编辑区域修改后,点击左下角的“更新水印图片浏览”按钮,此时预览图片也会立刻更新。

  • 对预览图片满意后,点击右下角的保存按钮,就可以把水印图片保存到指定的文件夹内。