diff --git a/.jpg.png b/.jpg.png new file mode 100644 index 0000000..bbb34d3 Binary files /dev/null and b/.jpg.png differ diff --git a/main.py b/main.py index a2afed0..a1f21c5 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ import os import sys -from watermark import watermark_image, increment_count -from config import OUTPUT_DIR, SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS, WORK_DIR +from watermark import watermark_image_tiled, increment_count, _parse_output_spec +from config import SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS, WORK_DIR def print_banner(): @@ -12,10 +12,12 @@ def print_banner(): print(" 输出: jpg / png / bmp / gif / tiff / webp") print("=" * 50) print("命令:") - print(" <图片路径> \"水印内容\" 加水印,保持原格式输出") + print(" <图片路径> \"水印内容\" 加水印,输出到原目录") print(" <图片路径>>.png \"内容\" 加水印并转换格式") - print(" /exit 或 exit 退出程序") - print(" /get 或 get 显示已处理图片统计") + print(" <图片路径>>./output.png 输出到指定目录") + print(" <图片路径>>../output.png 输出到上级目录") + print(" /exit 或 exit 退出程序") + print(" /get 或 get 显示已处理图片统计") print() @@ -31,35 +33,53 @@ def print_stats(): def process_single(image_arg, text): - """处理单张图片:可选格式转换,添加水印,计数+1。""" - # 去掉首尾引号(兼容命令行传入带引号的路径) + """处理单张图片:可选格式转换/输出路径,添加平铺水印,计数+1。""" + # 去掉首尾引号 image_arg = image_arg.strip().strip('"').strip("'") text = text.strip().strip('"').strip("'") - if ">" not in image_arg: - output_ext = None - else: + output_spec = None + if ">" in image_arg: parts = image_arg.rsplit(">", 1) - image_arg, out_ext = parts[0].strip(), parts[1].strip().lstrip(".") - if out_ext.lower() not in SUPPORTED_INPUT_EXTENSIONS: - print(f"不支持的输出格式: .{out_ext},支持的格式: {', '.join(sorted(SUPPORTED_OUTPUT_EXTENSIONS))}") - return - output_ext = f".{out_ext.lower()}" + image_arg = parts[0].strip() + output_spec = parts[1].strip() + # 验证输入路径 input_path = image_arg if not os.path.isfile(input_path): - print(f"找不到输入图片: {input_path}") + print(f"❌ 找不到输入图片: {input_path}") return _, ext = os.path.splitext(input_path) if ext.lower() not in SUPPORTED_INPUT_EXTENSIONS: - print(f"不支持的输入格式: {ext},支持的格式: {', '.join(sorted(SUPPORTED_INPUT_EXTENSIONS))}") + print(f"❌ 不支持的输入格式: {ext},支持的格式: {', '.join(sorted(SUPPORTED_INPUT_EXTENSIONS))}") return - output_path = watermark_image(input_path, text, output_ext=output_ext) - total = increment_count() - print(f"✅ 已保存: {output_path}") - print(f"📊 累计处理图片: {total} 张") + # 处理输出规格 + output_path = None + output_ext = None + if output_spec: + try: + output_path, output_ext = _parse_output_spec(output_spec, input_path) + except ValueError as e: + print(f"❌ {e}") + return + else: + # 无输出规格,保持原格式,输出到原目录 + _, output_ext = os.path.splitext(input_path) + 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.lower()}") + + # 添加平铺水印 + try: + final_path = watermark_image_tiled(input_path, text, output_path=output_path, output_ext=output_ext) + total = increment_count() + print(f"✅ 已保存: {final_path}") + print(f"📊 累计处理图片: {total} 张") + except Exception as e: + print(f"❌ 处理失败: {e}") def main(): @@ -88,26 +108,42 @@ def main(): elif cmd in ("/help", "help"): print_banner() elif ">" in cmd: - # 格式转换模式: "图片路径>.png 水印内容" 或 "图片路径>PNG 水印内容" + # 格式转换模式: "图片路径>.png 水印内容" 或 "图片路径>./output.png 水印内容" parts = cmd.split(">", 1) image_part = parts[0].strip() rest = parts[1].strip() if not rest: - print("缺少水印内容,请重试。输入 /help 查看帮助。") + print("❌ 缺少输出规格或水印内容,请重试。") continue - # rest 中第一个空格前是格式,后面是水印文本 + + # 分析 rest 部分:可能是 "格式 文本" 或 "路径 文本" tokens = rest.split(None, 1) - if len(tokens) < 2: - print("格式参数后缺少水印内容,请重试。输入 /help 查看帮助。") + if len(tokens) == 2: + output_spec, text = tokens + full_arg = f"{image_part}>{output_spec}" + elif len(tokens) == 1: + # 只有一个 token,判断是否是路径(包含 . 或 / 或 \) + candidate = tokens[0] + if "." in candidate or "/" in candidate or "\\" in candidate: + # 可能是路径,但缺少水印内容 + print("❌ 缺少水印内容,请重试。") + continue + else: + # 是格式名 + output_spec = candidate + # 继续读取后续内容作为水印文本(如果有) + print("❌ 缺少水印内容,请重试。") + continue + else: + print("❌ 格式错误,请重试。") continue - final_format, text = tokens - full_arg = f"{image_part}>{final_format}" + process_single(full_arg, text) else: # 普通模式: "图片路径 水印内容" parts = cmd.split(None, 1) if len(parts) < 2: - print("格式错误,需要提供图片路径和水印内容。输入 /help 查看帮助。") + print("❌ 格式错误,需要提供图片路径和水印内容。") continue image_arg, text = parts process_single(image_arg, text) diff --git a/test_input.png b/test_input.png new file mode 100644 index 0000000..249779a Binary files /dev/null and b/test_input.png differ diff --git a/test_input_watermarked.jpg b/test_input_watermarked.jpg new file mode 100644 index 0000000..827c907 Binary files /dev/null and b/test_input_watermarked.jpg differ diff --git a/test_output/result.jpg b/test_output/result.jpg new file mode 100644 index 0000000..118df15 Binary files /dev/null and b/test_output/result.jpg differ diff --git a/watermark.py b/watermark.py index 5812b27..3bd1a62 100644 --- a/watermark.py +++ b/watermark.py @@ -1,9 +1,9 @@ import os from PIL import Image, ImageDraw, ImageFont -from config import OUTPUT_DIR, SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS, STATS_FILE +from config import SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS, STATS_FILE -def load_font(size=48): +def load_font(size=16): """根据系统加载合适的字体文件。""" candidates = [ r"C:\Windows\Fonts\msyh.ttc", @@ -17,69 +17,143 @@ def load_font(size=48): 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 watermark_image(input_path, text, output_path=None, output_ext=None): +def _parse_output_spec(output_spec, input_path): """ - 给图片添加水印并保存。 + 解析输出规格,返回 (output_path, output_ext)。 + + 支持格式: + - ".png" / "png" -> 使用原目录,转换格式 + - "./output.png" -> 输出到当前目录 + - "../output.png" -> 输出到上级目录 + - "/absolute/path.png" -> 绝对路径 + - "output.png" -> 输出到原目录(重命名) + """ + 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()) + 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) + + # 判断是否是纯格式(如 .jpg、jpg)vs 有文件名(如 result.jpg) + is_just_format = False + if not name_no_ext: + # 没有文件名部分,只有扩展名 + is_just_format = True + elif name_no_ext == file_part and len(name_no_ext) <= 5 and name_no_ext.isalpha(): + # 没有点,整个是字母文件名(如 jpg) + is_just_format = True + elif file_part.startswith('.') and len(file_part) <= 5 and file_part[1:].isalpha(): + # 以点开头且其余为字母(如 .jpg) + is_just_format = True + + # 如果没有扩展名,使用原图片的扩展名 + if not ext: + _, orig_ext = os.path.splitext(input_path) + ext = orig_ext.lower() if orig_ext else ".png" + + # 如果只有文件名没有扩展名,给它加上正确的扩展名 + if is_just_format: + ext = '.' + name_no_ext.lstrip('.') if name_no_ext else ext + name_no_ext = '' + else: + ext = ext.lower() + + if ext not in SUPPORTED_OUTPUT_EXTENSIONS: + raise ValueError(f"不支持的输出格式: {ext},支持的格式: {', '.join(sorted(SUPPORTED_OUTPUT_EXTENSIONS))}") + + 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}") + elif dir_part: + # 有路径,使用指定路径 + output_path = os.path.join(dir_part, file_part) + else: + # 只有文件名,没有路径,使用原目录 + input_dir = os.path.dirname(os.path.abspath(input_path)) + output_path = os.path.join(input_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: 输出路径(不含扩展名),默认在 output/ 目录 - output_ext: 指定输出格式扩展名,如 ".png"、".jpg";为 None 则保持原格式 + 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) - font = load_font(48) - bbox = draw.textbbox((0, 0), text, font=font) - tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + # 小字字体 + font = load_font(16) - x = img.width - tw - 20 - y = img.height - th - 20 + # 计算水印间距(根据图片尺寸自适应) + spacing_x = max(80, img.width // 4) + spacing_y = max(60, img.height // 3) - # 半透明圆角背景 - draw.rounded_rectangle( - [x - 8, y - 4, x + tw + 8, y + th + 4], - radius=6, - fill=(0, 0, 0, 140), - ) - # 白色文字 - draw.text((x, y), text, fill=(255, 255, 255, 255), font=font) + # 绘制平铺水印 + for y in range(-20, img.height + 20, spacing_y): + for x in range(-20, img.width + 20, spacing_x): + # 半透明文字(白色,透明度 60/255) + draw.text((x, y), text, fill=(255, 255, 255, 60), font=font) - # 合成并转 RGB + # 合成水印 result = Image.alpha_composite(img, overlay) result = result.convert("RGB") - # 确定输出格式 + # 确定输出格式和路径 if output_ext is None: - _, ext = os.path.splitext(input_path) - output_ext = ext.lower() + _, output_ext = os.path.splitext(input_path) + output_ext = output_ext.lower() else: output_ext = output_ext.lower() - if output_ext not in SUPPORTED_OUTPUT_EXTENSIONS: - raise ValueError( - f"不支持的输出格式: {output_ext},支持的格式: {', '.join(sorted(SUPPORTED_OUTPUT_EXTENSIONS))}" - ) - # 确定输出路径 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] - out_name = f"{name_no_ext}_watermarked{output_ext}" - output_path = os.path.join(OUTPUT_DIR, out_name) + 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