237 lines
8.7 KiB
Python
237 lines
8.7 KiB
Python
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
|