Python編寫一個(gè)圖片批量添加文字水印工具(附代碼)
在日常工作和自媒體運(yùn)營中,我們經(jīng)常需要給大量的圖片添加水印以保護(hù)版權(quán)。市面上的工具要么收費(fèi),要么功能單一。今天,我們將使用 Python 強(qiáng)大的 GUI 庫 PyQt5 和圖像處理庫 Pillow (PIL),親手打造一個(gè)免費(fèi)、開源且功能強(qiáng)大的批量水印工具。
項(xiàng)目目標(biāo)
我們需要實(shí)現(xiàn)一個(gè)具備以下功能的桌面軟件:
- 批量處理:支持拖拽或選擇多個(gè)文件/文件夾。
- 可視化預(yù)覽:在調(diào)整參數(shù)時(shí)實(shí)時(shí)預(yù)覽水印效果。
- 高度自定義:支持設(shè)置水印文字、大小、顏色、透明度、旋轉(zhuǎn)角度。
- 布局靈活:支持九宮格位置(如左上、右下)以及全圖平鋪模式。
- 防卡頓:使用多線程處理圖片,避免界面凍結(jié)。
技術(shù)棧
Python 3.x
PyQt5: 用于構(gòu)建圖形用戶界面 (GUI)。
Pillow (PIL): 用于核心的圖像處理(繪制文字、旋轉(zhuǎn)、合成)。
運(yùn)行效果

