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