Python批量给图片添加文字或图片水印脚本

Luna
Luna
发布时间:2026-10-30 | 11人浏览过
原创

使用Python的Pillow库编写批量给图片添加文字与图片水印的高效脚本,详细讲解RGBA图层透明混合原理、九宫格坐标计算、自适应缩放与Exif方向校正等实战要点。

在网站运营、内容发布或数字资产归档过程中,为原始图像批量附加专属标识是一项高频需求。使用 Python 进行批量水印处理,核心在于借助成熟的 Pillow(PIL 图像处理库)实现底图解析、Alpha 透明通道混合与自动化目录遍历。本文将提供一套兼顾文字水印与图片水印的生产级脚本,并系统拆解坐标对齐、透明通道渲染、Exif 旋转校正与自适应缩放等关键实现细节。

一、整体处理架构与执行流程

批量水印脚本的核心逻辑并非简单地在原图上涂抹像素,而是建立一个标准化的图像流水线。完整的作业流程包括图片文件筛选、元数据方向矫正、临时 RGBA 透明图层创建、水印绘制与合成、以及最终的色彩空间还原与存储。

Python批量给图片添加文字或图片水印脚本
图1:批量水印脚本的标准化执行管道

二、环境准备与依赖安装

处理图像需要安装 Pillow 库。Pillow 是 Python 原生 PIL 的现代分支,支持绝大多数通用图像格式的读写与矢量绘制:

pip install Pillow

同时需要注意中文字体文件。Windows 系统自带 simhei.ttf(黑体)或 msyh.ttc(微软雅黑);macOS 提供 PingFang.ttc;Linux 服务器推荐提前安装开源字体包,例如 fonts-noto-cjk 或提供本地 .ttf 文件路径,以避免中文渲染为乱码或矩形方块。

三、核心原理:RGBA 模式与透明图层混合

初学者容易直接在原始图像对象上调用 ImageDraw.Draw(img).text()。这种方式存在严重缺陷:如果原图是 RGB 格式,无法为其赋予半透明 Alpha 通道;即使原图是 RGBA,直接在原像素上覆写文字也会导致抗锯齿边缘产生杂色边缘。

标准解法是采用双图层混合策略:创建与底图尺寸完全相同的纯透明图层(全 0 的 RGBA 缓冲区),在该图层指定位置渲染文字或粘贴图片水印,最后调用 Image.alpha_composite() 进行无损 Alpha 合成。

Python批量给图片添加文字或图片水印脚本
图2:基于独立透明缓冲区的 RGBA Alpha 复合运算模型

四、坐标计算与九宫格对齐定位

批量处理面临的最大挑战在于输入图片的分辨率各不相同。硬编码坐标(例如 (100, 100))在 4K 大图上会显得微不足道,在小缩略图上则可能完全溢出。因此需要实现通用的九宫格定位算法与边距控制。

Python批量给图片添加文字或图片水印脚本
图3:九宫格水印定位几何映射与边界边距模型

五、完整代码实现

下面是一个封装完备的 Python 脚本,支持文字水印与图片水印两种模式,具备 Exif 方向自动翻转、自适应缩放、多格式批量输出能力。

import os
import sys
from PIL import Image, ImageDraw, ImageFont, ImageOps

