本文要实现「自动生成博客题图」。研究这件事情的起因,是我在 Telegram 里面分享了几篇博客,它的效果如下:


这两篇文章中都是有图片的,但是 Telegram 仍然抓取了我的首页图片。而我希望博客内的图片被抓取,所以首先我应该研究一下 Telegram 的抓取机制。
0x01 OG 标签
查阅文档,rich link preview 是 Telegram 在 2015 年引入的功能。

于是找到一篇 Richard Oosterhof 的文章,据其描述,og:image
这个 meta 标签是「a JPG or PNG image, minimum dimensions of 300 x 200 pixels are advised」,形如:
<meta property="og:image" content="http://richpreview.com/richpreview.jpg" />
我们默认情况下的博客文章,返回的 og:image
确实是博客主题图:

给文章指定一个 feature image,Telegram 可以爬取正确的图片。


因此我们确定,只需要修改 og:image
,就能让 Telegram 等第三方平台爬取到合适的图片。
然而,我有不少的文章,里面一张图片都没有;或是图片太杂乱,不适合做题图。所以今天来研究一下如何对这些博客生成合适的图片。
0x02 算法
我参考了机核网电台转视频的风格:

机核网的这个生成器是开源的,源码在 https://github.com/rabbitism/GadioVideo 。看了一下主要逻辑,背景图片的来历是:
image = cv2.imread(image_dir)
image_suffix = page.image.suffix
background_image = Frame.expand_frame(image, Frame.width, Frame.height)
background_image = cv2.GaussianBlur(background_image, (255, 255), 255)
content_image = Frame.shrink_frame(image, 550, 550)
将主要图片扩张到 box 大小,然后做一个高斯模糊。图片扩张的算法是:首先把图片缩放到长或宽符合目标尺寸,然后裁剪取中心部分。
我希望生成的图片是:尺寸 640×480;以博客中的图片生成背景;加上白色的博客标题。用 GIMP 做了个样例:


观感还不错。总共用到了三个图层:
- 原图图层,做高斯模糊
- 纯黑色图层,用于降低亮度,方便显示文字
- 文字图层
现在我们写一个 Python 脚本来做这件事。
0x03 实现:原图图层
首先缩放原图,然后高斯模糊,再裁剪。
blured = img.filter(ImageFilter.GaussianBlur(radius=5))
bg = ImageOps.fit(blured, target_size)

0x04 实现:黑色图层
最简单的黑色图层是全黑,但图片的所有部分都被同等地遮盖,视觉不太美观。我们加一点类似于光晕的效果,让中心部分更黑。

代码实现,先画一个椭圆,然后做高斯模糊即可。
def make_shadow(bg, non_transparency=0.75):
shadow = Image.new('L', bg.size, color=220)
draw = ImageDraw.Draw(shadow)
h, w = bg.size
draw.ellipse([h//6, w//6, h*5//6, w*5//6], fill=255)
shadow = shadow.filter(ImageFilter.GaussianBlur(radius=64))
black_layer = Image.new('RGBA', bg.size, color='#000000')
black_layer.putalpha(shadow)
return Image.blend(bg, black_layer, non_transparency)
0x05 实现:文字层
文字分行采用 wrapper
库。尝试了几种字体,最终选择苹方 light。
def make_text(img, text, font):
res = img.copy()
draw = ImageDraw.Draw(res)
w, h = res.size
font = ImageFont.truetype(font, 48)
text_lines = '\n'.join(textwrap.wrap(text, 12))
draw.text([w/2, h/2], text_lines, anchor='mm', font=font)
return res

这样显示出来的文字有些单调。我们参考一下《杀戮尖塔》游戏的效果:

给字加上阴影。改改代码:
def make_text(img, text, font):
res = img.copy()
draw = ImageDraw.Draw(res)
w, h = res.size
font = ImageFont.truetype(font, 48)
text_lines = '\n'.join(textwrap.wrap(text, 12))
draw.text([w/2+5, h/2+5], text_lines, fill=(0, 0, 0, 200), anchor='mm', font=font)
draw.text([w/2, h/2], text_lines, anchor='mm', font=font)
return res

0x06 效果
用生成器做了几组题图,效果都还不错:








用 Telegram 发送,正确加载图片:
