commit 9a098f6c7fa0a928bab7ca4f8887261133c8b95b Author: ypshidifu Date: Fri Sep 18 20:43:59 2026 +0800 初始化图像水印CLI工具 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64bbd98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +*.pyo +output/ +*.db +processed_count +.env +.venv/ +venv/ diff --git a/config.py b/config.py new file mode 100644 index 0000000..302c0d4 --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +import os + +WORK_DIR = os.path.dirname(os.path.abspath(__file__)) +OUTPUT_DIR = os.path.join(WORK_DIR, "output") +STATS_FILE = os.path.join(WORK_DIR, ".processed_count") + +os.makedirs(OUTPUT_DIR, exist_ok=True) + +SUPPORTED_INPUT_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"} +SUPPORTED_OUTPUT_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"} diff --git a/main.py b/main.py new file mode 100644 index 0000000..30a1bcb --- /dev/null +++ b/main.py @@ -0,0 +1,116 @@ +import os +import sys +from watermark import watermark_image, increment_count, load_font +from config import OUTPUT_DIR, SUPPORTED_INPUT_EXTENSIONS, WORK_DIR + + +def print_help(): + help_text = """ +用法: + "图片路径" "水印内容" 加水印,保持原格式输出 + "图片路径>输出格式" "水印内容" 加水印并转换格式(格式可选:jpg/png/bmp/gif/tiff/webp) + /exit 退出程序 + /get 查询已处理图片总数 + +示例: + photo.jpg "版权所有" + photo.jpg>PNG "2026" +""" + print(help_text.strip()) + + +def print_stats(): + count = 0 + if os.path.exists(os.path.join(WORK_DIR, ".processed_count")): + with open(os.path.join(WORK_DIR, ".processed_count"), "r", encoding="utf-8") as f: + try: + count = int(f.read().strip()) + except ValueError: + pass + print(f"当前已处理图片数量: {count}") + + +def process_single(image_arg, text): + if ">" not in image_arg: + # 无格式转换,保持原格式 + output_ext = None + else: + parts = image_arg.rsplit(">", 1) + image_arg, out_ext = parts + out_ext = out_ext.strip().lstrip(".") + if out_ext.lower() not in SUPPORTED_INPUT_EXTENSIONS: + print(f"不支持的输出格式: .{out_ext},支持的格式: {', '.join(sorted(SUPPORTED_INPUT_EXTENSIONS))}") + return + output_ext = f".{out_ext.lower()}" + + input_path = image_arg.strip() + if not os.path.isfile(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))}") + return + + output_path = watermark_image(input_path, text, output_ext=output_ext) + total = increment_count() + print(f"✅ 已保存: {output_path}") + print(f"📊 累计处理图片: {total} 张") + + +def main(): + if len(sys.argv) >= 3: + # 非交互模式:直接处理一张图片 + process_single(sys.argv[1], sys.argv[2]) + return + + print("========================================") + print(" 图像水印工具 v1.0") + print(" 输入 /help 查看帮助") + print("========================================") + while True: + try: + cmd = input("\n> ").strip() + except (EOFError, KeyboardInterrupt): + print("\n再见!") + break + + if not cmd: + continue + + if cmd == "/exit" or cmd == "exit": + print("再见!") + break + elif cmd == "/get": + print_stats() + elif cmd == "/help": + print_help() + elif ">" in cmd: + # 格式转换模式: "图片>格式 水印内容" + parts = cmd.split(">", 1) + image_part = parts[0].strip() + rest = parts[1].strip() + if not rest: + print("缺少水印内容,请重试。输入 /help 查看帮助。") + continue + # 找出 ">格式 " 后面的水印文本(用 ">格式 " 分割后取第二部分) + if " " in rest: + final_format, text = rest.split(" ", 1) + full_arg = f"{image_part}>{final_format}" + else: + print("格式参数后缺少水印内容,请重试。输入 /help 查看帮助。") + continue + process_single(full_arg, text) + else: + # 普通模式: "图片路径 水印内容" + parts = cmd.split(None, 1) + if len(parts) < 2: + print("格式错误,需要提供图片路径和水印内容。输入 /help 查看帮助。") + continue + image_arg, text = parts + process_single(image_arg, text) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c7e63dd --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pillow>=10.0.0 diff --git a/watermark.py b/watermark.py new file mode 100644 index 0000000..80bd441 --- /dev/null +++ b/watermark.py @@ -0,0 +1,99 @@ +import os +from PIL import Image, ImageDraw, ImageFont +from config import OUTPUT_DIR, SUPPORTED_INPUT_EXTENSIONS, SUPPORTED_OUTPUT_EXTENSIONS + + +def load_font(size=40): + """根据系统加载合适的字体文件。""" + 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 watermark_image(input_path, text, output_path=None, output_ext=None): + """ + 给图片添加水印并保存。 + + Args: + input_path: 输入图片路径 + text: 水印文字内容 + output_path: 输出路径(不含扩展名),默认在 output/ 目录 + output_ext: 指定输出格式扩展名,如 ".png"、".jpg";为 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] + + x = img.width - tw - 20 + y = img.height - th - 20 + + # 半透明背景 + 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) + + # 合成 + 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() + else: + output_ext = output_ext.lower() + if output_ext not in SUPPORTED_OUTPUT_EXTENSIONS: + raise ValueError(f"不支持的输出格式: {output_ext},支持的格式: {SUPPORTED_OUTPUT_EXTENSIONS}") + + if output_path is None: + 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) + 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