【Python自动化】生成带装饰图形的渐变背景文字封面

Python自动化专栏,利用文字生成固定比例且带有装饰图形的封面

一、背景介绍

在写博客过程中,经常要用到一些专栏封面、文章封面,其中专栏封面要求的宽高比是1:1,而文章封面的推荐的是16:9。在网上搜索的图片,大部分都不是期望的比例,因此需要在 PS 或者 AI 中裁剪、添加文字

以上的处理过程重复步骤很多,因此考虑用 Python 来实现。简单起见,生成的封面没有以图片作为背景层,而是用渐变填充来替代,与此同时,在封面的左下角和右上角,绘制一些小的半透明装饰图形,让封面增加一些设计感

二、功能介绍

效果预览

cover-overview

功能清单

  • 内置4种渐变背景,并且可以很方便地扩充,配色方案可以参考:UIGradients

  • 内置4种边缘图形:圆环、三角形、正方形、六边形

  • 文字:支持如下两种方案

    • 只有主标题,居中展示
    • 主标题和副标题,副标题宽度不会超过主标题;主标题和副标题之间有一条半透明分割线

三、过程拆解

实现过程可以归纳为如下,主要是4个核心步骤
5-layers

先创建一个封装类 BlogCoverGenerator,在__init__方法中定义需要用到的属性,并定义生成封面的核心方法

注意:后面很多操作都需要用到透明度,因此基础图形的 mode 设置为 RGBA ,并且生成的图片格式需要指定为 PNG 格式

class BlogCoverGenerator:
    def __init__(self,
                 title: str,
                 sub_title: str,
                 title_h_ratio: float=.5,
                 ratio_pair: tuple[int]=(16, 9),
                 bg_gradient: BackgroundGradient=BackgroundGradient.skyline,
                 bg_shape: BackgroundShape=BackgroundShape.circle,
                 min_size='1M'):
        # 封面主标题
        self.title = title
        # 封面副标题
        self.sub_title = sub_title
        # 如果没有副标题,主标题需要垂直居中
        self.no_sub_title = False
        if self.sub_title is None or '' == self.sub_title.strip():
            self.no_sub_title = True
        # 标题区域垂直方向占比(从正中心开始计算),参考值0.3~0.6
        self.title_h_ratio = title_h_ratio
        if self.title_h_ratio < 0.3:
            self.title_h_ratio = 0.3
        elif self.title_h_ratio > 0.6:
            self.title_h_ratio = 0.6
        # 字体位置
        self.zh_font_location = ''
        self.en_font_location = ''
        # 封面宽高比,16:9, 4:3, 1:1等,以16:9为例,需要传入(16, 9)
        self.ratio_pair = ratio_pair
        # 封面渐变背景色
        self.bg_gradient = bg_gradient
        # 封面背景几何图形,目前支持三角形、六边形、圆形
        self.bg_shape = bg_shape
        # 封面大小。如果传入字符串,支持的单位为k, M;也可以传入数值
        self.min_size = self._parse_min_size(min_size)
        if self.min_size > 178956970:
            # ImageDraw.text大小限制
            self.min_size = 178956970

    def generate_cover(self, output_path: str, output_file_name: str):
        width, height = self._get_cover_size()
        img = Image.new('RGBA', (width, height))

        # 第1层,渐变背景
        self._display_gradient_bg(img)
        # 第2层,边缘装饰图形
        img = self._display_decorate_shape(img)
        # 第3层,半透明遮罩
        img = self._display_transparent_mask(img)
        # 第4层,文字
        img = self._display_title(img)
        # 保存
        img.save(os.path.join(output_path, output_file_name))

1.渐变背景层

首先定义一个渐变枚举

from enum import Enum
class BackgroundGradient(Enum):
    skyline = ['#1488CC', '#2B32B2']
    cool_brown = ['#603813', '#b29f94']
    rose_water = ['#E55D87', '#5FC3E4']
    crystal_clear = ['#159957', '#155799']

暂时没有在 Pillow 的文档中找到如何绘制渐变图形,这里只实现了水平方向的渐变色,实现思路是在 start_color 到 end_color 范围内设置一个渐变步长,这个范围和图形的宽度相同,用循环逐一绘制不同颜色的垂直线条。实现代码如下