def calculate_position(base_size, mark_size, position="bottom-right", margin=20):
    """
    计算水印在底图上的左上角放置坐标 (x, y)
    """
    bw, bh = base_size
    mw, mh = mark_size

    pos_map = {
        "top-left": (margin, margin),
        "top-center": ((bw - mw) // 2, margin),
        "top-right": (bw - mw - margin, margin),
        "center-left": (margin, (bh - mh) // 2),
        "center": ((bw - mw) // 2, (bh - mh) // 2),
        "center-right": (bw - mw - margin, (bh - mh) // 2),
        "bottom-left": (margin, bh - mh - margin),
        "bottom-center": ((bw - mw) // 2, bh - mh - margin),
        "bottom-right": (bw - mw - margin, bh - mh - margin)
    }
    return pos_map.get(position, pos_map["bottom-right"])


def add_text_watermark(image, text, font_path, font_size=36, color=(255, 255, 255, 128), position="bottom-right", margin=20):
    """
    给单张 PIL 图像添加文字水印(返回 RGBA 模式图像)
    """
    # 确保原图进入 RGBA 空间
    base = image.convert("RGBA")
    
    # 加载指定字体
    try:
        font = ImageFont.truetype(font_path, font_size)
    except IOError:
        # 回退至系统默认点阵字体(注意:默认字体不支持指定尺寸与中文)
        font = ImageFont.load_default()

    # 获取文本渲染外接矩形 (left, top, right, bottom)
    bbox = font.getbbox(text)
    text_w = bbox[2] - bbox[0]
    text_h = bbox[3] - bbox[1]

    # 创建相同尺寸的全透明叠加图层
    txt_layer = Image.new("RGBA", base.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(txt_layer)

    # 确定坐标并绘制文字
    pos = calculate_position(base.size, (text_w, text_h), position=position, margin=margin)
    # 补偿 bbox 左上角偏移量
    actual_pos = (pos[0] - bbox[0], pos[1] - bbox[1])
    draw.text(actual_pos, text, font=font, fill=color)

    # 图层混合
    return Image.alpha_composite(base, txt_layer)


def add_image_watermark(image, watermark_path, scale_ratio=0.15, opacity=0.8, position="bottom-right", margin=20):
    """
    给单张 PIL 图像添加图片水印
    """
    base = image.convert("RGBA")
    wm = Image.open(watermark_path).convert("RGBA")

    # 根据底图宽度按比例缩放水印图片
    bw, bh = base.size
    target_wm_w = max(int(bw * scale_ratio), 40)
    w_ratio = target_wm_w / float(wm.size[0])
    target_wm_h = int(float(wm.size[1]) * w_ratio)
    wm = wm.resize((target_wm_w, target_wm_h), Image.Resampling.LANCZOS)

    # 调整水印整体不透明度
    if opacity < 1.0:
        r, g, b, alpha = wm.split()
        alpha = alpha.point(lambda p: int(p * opacity))
        wm.putalpha(alpha)

    # 创建透明合成层并粘贴水印
    wm_layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
    pos = calculate_position(base.size, wm.size, position=position, margin=margin)
    wm_layer.paste(wm, pos, mask=wm)

    return Image.alpha_composite(base, wm_layer)


def batch_watermark(input_dir, output_dir, mode="text", **kwargs):
    """
    批量扫描目录并处理图片
    """
    valid_exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
    if not os.path.exists(output_dir):
        os.makedirs(output_dir, exist_ok=True)

    success_count = 0
    fail_count = 0

    for root, _, files in os.walk(input_dir):
        for file in files:
            ext = os.path.splitext(file)[1].lower()
            if ext not in valid_exts:
                continue

            src_path = os.path.join(root, file)
            # 计算对应的相对路径并保持输出目录结构一致
            rel_dir = os.path.relpath(root, input_dir)
            target_sub_dir = os.path.join(output_dir, rel_dir)
            os.makedirs(target_sub_dir, exist_ok=True)
            dst_path = os.path.join(target_sub_dir, file)

            try:
                with Image.open(src_path) as raw_img:
                    # 关键步骤:依据 Exif 旋转标记矫正物理方向
                    img = ImageOps.exif_transpose(raw_img)
                    
                    if mode == "text":
                        result = add_text_watermark(
                            img,
                            text=kwargs.get("text", "Official Watermark"),
                            font_path=kwargs.get("font_path", "msyh.ttc"),
                            font_size=kwargs.get("font_size", 36),
                            color=kwargs.get("color", (255, 255, 255, 128)),
                            position=kwargs.get("position", "bottom-right"),
                            margin=kwargs.get("margin", 25)
                        )
                    else:
                        result = add_image_watermark(
                            img,
                            watermark_path=kwargs.get("watermark_path"),
                            scale_ratio=kwargs.get("scale_ratio", 0.15),
                            opacity=kwargs.get("opacity", 0.75),
                            position=kwargs.get("position", "bottom-right"),
                            margin=kwargs.get("margin", 25)
                        )

                    # 根据目标格式保存
                    if ext in {".jpg", ".jpeg"}:
                        # JPEG 不支持 Alpha 通道,须转换回 RGB 并指定压缩质量
                        result.convert("RGB").save(dst_path, "JPEG", quality=92)
                    else:
                        result.save(dst_path, quality=95)

                success_count += 1
            except Exception as err:
                print(f"[错误] 处理失败 {src_path}: {err}", file=sys.stderr)
                fail_count += 1

    print(f"批量任务完成: 成功 {success_count} 张, 失败 {fail_count} 张。")


if __name__ == "__main__":
    # 使用示例:批量添加文字水印
    # batch_watermark(
    #     input_dir="./images_raw",
    #     output_dir="./images_watermarked",
    #     mode="text",
    #     text="© 2026 技术实战专栏",
    #     font_path="msyh.ttc",
    #     font_size=40,
    #     color=(255, 255, 255, 140),
    #     position="bottom-right"
    # )

    # 使用示例:批量添加图片水印
    # batch_watermark(
    #     input_dir="./images_raw",
    #     output_dir="./images_watermarked",
    #     mode="image",
    #     watermark_path="./logo.png",
    #     scale_ratio=0.18,
    #     opacity=0.7,
    #     position="bottom-right"
    # )
    pass

六、生产环境常见隐患与排查

避坑要点:
  1. 照片被“意外颠倒”:智能手机拍摄的照片通常带有 Exif 方向标记(Orientation)。如果不调用 ImageOps.exif_transpose(),读取出来的像素仍是传感器物理排布方向,添加完水印保存后可能导致照片颠倒或水印位置错乱。
  2. JPEG 导出报错 OSError: cannot write mode RGBA as JPEG:JPEG 规范不包含透明度通道。保存为 .jpg 时必须先调用 .convert("RGB")。
  3. 中文文字显示为方块豆腐块:未指定中文字体或使用的 .ttf 文件缺少中文字符集。请务必使用系统包含完整 CJK 字符集的字体文件。
  4. 小图上的水印过度溢出:如果字体固定使用 48px,而原图尺寸仅有 80x80,水印将严重溢出。建议在正式执行前根据 min(bw, bh) 动态缩放 font_size。

七、核心技术决策对比与总结

对文字水印与图片水印两种场景的常见配置及关键考量点进行归纳,以便在不同业务场景中做出适配:

Python批量给图片添加文字或图片水印脚本
图4:两种水印方案关键要素与工程选型矩阵

通过本文介绍的双图层 Alpha 混合机制、九宫格坐标映射与目录遍历管道,即可构建稳健可靠的自动化批量图片水印工具,满足生产环境下各种尺寸与格式图片的打标归档诉求。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表