Python 小工具实战:图片水印批量添加工具

Python 小工具实战:图片水印批量添加工具

在这里插入图片描述

Python 小工具实战:图片水印批量添加工具

Python 小工具实战:图片水印批量添加工具,本文详细介绍了使用 Python开发 给图片加水印的工具,该工具基于 Pillow 和 tkinter 库构建,可解决单图处理耗时、专业软件操作复杂的问题。工具支持单图与批量处理,用户能自定义水印文字、字体大小、透明度及颜色,还可选择 9 个常用水印位置或设置行列重复分布。新增的全屏水印模式可通过调整旋转角度与间距,生成铺满图片的版权保护水印,且界面采用卡片式布局,搭配浅灰背景与蓝色按钮,简洁美观,底部状态栏实时显示操作进度。文中提供完整可运行代码,并给出参数校验、字体兼容、常见报错解决等实用内容,新手按步骤即可上手,或者直接运行使用。
在这里插入图片描述

前言

    Python作为一门简洁、易读、功能强大的编程语言,其基础语法是入门学习的核心。掌握好基础语法,能为后续的编程实践打下坚实的基础。本文将全面讲解Python3的基础语法知识,适合编程初学者系统学习。Python以其简洁优雅的语法和强大的通用性,成为当今最受欢迎的编程语言。本专栏旨在系统性地带你从零基础入门到精通Python核心。无论你是零基础小白还是希望进阶的专业开发者,都将通过清晰的讲解、丰富的实例和实战项目,逐步掌握语法基础、核心数据结构、函数与模块、面向对象编程、文件处理、主流库应用(如数据分析、Web开发、自动化)以及面向对象高级特性,最终具备独立开发能力和解决复杂问题的思维,高效应对数据分析、人工智能、Web应用、自动化脚本等广泛领域的实际需求。

在这里插入图片描述

在这里插入图片描述

🥇 点击进入Python入门专栏,Python凭借简洁易读的语法,是零基础学习编程的理想选择。本专栏专为初学者设计,系统讲解Python核心基础:变量、数据类型、流程控制、函数、文件操作及常用库入门。通过清晰示例与实用小项目,助你快速掌握编程思维,打下坚实根基,迈出自动化办公、数据分析或Web开发的第一步。

🥇 点击进入Python小游戏实战专栏, 寓教于乐,用Python亲手打造经典小游戏!本专栏通过开发贪吃蛇、飞机大战、猜数字、简易版俄罗斯方块等趣味项目,在实践中掌握Python核心语法、面向对象编程、事件处理、图形界面(如Pygame)等关键技能,将枯燥的代码学习转化为可见的成果,让学习编程充满乐趣与成就感,快速提升实战能力。

🥇 点击进入Python小工具实战专栏,告别重复劳动,用Python打造效率神器!本专栏教你开发文件批量处理、自动邮件通知、简易爬虫、桌面提醒、密码生成器、天气查询等实用小工具。聚焦os、shutil、requests、smtplib、schedule等核心库,通过真实场景案例,快速掌握自动化脚本编写技巧,解放双手,显著提升工作与生活效率,让代码真正服务于你的日常。

🥇 点击进入Python爬虫实战专栏,解锁网络数据宝库!本专栏手把手教你使用Python核心库(如requests、BeautifulSoup、Scrapy)构建高效爬虫。从基础网页解析到动态页面抓取、数据存储(CSV/数据库)、反爬策略应对及IP代理使用,通过实战项目(如电商比价、新闻聚合、图片采集、舆情监控),掌握合法合规获取并利用网络数据的核心技能,让数据成为你的超能力。

🥇 点击进入Python项目实战专栏,告别碎片化学习,挑战真实项目!本专栏精选Web应用开发(Flask/Django)、数据分析可视化、自动化办公系统、简易爬虫框架、API接口开发等综合项目。通过需求分析、架构设计、编码实现、测试部署的全流程,深入掌握工程化开发、代码复用、调试排错与团队协作核心能力,积累高质量作品集,真正具备解决复杂问题的Python实战经验。


平时处理图片时,你是不是总遇到这样的麻烦:想给一批图片加水印,单张处理太费时间,用专业软件又觉得小题大做?今天就带大家亲手做一个实用的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)单个图片加水印函数
defadd_watermark_to_single_image():# 1. 让用户选择要处理的单个图片 img_path = filedialog.askopenfilename( title="选一张要加水印的图片", filetypes=[("图片文件","*.jpg;*.png;*.jpeg"),("所有文件","*.*")])ifnot img_path:# 要是用户没选图片,直接退出函数return# 2. 获取用户设置的水印参数(文字、大小、颜色、透明度) watermark_text = entry_text.get().strip()ifnot watermark_text:# 没填水印文字的话,弹出提示 messagebox.showwarning("提示","请先输入水印文字哦!")returntry: font_size =int(entry_font_size.get().strip()) opacity =int(entry_opacity.get().strip())# 简单判断参数是否合理,避免出错if font_size <=0or opacity <0or 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)批量处理文件夹图片函数

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

defbatch_add_watermark():# 1. 让用户选要批量处理的文件夹 folder_path = filedialog.askdirectory(title="选要批量加水印的文件夹")ifnot folder_path:return# 2. 获取用户设置的水印参数(和单图函数一样,避免重复写代码) watermark_text = entry_text.get().strip()ifnot watermark_text: messagebox.showwarning("提示","请先输入水印文字哦!")returntry: font_size =int(entry_font_size.get().strip()) opacity =int(entry_opacity.get().strip())if font_size <=0or opacity <0or 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 +=1except Exception as e:# 遇到错误不崩溃,只是提示哪个文件处理失败 messagebox.showerror("处理失败",f"文件 {filename} 处理出错:\n{str(e)}")# 批量处理结束后,提示结果 messagebox.showinfo("批量处理完成",f"总共处理了 {success_count} 张图片\n结果保存在原文件夹,文件名带“_watermarked”后缀")
(3)创建图形界面函数

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

defcreate_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 classWatermarkTool: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()defsetup_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")defcreate_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)defdisable_frame(self, frame):"""自定义禁用框架内所有组件的方法(替代state=disabled)"""for widget in frame.winfo_children(): widget.config(state="disabled")defenable_frame(self, frame):"""自定义启用框架内所有组件的方法"""for widget in frame.winfo_children(): widget.config(state="normal")deftoggle_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("已禁用全屏水印模式")defchoose_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}")defget_watermark_params(self):"""获取并校验水印参数""" watermark_text = self.entry_text.get().strip()ifnot watermark_text: messagebox.showwarning("提示","请输入水印文字")returnNonetry: 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())ifnot(font_size >0and0<= opacity <=100and rows >0and cols >0):raise ValueError except ValueError: messagebox.showerror("参数错误","请检查参数:\n- 字体大小:正整数\n- 透明度:0-100\n- 行数/列数:正整数")returnNone# 全屏模式参数校验 fullscreen_params =Noneif 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())ifnot(h_spacing >0and v_spacing >0):raise ValueError fullscreen_params =(rotation, h_spacing, v_spacing)except ValueError: messagebox.showerror("参数错误","全屏模式参数需为正整数")returnNonereturn( watermark_text, font_size, opacity, self.current_color, self.position_var.get(), rows, cols, fullscreen_params )defload_font(self, size):"""加载字体(兼容中文)"""try:if os.name =="nt":# Windows系统return ImageFont.truetype("simsun.ttc", size)# 宋体支持中文else:# Mac/Linuxreturn ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", size)# 苹方字体except IOError: self.update_status("未找到指定字体,使用默认字体")return ImageFont.load_default(size=size)defcalculate_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))# 默认左上defdraw_fullscreen_watermark(self, draw, watermark_text, font, img_width, img_height, fill_color):"""绘制全屏水印(优化旋转逻辑)"""# 获取全屏模式参数 params = self.get_watermark_params()ifnot 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 inrange(-text_width, grid_width, h_spacing):for y inrange(-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)defprocess_single_image(self):"""处理单个图片""" img_path = filedialog.askopenfilename( title="选择需要加水印的图片", filetypes=[("图片文件","*.jpg;*.png;*.jpeg"),("所有文件","*.*")])ifnot img_path: self.update_status("已取消选择图片")return# 获取参数 params = self.get_watermark_params()ifnot 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 >1else0 y_step = math.floor(img.height /(rows -1))if rows >1else0# 绘制水印for i inrange(rows):for j inrange(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 >1else base_x y = base_y +(i * y_step)if rows >1else 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("处理失败,请检查图片是否正常")defbatch_process_images(self):"""批量处理文件夹图片""" folder_path = filedialog.askdirectory(title="选择批量处理的文件夹")ifnot folder_path: self.update_status("已取消选择文件夹")return params = self.get_watermark_params()ifnot 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 >1else0 y_step = math.floor(img.height /(rows -1))if rows >1else0for i inrange(rows):for j inrange(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 >1else base_x y = base_y +(i * y_step)if rows >1else 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} 张")defupdate_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实用技能,又能做出自己能用的工具,这不比单纯看教程有意思多了?

赶紧把代码复制下来试试,要是改出了新功能,欢迎在评论区分享你的玩法!

附录:扩展学习资源

  1. 官方资源
  2. 本专栏特色资源
    • 代码资源仓库:ZEEKLOG专属资源在线获取
    • 海量Python教程:关注公众号:xcLeigh,获取网盘地址
    • 一对一答疑:添加微信与博主在线沟通(备注“Python专栏”

联系博主

    xcLeigh 博主,全栈领域优质创作者,博客专家,目前,活跃在ZEEKLOG、微信公众号、小红书、知乎、掘金、快手、思否、微博、51CTO、B站、腾讯云开发者社区、阿里云开发者社区等平台,全网拥有几十万的粉丝,全网统一IP为 xcLeigh。希望通过我的分享,让大家能在喜悦的情况下收获到有用的知识。主要分享编程、开发工具、算法、技术学习心得等内容。很多读者评价他的文章简洁易懂,尤其对于一些复杂的技术话题,他能通过通俗的语言来解释,帮助初学者更好地理解。博客通常也会涉及一些实践经验,项目分享以及解决实际开发中遇到的问题。如果你是开发领域的初学者,或者在学习一些新的编程语言或框架,关注他的文章对你有很大帮助。

    亲爱的朋友,无论前路如何漫长与崎岖,都请怀揣梦想的火种,因为在生活的广袤星空中,总有一颗属于你的璀璨星辰在熠熠生辉,静候你抵达。

     愿你在这纷繁世间,能时常收获微小而确定的幸福,如春日微风轻拂面庞,所有的疲惫与烦恼都能被温柔以待,内心永远充盈着安宁与慰藉。

    至此,文章已至尾声,而您的故事仍在续写,不知您对文中所叙有何独特见解?期待您在心中与我对话,开启思想的新交流。


     💞 关注博主 🌀 带你实现畅游前后端!

     🏰 大屏可视化 🌀 带你体验酷炫大屏!

     💯 神秘个人简介 🌀 带你体验不一样得介绍!

     🥇 从零到一学习Python 🌀 带你玩转Python技术流!

     🏆 前沿应用深度测评 🌀 前沿AI产品热门应用在线等你来发掘!

     💦 :本文撰写于ZEEKLOG平台,作者:xcLeigh所有权归作者所有)https://xcleigh.blog.ZEEKLOG.net/,如果相关下载没有跳转,请查看这个地址,相关链接没有跳转,皆是抄袭本文,转载请备注本文原地址。


在这里插入图片描述

     📣 亲,码字不易,动动小手,欢迎 点赞 ➕ 收藏,如 🈶 问题请留言(或者关注下方公众号,看见后第一时间回复,还有海量编程资料等你来领!),博主看见后一定及时给您答复 💌💌💌

Read more

Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务

Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务

目录 🎯 先说说我被ES"虐惨"的经历 ✨ 摘要 1. 为什么选择Elasticsearch? 1.1 从数据库的痛苦说起 1.2 Elasticsearch的优势 2. ES核心架构解析 2.1 集群架构 2.2 索引与分片 3. Java客户端实战 3.1 客户端选型对比 3.2 RestHighLevelClient配置 3.3 Spring Data Elasticsearch配置 4. 索引设计最佳实践 4.1 索引生命周期管理 4.2 映射设计技巧 5. 查询优化实战 5.1 查询类型对比 5.

By Ne0inhk
Java中的反射机制详解:从原理到实践的全面剖析

Java中的反射机制详解:从原理到实践的全面剖析

文章目录 * 摘要 * 第一章 反射机制概述 * 1.1 什么是反射? * 1.2 反射的江湖地位:为何需要它? * 1.3 反射的优缺点 * 第二章 反射的基石:Class类与类加载 * 2.1 万物皆对象:Class对象 * 2.2 获取Class对象的三种方式 * 2.3 类加载的幕后故事 * 第三章 解剖类:反射的核心API * 3.1 操作构造方法(Constructor):创建对象 * 3.2 操作字段(Field):访问与修改属性 * 3.3 操作方法(Method):动态调用 * 第四章 深入进阶:反射的高级特性 * 4.1

By Ne0inhk
【Java】2025 年 Java 学习路线:从入门到精通

【Java】2025 年 Java 学习路线:从入门到精通

文章目录 * 一、Java基础阶段(4-8周) * 1. 开发环境搭建 * 2. 核心语法基础 * 3. 面向对象编程(OOP) * 4. 核心类库 (Java SE API) * 5. 关联技术基础 * 二、Java 进阶阶段(6-10周) * 1. JVM 深度理解 * 2. 并发编程 - 应对高并发挑战 * 3. Java新特性 - 拥抱现代化 * 4. 设计模式 * 三、数据库与MySQL(2-3周) * 1. 环境搭建 * 2. SQL核心与进阶 * 3. 数据库设计与性能优化 * 四、开发框架与中间件(8-12周) * 1. Spring 生态

By Ne0inhk