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

MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

本系列主要通过调用天气的mcp server查询天气这个例子来学习什么是mcp,以及怎么设计mcp。话不多说,我们开始吧。主要参考的是B站的老哥做的一个教程,我把链接放到这里,大家如果有什么不懂的也可以去看一下。 https://www.bilibili.com/video/BV1NLXCYTEbj?spm_id_from=333.788.videopod.episodes&vd_source=32148098d54c83926572ec0bab6a3b1d https://blog.ZEEKLOG.net/fufan_LLM/article/details/146377471 最终的效果:让deepseek-v3使用天气查询的工具来查询指定地方的天气情况 技术介绍 MCP,即Model Context Protocol(模型上下文协议),是由Claude的母公司Anthropic在2024年底推出的一项创新技术协议。在它刚问世时,并未引起太多关注,反响较为平淡。然而,随着今年智能体Agent领域的迅猛发展,MCP逐渐进入大众视野并受到广泛关注。今年2月,

By Ne0inhk
可以在命令行通过大模型使用上下文协议(MCP)与外部工具交互的软件:小巧的MCPHost

可以在命令行通过大模型使用上下文协议(MCP)与外部工具交互的软件:小巧的MCPHost

小巧的MCPHost MCPHost 可以在命令行下使用,使大型语言模型(LLM)能够通过模型上下文协议(MCP)与外部工具进行交互。目前支持Claude 3.5 Sonnet和Ollama等。本次实践使用自己架设的Deepseek v3模型,跑通了Time MCP服务。  官网:GitHub - mark3labs/mcphost: A CLI host application that enables Large Language Models (LLMs) to interact with external tools through the Model Context Protocol (MCP). 下载安装 使用非常方便,直接下载解压即可使用。官网提供Windows、Linux和MacOS三个系统的压缩包: https://github.com/

By Ne0inhk
实战篇:Python开发monogod数据库mcp server看完你就会了

实战篇:Python开发monogod数据库mcp server看完你就会了

原创不易,请关注公众号:【爬虫与大模型开发】,大模型的应用开发之路,整理了大模型在现在的企业级应用的实操及大家需要注意的一些AI开发的知识点!持续输出爬虫与大模型的相关文章。 前言 目前mcp协议是给deepseek大模型插上工具链的翅膀,让大模型不仅拥有超高的推理和文本生成能力,还能具备执行大脑意识的工具能力! 如何开发一个mcp? mcp是一种协议,指的是模型上下文协议 (Model Context Protocol)。 官方结成的mcp https://github.com/modelcontextprotocol/python-sdk mcp库 pip install mcp from mcp.server.fastmcp import FastMCP 我们先来做一个简单的案例 from mcp.server.fastmcp import FastMCP import requests mcp = FastMCP("spider") @mcp.tool() def crawl(

By Ne0inhk
【大模型实战篇】基于Claude MCP协议的智能体落地示例

【大模型实战篇】基于Claude MCP协议的智能体落地示例

1. 背景         之前我们在《MCP(Model Context Protocol) 大模型智能体第一个开源标准协议》一文中,介绍了MCP的概念,虽然了解了其概念、架构、解决的问题,但还缺少具体的示例,来帮助进一步理解整套MCP框架如何落地。         今天我们基于claude的官方例子--获取天气预报【1】,来理解MCP落地的整条链路。 2. MCP示例         该案例是构建一个简单的MCP天气预报服务器,并将其连接到主机,即Claude for Desktop。从基本设置开始,然后逐步发展到更复杂的使用场景。         大模型虽然能力非常强,但其弊端就是内容是过时的,这里的过时不是说内容很旧,只是表达内容具有非实时性。比如没有获取天气预报和严重天气警报的能力。因此我们将使用MCP来解决这一问题。         构建一个服务器,该服务器提供两个工具:获取警报(get-alerts)和获取预报(get-forecast)。然后,将该服务器连接到MCP主机(在本例中为Claude for Desktop)。         首先我们配置下环

By Ne0inhk