用 Pillow 手工合成 16:9 技术封面:渐变、网格、字宽测量与截图画框

举报
yd_292694877 发表于 2026/10/18 09:09:42 2026/10/18
【摘要】 不依赖设计工具也能出统一风格的封面:逐行渐变控制色带、网格线做视觉密度、用 textlength 自动排布标签胶囊,以及截图卡片的等比缩放、描边与假阴影。附完整可运行脚本。

一、为什么不用现成的封面模板

给技术文章配封面,最省事的做法是打开在线设计工具,选个模板改文字。但做过几次之后会发现两个问题:一是每次都要重新对一遍字号和留白,风格会慢慢漂;二是几十张封面要改同一个细节(比如统一把主色从橙色换成珊瑚红),只能一张张点。

封面本质上是一张固定尺寸的图,完全可以用代码直接画。1600×900 的画布,左边放标题区,右边放产品截图,中间用一条网格线做视觉分隔——这个结构一旦写成脚本,以后每次只要替换标题文字和截图路径,十秒钟出一张,而且风格绝对一致。

下面这份脚本用 Pillow 实现,不依赖任何设计资源,Windows 自带字体就够。

二、画布与坐标系

Pillow 的坐标系是左上角原点,x 向右、y 向下,和 CSS 一样。先建画布:

from PIL import Image, ImageDraw, ImageFont

W, H = 1600, 900
img = Image.new("RGB", (W, H), "#10131a")
d = ImageDraw.Draw(img)

这里用 RGB 而不是 RGBA。封面最终是 PNG,不需要透明通道;用 RGB 保存出来的文件明显更小,而且不会有色偏。如果后面要把截图贴上去,注意截图是 RGB 就直接贴,是 RGBA 要先 convert("RGB"),否则 paste 到 RGB 画布上透明区域会变成黑色。

三、逐行渐变:简单但要注意性能

Pillow 没有内置的线性渐变,最直接的办法是逐行画线:

for y in range(H):
    t = y / H
    r = int(16 + 13 * t)
    g = int(19 + 17 * t)
    b = int(26 + 25 * t)
    d.line([(0, y), (W, y)], fill=(r, g, b))

900 次 draw.line,耗时大约十几毫秒,完全可以接受。关键在于颜色增量要小:这里从 #10131a 过渡到 #1d242f,两个色值在全通道上只差 13/17/25,人眼几乎看不出分界线,但整张图会有一种"底部微微透光"的质感。

如果增量给大(比如从纯黑到中灰),逐行画就会出现明显的色带(banding)。遇到这种情况有两个办法:一是把 range(H) 改成 range(H*4) 再按比例画,用更细的步长换取平滑;二是干脆用 numpy 生成整块渐变数组再 Image.fromarray 贴回去,速度更快也更干净。封面这种小图,逐行法够用。

四、网格线是免费的视觉密度

纯色渐变背景容易显得空。加一层 64px 的细网格,成本几乎为零,但视觉密度会立刻上来:

for x in range(0, W, 64):
    d.line([(x, 0), (x, H)], fill=(24, 28, 38))
for y in range(0, H, 64):
    d.line([(0, y), (W, y)], fill=(24, 28, 38))

网格色 #181c26 只比背景稍亮一点点,属于"不仔细看看不见"的程度。这个度很关键:网格太亮会抢标题的注意力,太暗又等于没画。经验值是和背景的亮度差控制在 8~12 之间。

五、字体:msyh.ttc 与 msyhbd.ttc

Windows 自带的中文字体里,微软雅黑是最安全的选择,两个文件分别是常规体和粗体:

FONT_BODY = r"C:\Windows\Fonts\msyh.ttc"
FONT_BOLD = r"C:\Windows\Fonts\msyhbd.ttc"

def font(path, size):
    try:
        return ImageFont.truetype(path, size)
    except Exception:
        return ImageFont.truetype(FONT_BODY, size)

.ttc 是字体集合文件,一个文件里可能包含多个字重。Pillow 的 truetype 支持传 index 参数选择具体那一个,不传默认取第 0 个。如果发现粗体没生效,可以先确认文件是否真的存在——有些精简版系统会删掉 msyhbd.ttc。

包一层 try/except 做 fallback 是必要的。脚本一旦在别的机器上跑(比如 CI 或者 Linux 服务器),字体路径必然不同,有 fallback 至少不会直接崩,最多是字重不对。

六、用 textlength 做自动排布的胶囊标签

封面底部通常放几个技能标签,比如「原生 JS」「SVG 进度环」。如果每个标签用固定宽度,中文和英文混排时就会出现有的撑满有的留白。

正确做法是先量字宽再画框:

tags = ["原生 JS", "SVG 进度环", "Web Audio", "无后端"]
x = 92
for t in tags:
    fs = font(FONT_BODY, 24)
    w = d.textlength(t, font=fs)
    d.rounded_rectangle([x, 640, x + w + 40, 690], radius=12,
                        outline=(58, 66, 84), width=2)
    d.text((x + 20, 652), t, font=fs, fill=(150, 162, 182))
    x += w + 56

textlength 返回的是浮点数,精确到亚像素。框的宽度取 w + 40,等于左右各 20px 内边距;下一个标签的起点是 x + w + 56,比框宽多 16px 当间距。这样无论标签内容多长,间距都是恒定的。

注意 textlength 必须传和 draw.text 完全相同的 font 对象。用不同 size 实例去量、去画,宽度就对不上——这是最容易踩的坑,因为代码看起来完全合理。

七、截图卡片:缩放、描边与假阴影

右侧贴产品截图,关键是三件事:等比缩放、加边框、加阴影。

shot = Image.open(SHOT).convert("RGB")
target_w = 640
ratio = target_w / shot.width
shot = shot.resize((target_w, int(shot.height * ratio)), Image.LANCZOS)

sx, sy = W - target_w - 90, (H - shot.height) // 2
shadow = Image.new("RGB", (shot.width + 40, shot.height + 40), (8, 9, 13))
img.paste(shadow, (sx - 20, sy - 16))
img.paste(shot, (sx, sy))
d.rectangle([sx, sy, sx + shot.width, sy + shot.height],
            outline=(60, 66, 82), width=2)

缩放用 LANCZOS,这是 Pillow 里质量最好的重采样算法,缩小时不会有明显锯齿。NEAREST 虽然快,但缩出来的文字边缘会糊成一团。

阴影这里是最土但最稳的做法:新建一块比截图大 40px 的深色矩形,先贴到截图位置偏左上 20/16 像素处,再把截图盖上去。这样露出来的部分就是"阴影"。真正的模糊阴影要 ImageFilter.GaussianBlur,但在这张深色封面上,硬边阴影反而更干净,而且省掉一次全图模糊的开销。

最后用 d.rectangle 沿着截图边缘描一圈 2px 的亮边,作用是让截图和背景分离——深色截图贴在深色背景上,没有这圈边框会整个糊在一起。

八、几个收尾细节

  • 文件体积。同样的图,PNG 保存时加 optimize=True 能小 10%~20%。封面通常 100~300KB,都在可接受范围。如果要求更小,可以转成调色板模式 convert("P", palette=Image.ADAPTIVE, colors=256),但渐变会出现轻微色带,慎用。
  • 尺寸比例。很多平台要求 16:9,实际验收时容差很小。1600×900 是标准 16:9,不要用 1920×1080 的近似值去凑,也不要后期裁切,直接按目标尺寸画。
  • 文字别贴边。左边距给到 90px,右边截图留 90px,上下也留足。留白不够的封面在小尺寸缩略图里会显得非常挤。
  • 中文字距。Pillow 不支持 letter-spacing。真要拉开字距,只能一个字一个字画,每次 x += char_width + spacing。大标题其实不需要拉字距,靠字号和字重区分层级就够了。

九、完整脚本

from PIL import Image, ImageDraw, ImageFont
import os

W, H = 1600, 900
OUT = "./assets"
SHOT = os.path.join(OUT, "screenshot.png")
DEST = os.path.join(OUT, "cover.png")

FONT_BODY = r"C:\Windows\Fonts\msyh.ttc"
FONT_BOLD = r"C:\Windows\Fonts\msyhbd.ttc"

def font(path, size):
    try:
        return ImageFont.truetype(path, size)
    except Exception:
        return ImageFont.truetype(FONT_BODY, size)

img = Image.new("RGB", (W, H), "#10131a")
d = ImageDraw.Draw(img)

for y in range(H):
    t = y / H
    d.line([(0, y), (W, y)], fill=(
        int(16 + 13 * t), int(19 + 17 * t), int(26 + 25 * t)))

for x in range(0, W, 64):
    d.line([(x, 0), (x, H)], fill=(24, 28, 38))
for y in range(0, H, 64):
    d.line([(0, y), (W, y)], fill=(24, 28, 38))

f_title = font(FONT_BOLD, 78)
d.text((90, 258), "番茄钟", font=f_title, fill=(255, 255, 255))
d.text((90, 356), "专注计时器", font=f_title, fill=(255, 95, 86))

shot = Image.open(SHOT).convert("RGB")
shot = shot.resize((640, int(shot.height * 640 / shot.width)), Image.LANCZOS)
sx, sy = W - 640 - 90, (H - shot.height) // 2
img.paste(Image.new("RGB", (shot.width + 40, shot.height + 40), (8, 9, 13)),
          (sx - 20, sy - 16))
img.paste(shot, (sx, sy))
d.rectangle([sx, sy, sx + shot.width, sy + shot.height],
            outline=(60, 66, 82), width=2)

img.save(DEST, "PNG", optimize=True)

十、小结

这套脚本一共不到 100 行,覆盖了封面需要的大部分元素:渐变背景、网格、主副标题、标签胶囊、截图画框。它真正解决的其实不是"画图"这件事,而是把"风格一致性"从人的注意力里拿出来,交给代码保证。以后换标题只改两个字符串,换配色只改三个色值,改完立刻重跑,不用再对着设计工具微调。

如果副标题需要自适应长度(比如从文章标题自动生成),可以用 textlength 做一个简单的缩放:从 34px 开始量,超过可用宽度就降到 32、30,直到装得下为止。这一步加上之后,封面就彻底变成"传参出图",可以挂到文章发布流程里自动跑。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。