Pythonでテキストを画像化する

2022/10/25
吉川

こんにちは。株式会社リンクネット ソリューション事業部の吉川です。

弊社ではCreateJSを使って入力文字列を画像化するシステムを開発していましたが、諸事情によりサーバ側で画像化を行う必要が出てきました。
そこで候補に挙がったのがPythonでした。
Pythonでの画像化にあたってフォント周りで悩んだことがあったので書き記しておきます。

環境

$ python --version
Python 3.7.10

概要

同じパラメータで画像化したときに、なるべくCreateJSと相違ない画像になることを目指します。
絵文字も描画する必要があるため、NotoColorEmojiを使用することにしました。
フォントの情報を読み取るためにfonttoolsを使用し、画像化にはPillowを使用しました。

問題点

絵文字を描画しようとするとエラーになる

試しにハンバーガーの絵文字🍔をフォントサイズ75で描画しようとしたところ、OSError: invalid pixel sizeというエラーが発生しました。

from PIL import ImageFont
font = ImageFont.truetype('NotoColorEmoji.ttf', 75)  # OSError: invalid pixel size

エラー内容で調べてみると、以下のissueが見つかりました。
https://github.com/python-pillow/Pillow/issues/3346#issuecomment-612139601
ビットマップフォントは拡大・縮小ができず、有効なフォントサイズを指定する必要があるとのことです。
NotoColorEmojiの場合は109を指定すべきということなので、フォントサイズ109で描画した画像をリサイズすることできれいに表示できました。

from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype('NotoColorEmoji.ttf', 109)
width, height = font.getsize('🍔')  # 絵文字を描画したときの幅と高さ
img = Image.new('RGBA', (width, height), '#FFFFFF00')  # リサイズするためにいったん描画する透明な背景
ImageDraw.Draw(img).text((0, 0), '🍔', font=font, embedded_color=True)  # 背景に絵文字を描画
img.resize((round(width * 75 / 109), round(height * 75 / 109)))  # フォントサイズ75相当の大きさにリサイズ

絵文字とそれ以外の出し分け

PillowにはImageDraw.multiline_textというメソッドがあり、複数行のテキストを簡単に出力することができます。
今回は絵文字も描画しなくてはならないため、1文字ずつ絵文字かどうかを判定しながら描画しなくてはならず、ImageDraw.multiline_textは使えませんでした。
各行を書き始める位置を計算するためにImageFont.getbboxImageFont.getmetricsで取得した値を使っていましたが、
CreateJSの画像とずれがあり、最終的には以下のサイトを参考にフォントのテーブルに格納されている値を参照して位置を調整しました。
https://aznote.jakou.com/prog/opentype/index.html

一部のフォントで文字の左端が見切れてしまう

画像化したときに「A」の左下部分のような尖った部分が見切れてしまうフォントがありました。
他のフォントと見比べてみると、描画したときにバウンディングボックスの左端がマイナスの値になっていました。

from PIL import ImageFont
font = ImageFont.truetype('/path/to/font_file', 75)
font.getbbox('ABC')  # (-2, 19, 113, 78)

描画する文字によってもバウンディングボックスの左端の値が変わるため、全行描画してから最も小さい左端の値ぶんだけずらして配置することで対応しました。

まとめ

ここまでの対応でひとまずテキストの画像化ができるようになりました。
実際には、追加で背景色の設定やテキストに影をつけるといった装飾をする機能を実装しました。
普段ブラウザでサイトを閲覧する時には気にも留めませんでしたが、きれいに文字を表示するのはかなり大変だなと実感しました。

前の記事次の記事