Files
imgwatermark/watermark.py
T

237 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from PIL import Image, ImageDraw, ImageFont
from config import SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS, STATS_FILE, WORK_DIR
def load_font(size=16):
"""根据系统加载合适的字体文件。"""
candidates = [
r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\simhei.ttf",
r"C:\Windows\Fonts\simsun.ttc",
r"C:\Windows\Fonts\arial.ttf",
]
for path in candidates:
if os.path.exists(path):
return ImageFont.truetype(path, size)
return ImageFont.load_default()
def _normalize_path(path):
"""规范化路径,统一使用正斜杠,解析相对路径。"""
# 统一将反斜杠转为正斜杠
path = path.replace("\\", "/")
# 规范化相对路径
path = os.path.normpath(path)
return path
def _strip_quotes(s):
"""去掉首尾的引号。"""
s = s.strip()
if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
return s[1:-1]
return s
def _parse_output_spec(output_spec, input_path):
"""
解析输出规格,返回 (output_path, output_ext)。
支持格式:
- ".png" / "png" -> 使用原目录,转换格式(保留原名)
- "./output.png" -> 输出到脚本所在目录
- "../output.png" -> 输出到脚本上级目录
- "/absolute/path.png" -> 绝对路径
- "output.png" -> 输出到脚本所在目录
- "./1/.bmp" -> 输出到 ./1/ 目录,转换为 bmp 格式
"""
if not output_spec:
# 无格式指定,保持原格式和原目录
input_dir = os.path.dirname(os.path.abspath(input_path))
base_name = os.path.basename(input_path)
name_no_ext = os.path.splitext(base_name)[0]
_, ext = os.path.splitext(input_path)
return os.path.join(input_dir, f"{name_no_ext}_watermarked{ext.lower()}"), ext.lower()
output_spec = _strip_quotes(output_spec.strip())
# 先检测纯格式(在 normpath 之前,避免信息丢失)
is_just_format = False
ext = None
# 处理以 / 开头的纯格式,如 /.png、/.jpg
if output_spec.startswith('/'):
suffix = output_spec[1:] # 去掉开头的 /
if suffix.startswith('.') and len(suffix) <= 6 and suffix[1:].isalpha():
is_just_format = True
ext = suffix
# 处理以 ./ 开头的纯格式,如 ./.bmp、./.png
elif output_spec.startswith('./'):
suffix = output_spec[2:] # 去掉开头的 ./
if suffix.startswith('.') and len(suffix) <= 6 and suffix[1:].isalpha():
is_just_format = True
ext = suffix # 直接使用,不要再加点
# 处理以 . 开头的纯格式,如 .png、.jpg
elif output_spec.startswith('.') and len(output_spec) <= 6 and output_spec[1:].isalpha():
is_just_format = True
ext = '.' + output_spec[1:]
# 处理无点的纯格式,如 png、jpg
elif not output_spec.startswith('.') and len(output_spec) <= 5 and output_spec.isalpha():
is_just_format = True
ext = '.' + output_spec
if is_just_format:
# 纯格式转换,输出到原图片所在目录,保留原名加后缀
input_dir = os.path.dirname(os.path.abspath(input_path))
base_name = os.path.basename(input_path)
name_no_ext = os.path.splitext(base_name)[0]
output_path = os.path.join(input_dir, f"{name_no_ext}_watermarked{ext}")
return _normalize_path(output_path), ext
# 特殊处理:./目录/.格式 的语法(如 ./1/.bmp)
# 格式:./[目录]/.[扩展名]
# 例如:./1/.bmp -> 输出到 ./1/ 目录,格式为 bmp
if output_spec.startswith('./') and '/.' in output_spec:
# 提取目录和扩展名
# output_spec 如 ./1/.bmp
parts = output_spec.split('/.', 1)
if len(parts) == 2:
dir_part = parts[0] # ./1
ext = '.' + parts[1] # .bmp
if ext not in SUPPORTED_OUTPUT_EXTENSIONS:
raise ValueError(f"不支持的输出格式: {ext},支持的格式: {', '.join(sorted(SUPPORTED_OUTPUT_EXTENSIONS))}")
# 规范化目录路径
dir_part = _normalize_path(dir_part)
# 获取原文件名(不含扩展名)
base_name = os.path.basename(input_path)
name_no_ext = os.path.splitext(base_name)[0]
# 构建输出路径
output_path = os.path.join(WORK_DIR, dir_part, f"{name_no_ext}{ext}")
return _normalize_path(output_path), ext
# 普通路径处理
output_spec = _normalize_path(output_spec)
# 分离目录和文件名
dir_part = os.path.dirname(output_spec)
file_part = os.path.basename(output_spec)
# 分离文件名和扩展名
name_no_ext, ext = os.path.splitext(file_part)
# 如果没有扩展名,使用原图片的扩展名
if not ext:
_, orig_ext = os.path.splitext(input_path)
ext = orig_ext.lower() if orig_ext else ".png"
else:
ext = ext.lower()
if ext not in SUPPORTED_OUTPUT_EXTENSIONS:
raise ValueError(f"不支持的输出格式: {ext},支持的格式: {', '.join(sorted(SUPPORTED_OUTPUT_EXTENSIONS))}")
# 确定输出路径
if dir_part:
# 有路径,使用指定路径
output_path = os.path.join(dir_part, file_part)
else:
# 只有文件名,没有路径,输出到脚本所在目录
output_path = os.path.join(WORK_DIR, file_part)
return _normalize_path(output_path), ext
def watermark_image_tiled(input_path, text, output_path=None, output_ext=None):
"""
给图片添加平铺水印(小字+全屏,类似身份证水印效果)。
Args:
input_path: 输入图片路径
text: 水印文字内容
output_path: 输出路径,默认在同目录生成 _watermarked 后缀文件
output_ext: 指定输出格式(如 ".png"),None 则保持原格式
Returns:
最终保存的绝对路径
"""
img = Image.open(input_path).convert("RGBA")
# 创建平铺水印图层
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# 小字字体(参考微信水印风格,约10px)
font = load_font(10)
# 计算水印间距(更密集的平铺,类似微信截图水印)
spacing_x = max(120, img.width // 3)
spacing_y = max(80, img.height // 2)
# 准备斜向水印:创建一个足够大的透明图层,旋转后绘制
# 旋转45度,图层需要更大以覆盖整个图片
angle = 45
diag = int((img.size[0]**2 + img.size[1]**2)**0.5)
rotated_overlay = Image.new("RGBA", (diag * 2, diag * 2), (0, 0, 0, 0))
r_draw = ImageDraw.Draw(rotated_overlay)
# 深色文字,透明度约85%(提高可见性)
water_color = (40, 40, 40, 220)
# 绘制平铺水印(在大图层上平铺,然后旋转)
for y in range(-diag, diag * 2, spacing_y):
for x in range(-diag, diag * 2, spacing_x):
r_draw.text((x, y), text, fill=water_color, font=font)
# 旋转图层
rotated_overlay = rotated_overlay.rotate(angle, resample=Image.BICUBIC, expand=False)
# 计算偏移量,将旋转后的图层对齐到原图
offset_x = (img.size[0] - diag * 2) // 2
offset_y = (img.size[1] - diag * 2) // 2
# 粘贴到正确位置
overlay.paste(rotated_overlay, (offset_x, offset_y), rotated_overlay)
# 合成水印
result = Image.alpha_composite(img, overlay)
result = result.convert("RGB")
# 确定输出格式和路径
if output_ext is None:
_, output_ext = os.path.splitext(input_path)
output_ext = output_ext.lower()
else:
output_ext = output_ext.lower()
if output_path is None:
input_dir = os.path.dirname(os.path.abspath(input_path))
base_name = os.path.basename(input_path)
name_no_ext = os.path.splitext(base_name)[0]
output_path = os.path.join(input_dir, f"{name_no_ext}_watermarked{output_ext}")
elif not output_path.endswith(output_ext):
output_path = output_path + output_ext
# 保存
save_kwargs = {}
if output_ext in (".jpg", ".jpeg"):
save_kwargs["quality"] = 95
save_kwargs["optimize"] = True
elif output_ext == ".png":
save_kwargs["optimize"] = True
result.save(output_path, **save_kwargs)
return os.path.abspath(output_path)
def increment_count():
"""已处理图片计数 +1,返回新总数。"""
count = 0
if os.path.exists(STATS_FILE):
with open(STATS_FILE, "r", encoding="utf-8") as f:
try:
count = int(f.read().strip())
except ValueError:
count = 0
count += 1
with open(STATS_FILE, "w", encoding="utf-8") as f:
f.write(str(count))
return count