基于Python實(shí)現(xiàn)一個(gè)圖片水印批量添加工具
平時(shí)處理圖片時(shí),你是不是總遇到這樣的麻煩:想給一批圖片加水印,單張?zhí)幚硖M(fèi)時(shí)間,用專業(yè)軟件又覺(jué)得小題大做?今天就帶大家親手做一個(gè)實(shí)用的Python小工具,既能選單個(gè)圖片加水印,也能批量處理整個(gè)文件夾的圖片,小白跟著步驟走也能搞定!
一、工具核心功能與準(zhǔn)備工作
先明確下這個(gè)工具能幫我們解決什么問(wèn)題,以及動(dòng)手前需要準(zhǔn)備哪些東西。
1. 核心功能
咱們做的這個(gè)工具,主打“靈活”和“高效”,具體能實(shí)現(xiàn)這3個(gè)功能:
- 單圖處理:選一張圖片,手動(dòng)調(diào)整水印位置、透明度,預(yù)覽效果后再保存
- 批量處理:選一個(gè)文件夾,一鍵給里面所有圖片(支持jpg、png、jpeg格式)加統(tǒng)一水印
- 自定義設(shè)置:自己改水印文字、字體大小、顏色,還能調(diào)水印的透明度,避免遮擋圖片內(nèi)容
2. 環(huán)境準(zhǔn)備
做這個(gè)工具不用復(fù)雜的環(huán)境,只要裝2個(gè)Python庫(kù)就行,新手直接按步驟來(lái):
- 先確認(rèn)電腦裝了Python(建議3.7及以上版本,沒(méi)裝的話去官網(wǎng)下載,記得勾“Add Python to PATH”)
- 打開(kāi)命令提示符(Windows按Win+R,輸cmd;Mac打開(kāi)終端),輸入下面兩行命令,安裝需要的庫(kù):
- 安裝處理圖片的庫(kù):
pip install pillow - 安裝做圖形界面的庫(kù)(讓工具更直觀):
pip install tkinter
(注:tkinter在Python3里通常是自帶的,要是安裝報(bào)錯(cuò),直接跳過(guò)這步試試,大概率能正常用)
- 安裝處理圖片的庫(kù):
二、代碼拆解與實(shí)現(xiàn)(附完整代碼)
我把整個(gè)工具的代碼分成了3個(gè)部分,每部分都標(biāo)了注釋,大家可以跟著理解,也能直接復(fù)制用。
1. 導(dǎo)入需要的庫(kù)
先把后面要用到的工具包導(dǎo)進(jìn)來(lái),就像做飯前把調(diào)料準(zhǔn)備好一樣:
from PIL import Image, ImageDraw, ImageFont # 處理圖片和添加文字水印 import tkinter as tk # 做圖形界面 from tkinter import filedialog, messagebox, ttk # 界面里的文件選擇、提示框等組件 import os # 處理文件夾和文件路徑
2. 核心功能函數(shù)(加水印的關(guān)鍵)
這部分是工具的“心臟”,負(fù)責(zé)實(shí)現(xiàn)圖片讀取、水印添加、保存等核心操作,我拆成了3個(gè)函數(shù),每個(gè)函數(shù)干一件事,邏輯更清楚:
(1)單個(gè)圖片加水印函數(shù)
def add_watermark_to_single_image():
# 1. 讓用戶選擇要處理的單個(gè)圖片
img_path = filedialog.askopenfilename(
title="選一張要加水印的圖片",
filetypes=[("圖片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
)
if not img_path: # 要是用戶沒(méi)選圖片,直接退出函數(shù)
return
# 2. 獲取用戶設(shè)置的水印參數(shù)(文字、大小、顏色、透明度)
watermark_text = entry_text.get().strip()
if not watermark_text: # 沒(méi)填水印文字的話,彈出提示
messagebox.showwarning("提示", "請(qǐng)先輸入水印文字哦!")
return
try:
font_size = int(entry_font_size.get().strip())
opacity = int(entry_opacity.get().strip())
# 簡(jiǎn)單判斷參數(shù)是否合理,避免出錯(cuò)
if font_size <= 0 or opacity < 0 or opacity > 100:
raise ValueError
except ValueError:
messagebox.showerror("錯(cuò)誤", "字體大小要填正整數(shù),透明度要在0-100之間哦!")
return
# 3. 讀取圖片,處理PNG透明格式(避免透明圖片加水印后背景變黑色)
img = Image.open(img_path).convert("RGBA")
# 創(chuàng)建一個(gè)和圖片一樣大的透明圖層,用來(lái)放水印
watermark_layer = Image.new("RGBA", img.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(watermark_layer)
# 4. 加載字體(這里用系統(tǒng)默認(rèn)字體,Windows和Mac路徑不一樣,做了兼容)
try:
if os.name == "nt": # Windows系統(tǒng)
font = ImageFont.truetype("arial.ttf", font_size)
else: # Mac或Linux系統(tǒng)
font = ImageFont.truetype("/Library/Fonts/Arial.ttf", font_size)
except IOError:
# 要是沒(méi)找到Arial字體,就用默認(rèn)字體,雖然丑點(diǎn)但不影響用
font = ImageFont.load_default(size=font_size)
# 5. 計(jì)算水印位置(默認(rèn)放在右下角,距離邊緣20像素,也可以自己改這里的數(shù)值)
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透明度,轉(zhuǎn)成PIL需要的0-255范圍
alpha = int(255 * (opacity / 100))
# 這里默認(rèn)水印是黑色,想改顏色的話,把(0,0,0,alpha)里的前三個(gè)數(shù)換成RGB值
draw.text((x, y), watermark_text, font=font, fill=(0, 0, 0, alpha))
# 7. 合并圖片和水印圖層,保存結(jié)果
result = Image.alpha_composite(img, watermark_layer)
# 處理PNG轉(zhuǎn)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)批量處理文件夾圖片函數(shù)
批量處理和單圖邏輯差不多,主要多了“遍歷文件夾”的步驟:
def batch_add_watermark():
# 1. 讓用戶選要批量處理的文件夾
folder_path = filedialog.askdirectory(title="選要批量加水印的文件夾")
if not folder_path:
return
# 2. 獲取用戶設(shè)置的水印參數(shù)(和單圖函數(shù)一樣,避免重復(fù)寫代碼)
watermark_text = entry_text.get().strip()
if not watermark_text:
messagebox.showwarning("提示", "請(qǐng)先輸入水印文字哦!")
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("錯(cuò)誤", "字體大小要填正整數(shù),透明度要在0-100之間哦!")
return
# 3. 遍歷文件夾里的所有文件,只處理圖片
# 先定義支持的圖片格式,避免處理非圖片文件
supported_formats = ('.jpg', '.jpeg', '.png')
# 統(tǒng)計(jì)成功處理的圖片數(shù)量
success_count = 0
# 加載字體(和單圖函數(shù)一樣,做了系統(tǒng)兼容)
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. 逐個(gè)處理圖片
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)
# 計(jì)算水印位置(同樣默認(rèn)右下角)
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:
# 遇到錯(cuò)誤不崩潰,只是提示哪個(gè)文件處理失敗
messagebox.showerror("處理失敗", f"文件 {filename} 處理出錯(cuò):\n{str(e)}")
# 批量處理結(jié)束后,提示結(jié)果
messagebox.showinfo("批量處理完成", f"總共處理了 {success_count} 張圖片\n結(jié)果保存在原文件夾,文件名帶“_watermarked”后綴")
(3)創(chuàng)建圖形界面函數(shù)
有了核心功能,再做個(gè)簡(jiǎn)單的界面,不用記命令,點(diǎn)鼠標(biāo)就能操作:
def create_gui():
# 1. 初始化窗口
root = tk.Tk()
root.title("Python圖片水印工具")
root.geometry("500x300") # 窗口大小,也可以自己改
root.resizable(True, True) # 允許拉伸窗口
# 2. 創(chuàng)建標(biāo)簽和輸入框(按網(wǎng)格布局,整齊好看)
# 水印文字設(shè)置
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, "我的圖片") # 默認(rèn)文字,可修改
# 字體大小設(shè)置
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") # 默認(rèn)20號(hào)字,可修改
# 透明度設(shè)置
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") # 默認(rèn)半透明,可修改
# 3. 創(chuàng)建功能按鈕
# 單圖處理按鈕
btn_single = ttk.Button(root, text="處理單個(gè)圖片", 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. 運(yùn)行窗口
root.mainloop()
3. 主程序入口
最后加一句代碼,讓腳本運(yùn)行時(shí)自動(dòng)打開(kāi)界面:
if __name__ == "__main__":
create_gui()
三、升級(jí)版圖片水印工具:支持多參數(shù)自定義配置
在前一版工具的基礎(chǔ)上,我們新增了文字顏色選擇、水印位置自由切換、重復(fù)水印次數(shù)設(shè)置等功能,讓水印效果更靈活可控。以下是完整的升級(jí)版代碼,包含所有參數(shù)配置功能。
3.1 功能升級(jí)說(shuō)明
- 新增文字顏色選擇:支持通過(guò)顏色選擇器自定義水印文字顏色
- 水印位置可選:提供9個(gè)常用位置(如左上角、中心、右下角等)
- 重復(fù)水印次數(shù):可設(shè)置水印在圖片中重復(fù)出現(xiàn)的行數(shù)和列數(shù)(如3x3網(wǎng)格分布)
- 保留原功能:兼容單圖/批量處理、字體大小/透明度調(diào)整
3.2 完整代碼實(shí)現(xiàn)(含參數(shù)配置)
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") # 淺灰背景
# 初始化參數(shù)
self.current_color = (0, 0, 0) # 默認(rèn)黑色
self.fullscreen_mode = False # 全屏水印模式開(kāi)關(guān)
# 設(shè)置全局樣式
self.setup_styles()
# 創(chuàng)建界面
self.create_widgets()
def setup_styles(self):
"""配置ttk樣式,統(tǒng)一美化界面"""
style = ttk.Style()
style.theme_use("clam") # 使用clam主題增強(qiáng)跨平臺(tái)一致性
# 標(biāo)題樣式
style.configure("Title.TLabel",
font=("微軟雅黑", 12, "bold"),
background="#f0f0f0",
foreground="#333333")
# 標(biāo)簽樣式
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")])
# 顏色預(yù)覽樣式(不依賴height,用padding控制大?。?
self.color_style = ttk.Style()
self.color_style.configure("Color.TLabel",
background="#000000", # 默認(rèn)黑色
borderwidth=1,
relief="solid",
padding=(10, 5)) # 用內(nèi)邊距控制寬高(水平10,垂直5)
# 分組框標(biāo)題樣式(解決-font錯(cuò)誤)
style.configure("TLabelFrame.Label",
font=("微軟雅黑", 10, "bold"),
foreground="#333333")
def create_widgets(self):
"""創(chuàng)建界面組件,采用卡片式布局"""
# 主容器(帶邊距)
main_frame = ttk.Frame(self.root, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 參數(shù)配置卡片(白色背景,陰影效果)
config_card = ttk.LabelFrame(main_frame, text="水印參數(shù)配置", 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. 文字顏色選擇(修復(fù)-height錯(cuò)誤:用padding控制預(yù)覽框大?。?
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參數(shù),依賴樣式中的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. 重復(fù)分布(行數(shù)+列數(shù))
ttk.Label(config_card, text="重復(fù)分布:", style="Label.TLabel").grid(
row=4, column=0, padx=10, pady=10, sticky="w")
ttk.Label(config_card, text="行數(shù):", 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="列數(shù):", 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="全屏水印模式(忽略位置和行列設(shè)置)",
command=self.toggle_fullscreen_mode
)
self.fullscreen_check.pack(side=tk.LEFT)
# 7. 全屏水印參數(shù)(僅在全屏模式下啟用)
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) # 默認(rèn)禁用(自定義禁用方法)
ttk.Label(self.fullscreen_params_frame, text="水印旋轉(zhuǎn)角度:", 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") # 默認(rèn)旋轉(zhuǎn)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")
# 按鈕區(qū)域(底部居中)
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(
btn_frame, text="處理單個(gè)圖片", 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)
# 狀態(tài)標(biāo)簽(底部顯示提示信息)
self.status_label = ttk.Label(
main_frame, text="請(qǐng)配置參數(shù)后選擇圖片處理", style="Label.TLabel", anchor=tk.CENTER
)
self.status_label.pack(fill=tk.X, pady=10)
# 讓列自適應(yīng)寬度
config_card.columnconfigure(1, weight=1)
config_card.columnconfigure(3, weight=1)
def disable_frame(self, frame):
"""自定義禁用框架內(nèi)所有組件的方法(替代state=disabled)"""
for widget in frame.winfo_children():
widget.config(state="disabled")
def enable_frame(self, frame):
"""自定義啟用框架內(nèi)所有組件的方法"""
for widget in frame.winfo_children():
widget.config(state="normal")
def toggle_fullscreen_mode(self):
"""切換全屏水印模式,啟用/禁用相關(guān)參數(shù)"""
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):
"""選擇水印顏色并更新預(yù)覽"""
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):
"""獲取并校驗(yàn)水印參數(shù)"""
watermark_text = self.entry_text.get().strip()
if not watermark_text:
messagebox.showwarning("提示", "請(qǐng)輸入水印文字")
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("參數(shù)錯(cuò)誤", "請(qǐng)檢查參數(shù):\n- 字體大小:正整數(shù)\n- 透明度:0-100\n- 行數(shù)/列數(shù):正整數(shù)")
return None
# 全屏模式參數(shù)校驗(yàn)
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("參數(shù)錯(cuò)誤", "全屏模式參數(shù)需為正整數(shù)")
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系統(tǒng)
return ImageFont.truetype("simsun.ttc", size) # 宋體支持中文
else: # Mac/Linux
return ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", size) # 蘋方字體
except IOError:
self.update_status("未找到指定字體,使用默認(rèn)字體")
return ImageFont.load_default(size=size)
def calculate_position(self, position, img_w, img_h, text_w, text_h):
"""計(jì)算水印位置坐標(biāo)"""
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)) # 默認(rèn)左上
def draw_fullscreen_watermark(self, draw, watermark_text, font, img_width, img_height, fill_color):
"""繪制全屏水?。▋?yōu)化旋轉(zhuǎn)邏輯)"""
# 獲取全屏模式參數(shù)
params = self.get_watermark_params()
if not params:
return
fullscreen_params = params[-1]
rotation, h_spacing, v_spacing = fullscreen_params
# 計(jì)算文字基礎(chǔ)尺寸
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]
# 計(jì)算網(wǎng)格覆蓋范圍(避免邊緣空白)
grid_width = img_width + text_width * 2
grid_height = img_height + text_height * 2
# 遍歷網(wǎng)格繪制旋轉(zhuǎn)水印
for x in range(-text_width, grid_width, h_spacing):
for y in range(-text_height, grid_height, v_spacing):
# 創(chuàng)建臨時(shí)圖層(尺寸足夠容納旋轉(zhuǎn)后的文字)
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)
# 在臨時(shí)圖層中心繪制文字
temp_draw.text(
(temp_size//2 - text_width//2, temp_size//2 - text_height//2),
watermark_text,
font=font,
fill=fill_color
)
# 旋轉(zhuǎn)臨時(shí)圖層(expand=True確保完整顯示)
rotated_layer = temp_layer.rotate(rotation, expand=True)
rotated_w, rotated_h = rotated_layer.size
# 計(jì)算最終繪制位置(文字中心對(duì)齊網(wǎng)格點(diǎn))
draw_x = x - rotated_w // 2
draw_y = y - rotated_h // 2
# 繪制旋轉(zhuǎn)后的水?。ㄓ胮aste替代bitmap,避免透明度問(wèn)題)
draw._image.paste(rotated_layer, (draw_x, draw_y), rotated_layer)
def process_single_image(self):
"""處理單個(gè)圖片"""
img_path = filedialog.askopenfilename(
title="選擇需要加水印的圖片",
filetypes=[("圖片文件", "*.jpg;*.png;*.jpeg"), ("所有文件", "*.*")]
)
if not img_path:
self.update_status("已取消選擇圖片")
return
# 獲取參數(shù)
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:
# 普通模式:計(jì)算文字尺寸和分布
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"錯(cuò)誤信息:\n{str(e)}")
self.update_status("處理失敗,請(qǐng)檢查圖片是否正常")
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"開(kāi)始批量處理:{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)
# 保存處理結(jié)果
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("單個(gè)文件失敗", f"文件 {filename} 處理出錯(cuò):\n{str(e)}")
messagebox.showinfo("批量完成", f"批量處理結(jié)束!\n共成功處理 {success_count} 張圖片")
self.update_status(f"批量處理完成,成功 {success_count} 張")
def update_status(self, text):
"""更新底部狀態(tài)提示"""
self.status_label.config(text=text)
self.root.update_idletasks() # 立即刷新界面
if __name__ == "__main__":
root = tk.Tk()
app = WatermarkTool(root)
root.mainloop()
3.3 參數(shù)配置說(shuō)明
3.3.1. 基礎(chǔ)參數(shù)
- 水印文字:輸入需要添加的水印內(nèi)容(支持中文)
- 字體大小:設(shè)置文字大?。ńㄗh10-50之間,根據(jù)圖片尺寸調(diào)整)
- 透明度:0-100的數(shù)值(0為完全透明,100為完全不透明)
3.3.2. 高級(jí)參數(shù)
- 文字顏色:點(diǎn)擊"選擇顏色"打開(kāi)調(diào)色板,可自定義任意顏色(默認(rèn)黑色)
- 水印位置:下拉選擇9個(gè)常用位置(如"中心"會(huì)將水印放在圖片正中間)
- 重復(fù)分布:通過(guò)"行數(shù)"和"列數(shù)"設(shè)置水印重復(fù)次數(shù):
- 1x1:只顯示一個(gè)水?。J(rèn))
- 2x2:顯示4個(gè)水?。?行2列均勻分布)
- 3x3:顯示9個(gè)水?。ㄟm合大圖片全屏水印)
3.4 使用示例和效果演示
- 制作版權(quán)水?。何淖?quot;© 2025 xcLeigh工作室",白色半透明(透明度30),右下角1x1分布
- 制作全屏水?。何淖?quot;內(nèi)部資料",灰色(RGB 128,128,128),透明度20,3x3中心分布

批量生成效果

生成水印前的圖片

生成水印后的圖片

四、工具使用步驟(小白友好版)
代碼寫好后,怎么用呢?跟著這4步來(lái),保證能成功:
1. 保存代碼
把上面所有代碼復(fù)制下來(lái),粘貼到記事本里,然后點(diǎn)“文件-另存為”,注意2個(gè)細(xì)節(jié):
- 文件名:結(jié)尾必須是
.py,比如watermark_tool.py - 保存類型:選“所有文件”,編碼選“UTF-8”(避免中文亂碼)
2. 運(yùn)行工具
找到保存好的watermark_tool.py文件,雙擊它就能打開(kāi)工具(前提是已經(jīng)裝了Python)。打開(kāi)后會(huì)看到這樣的界面:
- 上面三個(gè)輸入框,分別填“水印文字”“字體大小”“透明度”
- 下面兩個(gè)按鈕,選“處理單個(gè)圖片”或“批量處理文件夾”
3. 單圖處理操作
- 在輸入框里填好參數(shù)(比如水印文字填“我的旅行照”,字體20,透明度50)
- 點(diǎn)“處理單個(gè)圖片”,在彈出的窗口里選要加水印的圖片(比如桌面上的
photo.jpg) - 選好后,會(huì)彈出“保存”窗口,選個(gè)保存位置(比如還是桌面),點(diǎn)“保存”
- 提示“成功”后,去保存位置找圖片,就能看到右下角多了水印
4. 批量處理操作
- 同樣先填好參數(shù)(批量處理時(shí),所有圖片會(huì)用同一個(gè)參數(shù))
- 點(diǎn)“批量處理文件夾”,選要處理的文件夾(比如“D盤-圖片集”)
- 工具會(huì)自動(dòng)處理文件夾里所有jpg、png圖片,不用手動(dòng)等
- 處理完會(huì)提示“完成”,去原文件夾看,每張圖片都會(huì)多一個(gè)帶“_watermarked”后綴的副本,比如
photo_watermarked.jpg
五、常見(jiàn)問(wèn)題解決(避坑指南)
用的時(shí)候可能會(huì)遇到小問(wèn)題,這里整理了3個(gè)常見(jiàn)情況,教你怎么解決:
1. 雙擊腳本沒(méi)反應(yīng)/報(bào)錯(cuò)“no module named xxx”
原因:沒(méi)裝需要的庫(kù),或者Python沒(méi)添加到環(huán)境變量。
解決辦法:
- 按前面“準(zhǔn)備工作”的步驟,重新打開(kāi)cmd/終端,輸入
pip install pillow和pip install tkinter - 要是還沒(méi)反應(yīng),右鍵點(diǎn)擊腳本,選“打開(kāi)方式”,手動(dòng)選Python.exe(通常在
C:\Users\你的用戶名\AppData\Local\Programs\Python\PythonXX文件夾里)
2. 處理PNG圖片后,背景變黑色
原因:PNG圖片有透明背景,保存成JPG時(shí)不支持透明,默認(rèn)會(huì)填黑色。
解決辦法:
- 處理PNG圖片時(shí),保存的時(shí)候選“保存類型”為PNG,不要選JPG
- 要是必須存JPG,可以在代碼里改“填充顏色”:把
fill=(0,0,0,alpha)改成fill=(255,255,255,alpha),水印會(huì)變成白色,背景也會(huì)變成白色
3. 水印文字顯示亂碼/方塊
原因:系統(tǒng)里沒(méi)有Arial字體,導(dǎo)致字體加載失敗。
解決辦法:
- 手動(dòng)換個(gè)系統(tǒng)有的字體,比如Windows里的“微軟雅黑”,把代碼里加載字體的行改成:
font = ImageFont.truetype("msyh.ttc", font_size) - 或者直接用默認(rèn)字體,雖然不好看,但不會(huì)亂碼,代碼里保留
font = ImageFont.load_default(size=font_size)就行
六、功能擴(kuò)展建議(進(jìn)階玩法)
要是你覺(jué)得基礎(chǔ)功能不夠用,還能給工具加這些小功能,難度也不大:
- 加水印位置選擇:現(xiàn)在默認(rèn)右下角,可以加個(gè)下拉框,讓用戶選“左上角”“中間”“左下角”等位置
- 加圖片水?。翰还饽芗游淖?,還能加圖片水印(比如自己的logo),用
Image.open("logo.png")讀取logo,再貼到原圖上 - 批量修改保存路徑:現(xiàn)在批量處理只能存在原文件夾,可加個(gè)“選擇輸出文件夾”按鈕,把結(jié)果統(tǒng)一存到新文件夾里
- 預(yù)覽功能:加水印前先顯示預(yù)覽圖,用戶確認(rèn)沒(méi)問(wèn)題再保存,避免反復(fù)修改
七、總結(jié)
這個(gè)Python水印工具雖然簡(jiǎn)單,但特別實(shí)用,不管是處理日常照片,還是工作中給素材加水印,都能省不少時(shí)間。關(guān)鍵是代碼全在上面,自己能改,想加什么功能就加什么。
新手不用怕,跟著步驟一步步來(lái),先跑通基礎(chǔ)版本,再慢慢改細(xì)節(jié),既能學(xué)會(huì)Python實(shí)用技能,又能做出自己能用的工具,這不比單純看教程有意思多了?
以上就是基于Python實(shí)現(xiàn)一個(gè)圖片水印批量添加工具的詳細(xì)內(nèi)容,更多關(guān)于Python圖片水印批量添加的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python首次安裝后運(yùn)行報(bào)錯(cuò)(0xc000007b)的解決方法
最近在安裝完P(guān)ython后運(yùn)行發(fā)現(xiàn)居然報(bào)錯(cuò)了,錯(cuò)誤代碼是0xc000007b,于是通過(guò)往上查找發(fā)現(xiàn)是因?yàn)槭状伟惭bPython缺乏VC++庫(kù)的原因,下面通過(guò)這篇文章看看如何解決這個(gè)問(wèn)題吧。2016-10-10
python自動(dòng)化測(cè)試之Selenium詳解
這篇文章主要為大家詳細(xì)介紹了python自動(dòng)化測(cè)試之Selenium,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
Python網(wǎng)絡(luò)爬蟲項(xiàng)目:內(nèi)容提取器的定義
本篇文章主要介紹了Python網(wǎng)絡(luò)爬蟲項(xiàng)目,這能有效的節(jié)省程序員的時(shí)間,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-10-10
python實(shí)現(xiàn)信號(hào)時(shí)域統(tǒng)計(jì)特征提取代碼
今天小編就為大家分享一篇python實(shí)現(xiàn)信號(hào)時(shí)域統(tǒng)計(jì)特征提取代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02
Python3 pip3 list 出現(xiàn) DEPRECATION 警告的解決方法
今天小編就為大家分享一篇Python3 pip3 list 出現(xiàn) DEPRECATION 警告的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
時(shí)間序列重采樣和pandas的resample方法示例解析
這篇文章主要為大家介紹了時(shí)間序列重采樣和pandas的resample方法示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
python實(shí)現(xiàn)機(jī)器人行走效果
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)機(jī)器人行走效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
Python正則表達(dá)式語(yǔ)法及re模塊中的常用函數(shù)詳解
這篇文章主要給大家介紹了關(guān)于Python正則表達(dá)式語(yǔ)法及re模塊中常用函數(shù)的相關(guān)資料,正則表達(dá)式是一種強(qiáng)大的字符串處理工具,可以用于匹配、切分、查找和替換等操作,文章詳細(xì)講解了如何使用正則表達(dá)式進(jìn)行匹配,,需要的朋友可以參考下2025-04-04

