几百张图批量加平铺文字水印:透明度只调一次,别让它乘两次(含 3 个实跑大坑)

2026-10-06 · 喵窝资源铺

截图要发给客户、图片要发公众号、资料要外发协作——想防盗用又不肯一张张开修图软件。这篇给一份 PIL 批量平铺水印脚本:整图倾斜平铺、字号随图片尺寸自动缩放、透明度一处可调、透明 PNG 的 alpha 保留、JPEG/WebP 统一进输出目录可追溯。代码完整实跑过,真实输出贴在下面。

它做的判断顺序

  1. 扫目录下所有 png/jpg/jpeg/webp/bmp,排除输出目录自己(见坑 2,不然二次运行套娃);
  2. 字号 = 短边 ÷ 28,下限 18px——大图小图水印视觉占比一致,不用手动调;
  3. 水印文字用满强度画进平铺层,整层拼完后再统一降一次透明度(见坑 1,这是最容易翻车的地方);
  4. 输出统一 PNG;全不透明的图自动转 RGB 存 JPEG(quality 92)省体积,带 alpha 的保留 PNG;
  5. 逐张打印 原名 尺寸 → 输出名,最后报完成数,出了问题能对上哪一张。

脚本全文

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

SRC = sys.argv[1]
OUT = os.path.abspath(sys.argv[2] if len(sys.argv) > 2 else os.path.join(SRC, 'watermarked'))
TEXT = sys.argv[3] if len(sys.argv) > 3 else '喵窝资源铺 miaowoo.900890.xyz'
OPACITY = 0.13        # 水印透明度(0~1),太深毁图太浅看不见
ANGLE = -30           # 倾斜角度
STEP_X, STEP_Y = 340, 260   # 平铺间距(像素)

def find_font():
    # 中文水印必须用含 CJK 字形的字体,DejaVu 只有拉丁字形,中文会变方框
    for p in ('/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc',
              '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
              '/System/Library/Fonts/PingFang.ttc',
              'C:/Windows/Fonts/msyh.ttc',
              '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'):
        if os.path.exists(p):
            return p
    return None

FONT_PATH = find_font()
if FONT_PATH is None:
    sys.exit('没找到可用的 TTF 字体,请改 FONT_PATH 指向本机任意 .ttf/.ttc(PIL 默认位图字体不支持中文且大小固定)')

def make_tile(text, size_px):
    font = ImageFont.truetype(FONT_PATH, size_px)
    tmp = Image.new('RGBA', (STEP_X + size_px * len(text), STEP_Y + size_px * 2), (0, 0, 0, 0))
    d = ImageDraw.Draw(tmp)
    bbox = d.textbbox((0, 0), text, font=font)
    tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
    # 坑:这里用满强度 255 画字,透明度在拼完整层后统一降一次
    # 若在此处就填 alpha=33,paste(mask=tile) 会把 alpha 再乘一次(33*33/255≈4),
    # 结果只有 ~1.5% 不透明度,水印肉眼几乎看不见
    d.text((0, 0), text, font=font, fill=(255, 255, 255, 255))
    big = tmp.rotate(ANGLE, expand=True)
    return big, (tw, th)

