好久没写博客了。最近在做图片水印功能,本来觉得难度不大,照着 Pillow 文档随便搞搞就行了。没想到设计师验收的时候发现一堆坑。也许是学艺不精,没有仔细研究过 Pillow,只是随便看看文档做的,可能是我用的姿势不对才导致一堆坑。

基本需求是:

  1. 前面一个品牌 logo
  2. 后面一个 @用户名,用思源黑体常规字重的字体
  3. 有投影效果
  4. 整体大小和图片宽度成比例

首先去 Pillow 文档里找了个 example

在网上查一些 blog 也都是这么写的,于是就照着写了一个基本的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python
# encoding: utf-8

from PIL import Image, ImageDraw, ImageFont

SourceHanSans_font = '~/SourceHanSansCN-Regular.otf'

STANDARD_SIDE_MARGIN = 12 # 右边距
STANDARD_BOTTOM_MARGIN = 10 # 下边距
SHADOW_OFFSET = 1 # 水印阴影偏移

im = Image.open('test.jpg').convert("RGBA")
username = u'@用户名'

im_width, im_height = im.size

# 450 宽度是基准,设计要求小于这个宽度的不会有水印
scale = im_width * 1.0 / 450

font = ImageFont.truetype(SourceHanSans_font, size=int(round(13 * scale))) # 13 号字体也是基准
text = username
text_width, text_height = font.getsize(text)
text_x = int(round(im_width - STANDARD_SIDE_MARGIN * scale - text_width))
text_y = int(round(im_height - STANDARD_BOTTOM_MARGIN * scale - text_height))

txt = Image.new('RGBA', im.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(txt)

draw.text((text_x + int(round(SHADOW_OFFSET * scale)), text_y + int(round(SHADOW_OFFSET * scale))), text, font=font, fill=(0, 0, 0, 77)) # 黑色 40% 不透明度的阴影
draw.text((text_x, text_y), text, font=font, fill=(255, 255, 255, 255)) # 白色
r = Image.alpha_composite(combined, txt)
draw = ImageDraw.Draw(r)

r.save("result.jpg", format="JPEG", quality=95) # 不设置quality的话会默认以 75 的质量保存,损失很多画质,95 是最大值。

看起来还可以啊,计算好文字的长宽,先绘制投影。投影就是一个黑色带透明度的同样的文字,(x,y) 坐标分别向右和下增加若干像素。绘制完之后,再在原来的 (x,y) 位置绘制白色文字就行了。文字都绘制到一张空白的透明图上,然后把两个图层贴一块就行了。

搞出来发现,怎么文字不是按照我说的 (x,y) 放置,y 坐标偏移了若干像素呢?

然后网上查了一圈发现,字体这个东西,原来是有个 offset 的概念的。绘制的时候把 offset 自己减去就行了。晕。改一把。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python
# encoding: utf-8

from PIL import Image, ImageDraw, ImageFont

SourceHanSans_font = '~/SourceHanSansCN-Regular.otf'

STANDARD_SIDE_MARGIN = 12 # 右边距
STANDARD_BOTTOM_MARGIN = 10 # 下边距
SHADOW_OFFSET = 1 # 水印阴影偏移

im = Image.open('test.jpg').convert("RGBA")
username = u'@用户名'

im_width, im_height = im.size

# 450 宽度是基准,设计要求小于这个宽度的不会有水印
scale = im_width * 1.0 / 450

font = ImageFont.truetype(SourceHanSans_font, size=int(round(13 * scale))) # 13 号字体也是基准
text = username
text_width, text_height = font.getsize(text)
text_x_offset, text_y_offset = font.getoffset(text) # 这里
text_x = int(round(im_width - STANDARD_SIDE_MARGIN * scale - text_width))
text_y = int(round(im_height - STANDARD_BOTTOM_MARGIN * scale - text_height))

txt = Image.new('RGBA', im.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(txt)

draw.text((text_x - text_x_offset + int(round(SHADOW_OFFSET * scale)), text_y - text_y_offset + int(round(SHADOW_OFFSET * scale))), text, font=font, fill=(0, 0, 0, 77)) # 黑色 40% 不透明度的阴影
draw.text((text_x - text_x_offset, text_y - text_y_offset), text, font=font, fill=(255, 255, 255, 255)) # 白色
r = Image.alpha_composite(combined, txt)
draw = ImageDraw.Draw(r)

r.save("result.jpg", format="JPEG", quality=95) # 不设置quality的话会默认以 75 的质量保存,损失很多画质,95 是最大值。

然后就开心的给设计师验收了。

然后就坑爹了。

在图片大的时候,比如 1080P 这么大的图,看着效果确实还行。但是在小图的时候图片中文字周围有不明的黑色锯齿。

例如这样

黑色锯齿

原因不明。然后就网上搜抗锯齿的方案。stackoverflow 上看到一些方法,例如这里。就是把图片先放大 2 倍,等比放大绘制完成之后,再缩小到 1/2,也就是原图大小,并在 pillow 里的 resize 方法添加 filter=Image.ANTIALIAS

确实有不少效果,黑色锯齿少了很多,但是还是有,并且因为缩小,绘制出来的文字画质有损失。不能接受。

查到最后发现,原来直接在原图上绘制就不会有黑色锯齿。之前都是先生成一张透明的图层,在这个上面绘制完成之后,把原图和透明图合并。不知道为什么,在透明图上绘制白色的文字就会有黑色锯齿,在尺寸小的时候。于是就改了一下思路,全部直接在原图上进行绘制,不用透明图了。

但是又发现,绘制出来的问题,没有透明度了。崩溃。不知道为什么。就又改了一下思路,把需要透明度的黑色阴影在透明图上绘制,和原图贴合之后,再在原图上绘制不需要透明度的白色文字。这样就解决了问题。晕死。

其实同时还有另一个问题,就是品牌 logo,其实就是「知乎」两个字。设计师给我的是一个 svg 图,方便等比放大。但是搜了一圈,发现 Python 没有什么特别方便的,操作 svg 的库,不是依赖太大,就是用起来很麻烦,没什么人维护。就让设计师换了个思路,把 svg 图转成 ttf 字体文件,到时候一起用字体绘制多方便。