class BlogCoverGenerator:
    def _display_gradient_bg(self, base_img: Image):
        img_w, img_h = base_img.size
        draw = ImageDraw.Draw(base_img)
        start_color, end_color = self.bg_gradient.value
        if '#' in start_color:
            start_color = ImageColor.getrgb(start_color)
        if '#' in end_color:
            end_color = ImageColor.getrgb(end_color)

        # 水平方向渐变,渐变步长
        step_r = (end_color[0] - start_color[0]) / img_w
        step_g = (end_color[1] - start_color[1]) / img_w
        step_b = (end_color[2] - start_color[2]) / img_w

        for i in range(0, img_w):
            bg_r = round(start_color[0] + step_r * i)
            bg_g = round(start_color[1] + step_g * i)
            bg_b = round(start_color[2] + step_b * i)
            draw.line([(i, 0), (i, img_h)], fill=(bg_r, bg_g, bg_b))

这一步的效果图如下
Step-01

2.装饰图形层

装饰图形层的实现代码很多,这里只介绍思路

定义了一个装饰图形枚举

from enum import Enum
class BackgroundShape(Enum):
    circle = 1
    triangle = 2
    square = 3
    hexagon = 4

因为要绘制的装饰图形是带透明度的,所以要用如下方法把半透明图形混合到底下的渐变背景图层上

Image.alpha_composite(base_img, img_shape)

此外, 最上边的文字层是核心内容,装饰图形层不能盖住文字区域,控制文字区域的参数是 title_h_ratio

Pillow 的 ImageDraw 类有一个绘制正多边形的方法 regular_polygon(), 但是这个方法不支持设置轮廓的宽度,也就是没有提供 width 参数(默认值为1),而这个功能却要指定轮廓宽度。如果用 ImageDraw 的普通方法 polygon(),需要指定各个顶点的坐标,如果多边形要旋转,难度可见一斑

因此,这里用了一个变通的方法,具体实现如下

BlogCoverGenerator
    @staticmethod
    def _width_regular_polygon(draw: ImageDraw, width: int,
                               bounding_circle, n_sides, rotation=0, fill=None, outline=None):
        """
        pillow提供的regular_polygon,不支持对outline设置width,自定义方法,支持轮廓宽度
        """
        start = bounding_circle[2]
        for i in np.arange(0, width, 0.05):
            new_bounding_circle = (bounding_circle[0], bounding_circle[1], start + i)
            draw.regular_polygon(bounding_circle=new_bounding_circle, n_sides=n_sides,
                                 rotation=rotation, fill=fill, outline=outline)

这里的 bounding_circle 参数是一个 tuple 类型 (x, y, r),定义了多边形的外切圆, (x, y) 是圆心坐标,r 是外切圆半径

绘制圆环时调用的另一个方法 ImageDraw.ellipse(),这里需要传入左上角和右下角坐标,可以转化为 bounding_circle,这样就可以和多边形复用位置参数了

这一步的效果图如下(以绘制圆环为例)
Step-02

3.半透明遮罩层

这一步很简单,直接上代码

class BlogCoverGenerator:
    @staticmethod
    def _display_transparent_mask(base_img: Image):
        img_w, img_h = base_img.size
        img_mask = Image.new('RGBA', (img_w, img_h), color=(0, 0, 0, 135))
        return Image.alpha_composite(base_img, img_mask)

4. 文字层

这一步有2个要点,其一,根据标题文字确定选择中文字体还是英文字体

class BlogCoverGenerator:
    def _get_real_font(self, target_title: str, font_size: int):
        for ch in target_title:
            if u'\u4e00' <= ch <= u'\u9fff':
                # 中文字体
                return ImageFont.truetype(self.zh_font_location, font_size)
        # 英文字体
        return ImageFont.truetype(self.en_font_location, font_size)

    def set_fonts(self, zh_font_location: str, en_font_location: str):
        self.zh_font_location = zh_font_location
        self.en_font_location = en_font_location

其二,动态调整字体大小,因此有一个预渲染并检查字体宽度的过程,会使用到 ImageFont.getbbox()

class BlogCoverGenerator:
    def _display_title(self, base_img: Image):
        def get_checked_font_size(target_title: str, font_size: int, max_width: int):
            # 预检查,判断文字宽度是否超出封面
            check_font = self._get_real_font(target_title, font_size)
            _, _, check_w, check_h = check_font.getbbox(target_title)
            if check_w > max_width:
                scale_ratio = max_width / check_w
                font_size = int(font_size * scale_ratio)
            return font_size

这一步的效果图,也就是最终效果了
Step-04

四、完整代码

代码量300+,免费下载请移步CSDN下载地址

参考文档

1.Pillow官网ImageDraw模块

2.如何用Pillow库制作渐变色图片并添加文字

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一束尘光

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值