環(huán)境搭建
首先,我們需要安裝必要的第三方庫:
pip install PyQt5 Pillow
核心實(shí)現(xiàn)思路
界面設(shè)計(jì) (PyQt5)
我們將界面分為左右兩部分:
- 左側(cè) (控制面板):包含文件列表、輸出路徑設(shè)置、以及所有的水印參數(shù)控件(輸入框、滑塊、下拉框等)。
- 右側(cè) (預(yù)覽區(qū)):顯示當(dāng)前選中圖片的實(shí)時(shí)預(yù)覽效果。
我們使用 QHBoxLayout (水平布局) 來容納左右面板,左側(cè)面板內(nèi)部使用 QVBoxLayout (垂直布局) 來排列各個(gè)設(shè)置組 (QGroupBox)。
圖像處理核心 (Pillow)
這是整個(gè)工具的靈魂。主要步驟如下:
1.打開圖片:使用 Image.open() 并轉(zhuǎn)換為 RGBA 模式以便處理透明度。
2.創(chuàng)建水印層:創(chuàng)建一個(gè)與原圖等大的透明圖層。
3.繪制文字:
- 使用
ImageDraw.Draw繪制文本。 - 計(jì)算文本大小 (
draw.textbbox) 以便居中或定位。 - 處理顏色和透明度。
4.旋轉(zhuǎn)與平鋪:
- 如果需要旋轉(zhuǎn),先在一個(gè)單獨(dú)的小圖層上繪制文字并旋轉(zhuǎn),然后粘貼到大水印層上。
- 平鋪模式:通過雙重循環(huán) (
for x... for y...) 計(jì)算坐標(biāo),將水印重復(fù)粘貼到全圖。
5.合成與保存:使用 Image.alpha_composite 將水印層疊加到原圖,最后保存。
多線程處理 (QThread)
為了防止在處理幾百張大圖時(shí)界面卡死(“未響應(yīng)”),我們將耗時(shí)的圖片處理邏輯放入后臺(tái)線程 Worker 中。
class Worker(QThread):
progress = pyqtSignal(int) # 進(jìn)度信號(hào)
finished = pyqtSignal(str) # 完成信號(hào)
def run(self):
# 遍歷文件列表進(jìn)行處理
for i, file_path in enumerate(self.files):
self.process_image(file_path)
self.progress.emit(...) # 更新進(jìn)度條
核心代碼解析
水印繪制邏輯
這是實(shí)現(xiàn)平鋪和定位的關(guān)鍵代碼片段:
def process_image(self, file_path):
with Image.open(file_path).convert("RGBA") as img:
# 創(chuàng)建全透明水印層
watermark = Image.new('RGBA', img.size, (0, 0, 0, 0))
# ... (省略字體加載和顏色設(shè)置) ...
# 創(chuàng)建單個(gè)水印小圖用于旋轉(zhuǎn)
txt_img = Image.new('RGBA', (max_dim, max_dim), (0, 0, 0, 0))
txt_draw = ImageDraw.Draw(txt_img)
txt_draw.text((text_x, text_y), text, font=font, fill=fill_color)
# 旋轉(zhuǎn)
if rotation != 0:
txt_img = txt_img.rotate(rotation, resample=Image.BICUBIC)
# 核心布局邏輯
if position == '平鋪 (Tile)':
# 雙重循環(huán)實(shí)現(xiàn)全圖平鋪
step_x = int(w_width + spacing)
step_y = int(w_height + spacing)
for y in range(0, img.height, step_y):
for x in range(0, img.width, step_x):
watermark.paste(txt_img, (x, y), txt_img)
else:
# 九宮格定位邏輯
# 根據(jù) '左', '右', '上', '下' 關(guān)鍵字計(jì)算坐標(biāo)
# ...
watermark.paste(txt_img, (pos_x, pos_y), txt_img)
# 合成最終圖片
out = Image.alpha_composite(img, watermark)
實(shí)時(shí)預(yù)覽實(shí)現(xiàn)
預(yù)覽功能的難點(diǎn)在于性能。我們不能每次調(diào)整參數(shù)都去處理原圖(原圖可能幾千萬像素)。
優(yōu)化方案:
- 加載原圖后,先生成一個(gè)較小的縮略圖(例如最大邊長 800px)。
- 所有的預(yù)覽計(jì)算都在這個(gè)縮略圖上進(jìn)行。
- 注意:字體大小和間距需要根據(jù)縮略圖的比例進(jìn)行縮放,否則預(yù)覽效果會(huì)和實(shí)際輸出不一致。
# 縮放比例計(jì)算 scale_factor = preview_img.width / original_img_width # 字體大小也要隨之縮放 preview_font_size = int(user_set_font_size * scale_factor)
完整功能展示
運(yùn)行 main.py 后,你將看到如下界面:
添加圖片:點(diǎn)擊“添加圖片”或“添加文件夾”導(dǎo)入素材。
調(diào)整參數(shù):
- 輸入文字 “My Watermark”。
- 拖動(dòng)“旋轉(zhuǎn)角度”滑塊到 30 度。
- 選擇位置為“平鋪”。
- 調(diào)整透明度為 30% 使得水印不喧賓奪主。
預(yù)覽:右側(cè)會(huì)立即顯示效果,所見即所得。
輸出:選擇輸出目錄,點(diǎn)擊“開始處理”,進(jìn)度條跑完即大功告成!
完整代碼
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QFileDialog, QSlider, QSpinBox,
QComboBox, QColorDialog, QProgressBar, QMessageBox, QGroupBox,
QScrollArea, QListWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QPixmap, QImage, QColor, QFont
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
import math
class Worker(QThread):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, files, output_dir, config):
super().__init__()
self.files = files
self.output_dir = output_dir
self.config = config
self.is_running = True
def run(self):
total = len(self.files)
success_count = 0
if not os.path.exists(self.output_dir):
try:
os.makedirs(self.output_dir)
except Exception as e:
self.error.emit(f"無法創(chuàng)建輸出目錄: {str(e)}")
return
for i, file_path in enumerate(self.files):
if not self.is_running:
break
try:
self.process_image(file_path)
success_count += 1
except Exception as e:
print(f"Error processing {file_path}: {e}")
self.progress.emit(int((i + 1) / total * 100))
self.finished.emit(f"處理完成!成功: {success_count}/{total}")
def process_image(self, file_path):
try:
with Image.open(file_path).convert("RGBA") as img:
# 創(chuàng)建水印層
watermark = Image.new('RGBA', img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark)
text = self.config['text']
font_size = self.config['font_size']
opacity = self.config['opacity']
rotation = self.config['rotation']
color = self.config['color'] # Tuple (r, g, b)
position = self.config['position']
spacing = self.config['spacing'] # For tiling
# 加載字體 (使用默認(rèn)字體,因?yàn)橄到y(tǒng)字體路徑復(fù)雜,這里簡化處理)
try:
# 嘗試使用微軟雅黑
font = ImageFont.truetype("msyh.ttc", font_size)
except:
font = ImageFont.load_default()
# default font doesn't scale well, but fallback is needed
# If we really want size, we might need a standard font file distributed with app
# Trying basic arial if msyh fails
try:
font = ImageFont.truetype("arial.ttf", font_size)
except:
pass # Fallback to default
# 計(jì)算文本大小
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# 創(chuàng)建單個(gè)水印圖片用于旋轉(zhuǎn)
# 留出足夠空間以防旋轉(zhuǎn)后被裁剪
max_dim = int(math.sqrt(text_width**2 + text_height**2))
txt_img = Image.new('RGBA', (max_dim, max_dim), (0, 0, 0, 0))
txt_draw = ImageDraw.Draw(txt_img)
# 居中繪制文本
text_x = (max_dim - text_width) // 2
text_y = (max_dim - text_height) // 2
# 設(shè)置顏色和透明度
fill_color = (color[0], color[1], color[2], int(255 * opacity))
txt_draw.text((text_x, text_y), text, font=font, fill=fill_color)
# 旋轉(zhuǎn)
if rotation != 0:
txt_img = txt_img.rotate(rotation, resample=Image.BICUBIC)
# 獲取旋轉(zhuǎn)后的實(shí)際內(nèi)容邊界(可選,但為了精確布局最好做)
# 這里簡單處理,直接使用txt_img
w_width, w_height = txt_img.size
if position == '平鋪 (Tile)':
# 平鋪邏輯
# spacing 是間距倍數(shù)或像素
step_x = int(w_width + spacing)
step_y = int(w_height + spacing)
if step_x <= 0: step_x = w_width + 50
if step_y <= 0: step_y = w_height + 50
for y in range(0, img.height, step_y):
for x in range(0, img.width, step_x):
watermark.paste(txt_img, (x, y), txt_img)
else:
# 單個(gè)位置邏輯
pos_x = 0
pos_y = 0
margin = 20
if '左' in position:
pos_x = margin
elif '右' in position:
pos_x = img.width - w_width - margin
else: # 中 (水平)
pos_x = (img.width - w_width) // 2
if '上' in position:
pos_y = margin
elif '下' in position:
pos_y = img.height - w_height - margin
else: # 中 (垂直)
pos_y = (img.height - w_height) // 2
watermark.paste(txt_img, (pos_x, pos_y), txt_img)
# 合成
out = Image.alpha_composite(img, watermark)
# 保存
filename = os.path.basename(file_path)
save_path = os.path.join(self.output_dir, filename)
# Convert back to RGB if saving as JPEG, otherwise keep RGBA for PNG
if filename.lower().endswith(('.jpg', '.jpeg')):
out = out.convert('RGB')
out.save(save_path, quality=95)
else:
out.save(save_path)
except Exception as e:
print(f"Processing failed for {file_path}: {e}")
raise e
class WatermarkApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("批量圖片水印工具")
self.resize(1000, 700)
# Data
self.image_files = []
self.current_preview_image = None
self.watermark_color = (255, 255, 255) # Default white
# UI Setup
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# Left Panel (Settings)
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_panel.setFixedWidth(350)
main_layout.addWidget(left_panel)
# 1. File Selection
grp_files = QGroupBox("文件選擇")
grp_files_layout = QVBoxLayout()
btn_layout = QHBoxLayout()
self.btn_add_files = QPushButton("添加圖片")
self.btn_add_files.clicked.connect(self.add_files)
self.btn_add_folder = QPushButton("添加文件夾")
self.btn_add_folder.clicked.connect(self.add_folder)
self.btn_clear_files = QPushButton("清空列表")
self.btn_clear_files.clicked.connect(self.clear_files)
btn_layout.addWidget(self.btn_add_files)
btn_layout.addWidget(self.btn_add_folder)
btn_layout.addWidget(self.btn_clear_files)
self.list_files = QListWidget()
self.list_files.currentRowChanged.connect(self.update_preview)
grp_files_layout.addLayout(btn_layout)
grp_files_layout.addWidget(self.list_files)
grp_files.setLayout(grp_files_layout)
left_layout.addWidget(grp_files)
# 2. Output Directory
grp_output = QGroupBox("輸出設(shè)置")
grp_output_layout = QVBoxLayout()
out_path_layout = QHBoxLayout()
self.edit_output = QLineEdit()
self.edit_output.setPlaceholderText("選擇輸出目錄...")
self.btn_browse_output = QPushButton("瀏覽")
self.btn_browse_output.clicked.connect(self.browse_output)
out_path_layout.addWidget(self.edit_output)
out_path_layout.addWidget(self.btn_browse_output)
grp_output_layout.addLayout(out_path_layout)
grp_output.setLayout(grp_output_layout)
left_layout.addWidget(grp_output)
# 3. Watermark Settings
grp_settings = QGroupBox("水印設(shè)置")
grp_settings_layout = QVBoxLayout()
# Text
self.edit_text = QLineEdit("Sample Watermark")
self.edit_text.setPlaceholderText("輸入水印文字")
self.edit_text.textChanged.connect(self.update_preview_delayed)
grp_settings_layout.addWidget(QLabel("水印文字:"))
grp_settings_layout.addWidget(self.edit_text)
# Color
color_layout = QHBoxLayout()
self.btn_color = QPushButton("選擇顏色")
self.btn_color.clicked.connect(self.choose_color)
self.lbl_color_preview = QLabel(" ")
self.lbl_color_preview.setStyleSheet("background-color: white; border: 1px solid black;")
self.lbl_color_preview.setFixedWidth(30)
color_layout.addWidget(QLabel("顏色:"))
color_layout.addWidget(self.btn_color)
color_layout.addWidget(self.lbl_color_preview)
color_layout.addStretch()
grp_settings_layout.addLayout(color_layout)
# Font Size
size_layout = QHBoxLayout()
self.spin_size = QSpinBox()
self.spin_size.setRange(10, 500)
self.spin_size.setValue(40)
self.spin_size.valueChanged.connect(self.update_preview_delayed)
size_layout.addWidget(QLabel("字體大小:"))
size_layout.addWidget(self.spin_size)
grp_settings_layout.addLayout(size_layout)
# Opacity
opacity_layout = QHBoxLayout()
self.slider_opacity = QSlider(Qt.Horizontal)
self.slider_opacity.setRange(0, 100)
self.slider_opacity.setValue(50)
self.slider_opacity.valueChanged.connect(self.update_preview_delayed)
opacity_layout.addWidget(QLabel("透明度:"))
opacity_layout.addWidget(self.slider_opacity)
grp_settings_layout.addLayout(opacity_layout)
# Rotation
rotation_layout = QHBoxLayout()
self.slider_rotation = QSlider(Qt.Horizontal)
self.slider_rotation.setRange(0, 360)
self.slider_rotation.setValue(0)
self.slider_rotation.valueChanged.connect(self.update_preview_delayed)
rotation_layout.addWidget(QLabel("旋轉(zhuǎn)角度:"))
rotation_layout.addWidget(self.slider_rotation)
grp_settings_layout.addLayout(rotation_layout)
# Position
pos_layout = QHBoxLayout()
self.combo_pos = QComboBox()
positions = [
"左上", "中上", "右上",
"左中", "正中", "右中",
"左下", "中下", "右下",
"平鋪 (Tile)"
]
self.combo_pos.addItems(positions)
self.combo_pos.setCurrentText("右下")
self.combo_pos.currentIndexChanged.connect(self.update_preview_delayed)
pos_layout.addWidget(QLabel("位置:"))
pos_layout.addWidget(self.combo_pos)
grp_settings_layout.addLayout(pos_layout)
# Spacing (only for tile)
spacing_layout = QHBoxLayout()
self.spin_spacing = QSpinBox()
self.spin_spacing.setRange(0, 500)
self.spin_spacing.setValue(100)
self.spin_spacing.valueChanged.connect(self.update_preview_delayed)
spacing_layout.addWidget(QLabel("間距 (平鋪):"))
spacing_layout.addWidget(self.spin_spacing)
grp_settings_layout.addLayout(spacing_layout)
grp_settings.setLayout(grp_settings_layout)
left_layout.addWidget(grp_settings)
left_layout.addStretch()
# Action Buttons
self.btn_start = QPushButton("開始處理")
self.btn_start.setMinimumHeight(40)
self.btn_start.setStyleSheet("font-weight: bold; font-size: 14px;")
self.btn_start.clicked.connect(self.start_processing)
left_layout.addWidget(self.btn_start)
self.progress_bar = QProgressBar()
left_layout.addWidget(self.progress_bar)
# Right Panel (Preview)
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
main_layout.addWidget(right_panel)
right_layout.addWidget(QLabel("預(yù)覽 (點(diǎn)擊文件列表查看):"))
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.lbl_preview = QLabel()
self.lbl_preview.setAlignment(Qt.AlignCenter)
self.scroll_area.setWidget(self.lbl_preview)
right_layout.addWidget(self.scroll_area)
# Debounce timer for preview update to avoid lag
self.preview_timer = None
def add_files(self):
files, _ = QFileDialog.getOpenFileNames(self, "選擇圖片", "", "Images (*.png *.jpg *.jpeg *.bmp)")
if files:
self.image_files.extend(files)
self.update_file_list()
if not self.edit_output.text():
self.edit_output.setText(os.path.dirname(files[0]) + "/watermarked")
def add_folder(self):
folder = QFileDialog.getExistingDirectory(self, "選擇文件夾")
if folder:
for root, dirs, files in os.walk(folder):
for file in files:
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
self.image_files.append(os.path.join(root, file))
self.update_file_list()
if not self.edit_output.text():
self.edit_output.setText(folder + "/watermarked")
def clear_files(self):
self.image_files = []
self.update_file_list()
self.lbl_preview.clear()
def update_file_list(self):
self.list_files.clear()
for f in self.image_files:
self.list_files.addItem(os.path.basename(f))
if self.image_files:
self.list_files.setCurrentRow(0)
def browse_output(self):
folder = QFileDialog.getExistingDirectory(self, "選擇輸出目錄")
if folder:
self.edit_output.setText(folder)
def choose_color(self):
color = QColorDialog.getColor()
if color.isValid():
self.watermark_color = (color.red(), color.green(), color.blue())
self.lbl_color_preview.setStyleSheet(f"background-color: {color.name()}; border: 1px solid black;")
self.update_preview()
def update_preview_delayed(self):
# In a real app, use a QTimer to debounce.
# For simplicity here, just call update_preview directly,
# but keep method name to indicate intent if we add timer later.
self.update_preview()
def update_preview(self):
row = self.list_files.currentRow()
if row < 0 or row >= len(self.image_files):
return
file_path = self.image_files[row]
# Generate preview
try:
config = self.get_config()
# Use PIL to generate preview
with Image.open(file_path).convert("RGBA") as img:
# Resize for preview if too large
preview_max_size = 800
if img.width > preview_max_size or img.height > preview_max_size:
img.thumbnail((preview_max_size, preview_max_size))
# Apply watermark (Reuse logic? For now duplicate simplified logic for preview speed)
watermark = Image.new('RGBA', img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(watermark)
font_size = config['font_size']
# Scale font size relative to preview thumbnail
# Note: config['font_size'] is for the original image?
# Ideally we should scale it down. But font size is usually absolute pixels.
# If we scaled down the image, the font will look HUGE if we don't scale it too.
# So we need to know the original image size vs preview size.
# Let's read original size first
with Image.open(file_path) as orig_img:
orig_w, orig_h = orig_img.size
scale_factor = img.width / orig_w
scaled_font_size = int(font_size * scale_factor)
if scaled_font_size < 1: scaled_font_size = 1
try:
font = ImageFont.truetype("msyh.ttc", scaled_font_size)
except:
font = ImageFont.load_default()
try:
font = ImageFont.truetype("arial.ttf", scaled_font_size)
except:
pass
text = config['text']
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
max_dim = int(math.sqrt(text_width**2 + text_height**2))
txt_img = Image.new('RGBA', (max_dim, max_dim), (0, 0, 0, 0))
txt_draw = ImageDraw.Draw(txt_img)
text_x = (max_dim - text_width) // 2
text_y = (max_dim - text_height) // 2
color = config['color']
opacity = config['opacity']
fill_color = (color[0], color[1], color[2], int(255 * opacity))
txt_draw.text((text_x, text_y), text, font=font, fill=fill_color)
if config['rotation'] != 0:
txt_img = txt_img.rotate(config['rotation'], resample=Image.BICUBIC)
w_width, w_height = txt_img.size
if config['position'] == '平鋪 (Tile)':
scaled_spacing = int(config['spacing'] * scale_factor)
step_x = int(w_width + scaled_spacing)
step_y = int(w_height + scaled_spacing)
if step_x <= 0: step_x = w_width + 10
if step_y <= 0: step_y = w_height + 10
for y in range(0, img.height, step_y):
for x in range(0, img.width, step_x):
watermark.paste(txt_img, (x, y), txt_img)
else:
pos_x = 0
pos_y = 0
margin = int(20 * scale_factor)
position = config['position']
if '左' in position: pos_x = margin
elif '右' in position: pos_x = img.width - w_width - margin
else: pos_x = (img.width - w_width) // 2
if '上' in position: pos_y = margin
elif '下' in position: pos_y = img.height - w_height - margin
else: pos_y = (img.height - w_height) // 2
watermark.paste(txt_img, (pos_x, pos_y), txt_img)
out = Image.alpha_composite(img, watermark)
# Convert to QPixmap
if out.mode == "RGBA":
r, g, b, a = out.split()
out = Image.merge("RGBA", (b, g, r, a))
elif out.mode == "RGB":
r, g, b = out.split()
out = Image.merge("RGB", (b, g, r))
im2 = out.convert("RGBA")
data = im2.tobytes("raw", "RGBA")
qim = QImage(data, out.size[0], out.size[1], QImage.Format_ARGB32)
pixmap = QPixmap.fromImage(qim)
self.lbl_preview.setPixmap(pixmap)
except Exception as e:
print(f"Preview error: {e}")
def get_config(self):
return {
'text': self.edit_text.text(),
'font_size': self.spin_size.value(),
'opacity': self.slider_opacity.value() / 100.0,
'rotation': self.slider_rotation.value(),
'color': self.watermark_color,
'position': self.combo_pos.currentText(),
'spacing': self.spin_spacing.value()
}
def start_processing(self):
if not self.image_files:
QMessageBox.warning(self, "提示", "請(qǐng)先添加圖片!")
return
output_dir = self.edit_output.text()
if not output_dir:
QMessageBox.warning(self, "提示", "請(qǐng)選擇輸出目錄!")
return
self.btn_start.setEnabled(False)
self.progress_bar.setValue(0)
config = self.get_config()
self.worker = Worker(self.image_files, output_dir, config)
self.worker.progress.connect(self.progress_bar.setValue)
self.worker.finished.connect(self.processing_finished)
self.worker.error.connect(self.processing_error)
self.worker.start()
def processing_finished(self, msg):
self.btn_start.setEnabled(True)
QMessageBox.information(self, "完成", msg)
def processing_error(self, msg):
self.btn_start.setEnabled(True)
QMessageBox.critical(self, "錯(cuò)誤", msg)
if __name__ == "__main__":
app = QApplication(sys.argv)
# 設(shè)置全局字體,看起來更現(xiàn)代一點(diǎn)
font = QFont("Microsoft YaHei", 9)
app.setFont(font)
window = WatermarkApp()
window.show()
sys.exit(app.exec_())
總結(jié)
通過不到 400 行代碼,我們結(jié)合了 PyQt5 的交互能力和 Pillow 的圖像處理能力,開發(fā)出了一個(gè)實(shí)用的桌面工具。這個(gè)項(xiàng)目很好的展示了 Python 在自動(dòng)化辦公和工具開發(fā)領(lǐng)域的優(yōu)勢。
擴(kuò)展思路:
- 支持圖片水印(Logo)。
- 保存/加載配置模板,方便下次直接使用。
- 打包成 exe 文件(使用
pyinstaller),方便分享給沒有安裝 Python 的同事使用。
以上就是Python編寫一個(gè)圖片批量添加文字水印工具(附代碼)的詳細(xì)內(nèi)容,更多關(guān)于Python圖片批量添加文字水印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python游戲開發(fā)之魔塔小游戲的實(shí)現(xiàn)
魔塔小游戲作為一款角色扮演RPG小游戲,一直深受大家的喜愛。本文將利用Python的cpgames模塊制作這一經(jīng)典小游戲,感興趣的可以跟隨小編一起動(dòng)手試一試2022-02-02
Python與C++中梯度方向直方圖的實(shí)現(xiàn)
在學(xué)習(xí)HOG特征的時(shí)候,發(fā)現(xiàn)一片英文文章講得淺顯易懂。因此翻譯在這里學(xué)習(xí),感興趣的朋友快來看看吧2022-03-03
python實(shí)現(xiàn)堆棧與隊(duì)列的方法
這篇文章主要介紹了python實(shí)現(xiàn)堆棧與隊(duì)列的方法,包含了堆棧與隊(duì)列的定義方法及常用操作,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
python實(shí)現(xiàn)輸入的數(shù)據(jù)在地圖上生成熱力圖效果
今天小編就為大家分享一篇python實(shí)現(xiàn)輸入的數(shù)據(jù)在地圖上生成熱力圖效果,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-12-12
python Popen 獲取輸出,等待運(yùn)行完成示例
今天小編就為大家分享一篇python Popen 獲取輸出,等待運(yùn)行完成示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-12-12
Python快速進(jìn)修指南之向量數(shù)據(jù)庫文本搜索
這篇文章主要為大家介紹了Java開發(fā)快速進(jìn)修Python指南之向量數(shù)據(jù)庫文本搜索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
python面向?qū)ο蟀鎸W(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了python面向?qū)ο蟀鎸W(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06