def watermark(im):
    w, h = im.size
    base_size = max(18, min(w, h) // 28)          # 字号随图片大小缩放
    tile, (tw, th) = make_tile(TEXT, base_size)
    if im.mode != 'RGBA':
        im = im.convert('RGBA')
    layer = Image.new('RGBA', (w + tile.width, h + tile.height), (0, 0, 0, 0))
    for y in range(0, h + tile.height, STEP_Y):
        for x in range(0, w + tile.width, STEP_X):
            layer.paste(tile, (x, y), tile)
    layer = layer.crop((0, 0, w, h))
    # 透明度只在这里施加一次
    a = layer.getchannel('A').point(lambda v: int(v * OPACITY))
    layer.putalpha(a)
    return Image.alpha_composite(im, layer)

os.makedirs(OUT, exist_ok=True)
OUT_REAL = os.path.abspath(OUT)
files = [f for f in sorted(glob.glob(os.path.join(SRC, '*.*')))
         if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.bmp'))
         and os.path.dirname(os.path.abspath(f)) != OUT_REAL]   # 坑:输出目录在源目录里时别把已加水印的图再水印一遍
if not files:
    sys.exit('目录里没有可处理的图片')

done = 0
for f in files:
    name = os.path.basename(f)
    out_path = os.path.join(OUT, os.path.splitext(name)[0] + '.png')  # 统一出 PNG 保透明度
    try:
        im = Image.open(f)
        im.load()
    except Exception as e:
        print(f'跳过 {name}: 打不开({e})')
        continue
    result = watermark(im)
    # PNG 保留 alpha;全不透明转 RGB 存 JPEG 省体积
    has_alpha = result.getextrema()[3][0] < 255 if result.mode == 'RGBA' else False
    if has_alpha:
        result.save(out_path)
    else:
        result.convert('RGB').save(os.path.join(OUT, os.path.splitext(name)[0] + '.jpg'), quality=92)
        out_path = os.path.join(OUT, os.path.splitext(name)[0] + '.jpg')
    print(f'{name} {im.size[0]}x{im.size[1]} -> {os.path.basename(out_path)}')
    done += 1
print(f'完成 {done}/{len(files)} 张,输出目录 {OUT}')

实跑输出(原样贴)

测试目录 4 个文件:1200×800 仿文档截图 JPG、400×300 小 PNG、300×300 全透明 logo PNG、400×300 WebP,外加一个 说明.txt 干扰文件:

logo透明.png 300x300 -> logo透明.png
小图.png 400x300 -> 小图.jpg
报告截图.jpg 1200x800 -> 报告截图.jpg
表情包.webp 400x300 -> 表情包.jpg
完成 4/4 张,输出目录 .../wm_test/src/wm_out

验证(不拿肉眼看,拿数据说话)

检查项结果
尺寸不变输出与原图逐张一致(水印只叠加不缩放)
水印确实盖上了输出 vs 原图逐像素比对,7.8% 像素被改变(13% 透明度平铺的合理覆盖量;不是 0% 说明盖上了,不是 100% 说明没毁图)
透明 logo 的 alpha输出 alpha 极值 (0, 33)——33 = 255 × 0.13,恰好施加一次,背景全透明保留
幂等连跑 2 次,3 张输出文件 MD5 完全一致;输出目录放源目录里二次运行,仍只处理 4 张源图,不套娃
干扰文件说明.txt 不在后缀白名单,自动跳过

3 个实跑才踩得到的坑

坑 1:透明度被乘了两次,水印淡到看不见。最顺手的写法是画字时就填 fill=(255,255,255,33)(0.13×255),然后 layer.paste(tile, pos, mask=tile)——但 paste 的 mask 是按源图 alpha 加权的,33 的 alpha 过一遍 mask 变成 33×33/255≈4,实际不透明度只剩 ~1.5%。更阴的是它不报错、不崩、输出图也「看起来有水印」,直到你把图放到浅色背景上才发现什么都看不见。正确做法:画字用满强度 255,整层拼完后再 putalpha 统一降一次。实跑验证:修正后 alpha 极值恰好是 33。
坑 2:输出目录放进源目录,二次运行把水印图再水印一遍。「输出到 源目录/watermarked」这个习惯没错,但下次全目录扫描会把上次的水印图当新图再盖一层——水印越叠越深。修法是处理前比对绝对路径排除输出目录。首次实跑就因为这个过滤条件写错(用了 startswith 前缀判断),把整个源目录都排除了,直接「目录里没有可处理的图片」退出——过滤条件太宽和太窄都是坑。
坑 3:字体不对,中文全是方框。Linux 上 PIL 最常摸到的字体是 DejaVu,但它没有 CJK 字形,水印文字里的中文会画成一排 □□□,而且因为水印只有 13% 透明度,缩略图里根本看不出来,发出去才发现。脚本按平台优先找 CJK 字体(Noto CJK / 文泉驿 / 苹方 / 微软雅黑),最后才退到 DejaVu。另外 PIL 自带的位图默认字体既不支持中文也不能调字号,别用。

参数怎么调

不想自己跑脚本?喵窝资源铺提供 AI 代办:¥9.9 起,把图片打包发给我们,批量加水印/压缩/转格式一次搞定,当天交付。脚本类服务同样 9.9 起,改到你满意为止。
👉 miaowoo.900890.xyz(虚拟物品,售出后不退款,下单前确认需求)

← 返回全部文章 · 下一篇:文件按关键词自动归类