cloverrose's blog

Python, Machine learning, Emacs, CI/CD, Webアプリなど

Pillow(PIL)で文字列を画像として保存(位置・サイズ自動調整)

背景

ゆゆ式ゆゆ式、すきすき(ゆゆ式が大好きという意)で何か作ろうと思ってゆゆ式アイコンを作りました。
ゆゆ式アイコンが50個に到達した記念として、アイコンが「せーのっ!」「「ゆゆ式!」」ってしゃべるようなエフェクトを付けました。


セリフのフォントはこのTweetを参考にやさしさゴシックを使うことにしました。



やさしさゴシックはWebフォントが無いので、このフォントを使うためにセリフを画像として保存することにしました。
最初はGIMPで背景を吹き出しにして、その上になるべく大きな文字で・ちょうど真ん中になるように手作業でセリフを入力して作っていました。

セリフの自動化

先日ついにゆゆ式アイコンが100個に到達したので、また何か面白いことをしようと思って、アイコンがしゃべるセリフをWebから投稿できるようにしようと思いました。

そこで次のようなスクリプトを書きました。
やってることは次の2つです
1. 吹き出しに収まるなるべく大きなフォントサイズの計算
2. 吹き出しのちょうど真ん中に配置

# -*- coding:utf-8 -*-
from __future__ import division
from PIL import Image, ImageDraw, ImageFont
import math


max_font_size = 25
min_font_size = 6
diameter = 147
space_between_lines = 2
margin = 5


def render(texts, filename):
    # http://librabuch.jp/2013/05/python_pillow_pil/
    text_canvas = Image.new('RGBA', (diameter, diameter), (0, 0, 0, 0))
    draw = ImageDraw.Draw(text_canvas)
    font_size, left, top = calc_font_size(texts)
    font = make_font(font_size)
    height = top
    for text in texts:
        draw.text((left, height), text, font=font, fill='#303030')
        height += font.getsize(text)[1] + space_between_lines
    text_canvas.save(filename, 'PNG', optimize=True)


def make_font(font_size):
    return ImageFont.truetype('/usr/local/share/fonts/07Yasashisa.ttf', font_size)


def inner(texts, font_size):
    font = make_font(font_size)
    sizes = [font.getsize(text) for text in texts]
    w = max([size[0] for size in sizes])
    h = sum([size[1] for size in sizes]) + space_between_lines * (len(texts) - 1)
    d = math.sqrt(w ** 2 + h ** 2)
    return w, h, d


def calc_font_size(texts):
    for font_size in range(max_font_size, min_font_size - 1, -1):
        w, h, d = inner(texts, font_size)
        if w < diameter - 2 * margin and h < diameter - 2 * margin and d < diameter - 2 * margin:
            left = (diameter - 2 * margin - w) / 2 + margin
            top = (diameter -2 * margin - h) / 2 + margin
            return font_size, left, top
    raise Exception('texts are too long')