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

  这两篇文章中都是有图片的,但是 Telegram 仍然抓取了我的首页图片。而我希望博客内的图片被抓取,所以首先我应该研究一下 Telegram 的抓取机制。

0x01 OG 标签

  查阅文档,rich link preview 是 Telegram 在 2015 年引入的功能。

▲ Telegram 对 link preview 的描述

  于是找到一篇 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 算法

  我参考了机核网电台转视频的风格:

▲ 图源:游戏潮里那些让我们失望的游戏-游戏茶话会Vol.35丨机核

  机核网的这个生成器是开源的,源码在 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 实现:黑色图层

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

▲ 不同的 shadow 产生不同的最终效果

  代码实现,先画一个椭圆,然后做高斯模糊即可。

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 发送,正确加载图片: