🆗

Python-Pillowで文字列をきちんと描画するのは大変だったけど、8.0.0のtextbboxで改善した話

2021/01/11に公開

Python-Pillow で文字を描画する領域サイズを得るにはPIL.ImageDraw.textsize()/PIL.ImageDraw.multiline_textsize()を使っていた(最近では multiline は気にしなくてよくなっているけど)。しかし、こいつが色々と問題ありで、マルチラインで取ってきて最終行にjなどがあると下にはみ出してしまったり、日本語フォントでもはみ出たり隙間だらけだった。j/Jの飾り字などでは左にはみ出る場合もある。もっと他の言語だと色々あるかもしれない。

この辺りは https://freetype.org/freetype2/docs/tutorial/step2.html にあるフォントグリフのメトリクスの関係やらリガチャやカーニング、縦書き関係(Pillow で縦書き使うのは大変だけど)とか色々あって本当にややこしい。

そんなあれこれの一部は"historical reasons"とされてしまっているので修正はあきらめた方が良さそうだ。

しかし、Pillow は 8.0.0 から PIL.ImageDraw.textbbox()/PIL.ImageDraw.multiline_textbbox() が追加されている。

https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.textbbox

以下は Windows での例。ITCBLKAD.TTF がいい感じに左や下にはみ出てるフォントなのでちょうど良かった。最後に img を置いてるのは Jupyter notebook で表示しているため。

from PIL import Image, ImageDraw, ImageFont

img = Image.new("RGB", (800, 600))
draw = ImageDraw.Draw(img)

if True:
    bbox_fill = None
    bbox_outline = (255, 0, 0)
else:
    bbox_fill = (0, 0, 0)
    bbox_outline = None

    
font = ImageFont.truetype("meiryo.ttc", 16)
draw.text((10, 20), "ImageDraw.multiline_text/.multiline_textsize", font=font)
draw.text((10, 170), "ImageDraw.multiline_text/.textbbox", font=font)
draw.text((10, 320), "ImageDraw.multiline_text/.textbbox with Adjust", font=font)
    
for text, fontname, x in [("A", "ITCBLKAD.TTF", 250), ("A\nj", "ITCBLKAD.TTF", 300), ("日本語も\n可能なり", "meiryo.ttc", 350)]:
    font = ImageFont.truetype(fontname, 40)

    tl = (x, 50)
    draw.multiline_text(tl, text, font=font)
    sz = draw.multiline_textsize(text, font=font)
    draw.rectangle((tl, (tl[0] + sz[0], tl[1] + sz[1])), fill=bbox_fill, outline=bbox_outline)
    draw.line((tl, (tl[0], tl[1] - 5)))

    tl = (x, 200)
    draw.multiline_text(tl, text, font=font)
    bbox = draw.multiline_textbbox(tl, text, font=font)
    draw.rectangle(bbox, fill=bbox_fill, outline=bbox_outline)
    draw.line((tl, (tl[0], tl[1] - 5)))

    tl = (x, 350)
    bbox = draw.multiline_textbbox((0, 0), text, font=font)
    draw.multiline_text((tl[0] - bbox[0], tl[1] - bbox[1]), text, font=font)
    draw.rectangle(((tl[0], tl[1]), (tl[0] + bbox[2] - bbox[0], tl[1] + bbox[3] - bbox[1])), fill=bbox_fill, outline=bbox_outline)
    draw.line(((tl[0], tl[1] - 5), (tl[0], tl[1]), (tl[0] - 5, tl[1])))

img

見ての通り、textbboxは文字の描画領域を覆いつくすバウンディングボックスを得られている。ただ、この場合、jが入るときや日本語の一行目などで top-left の指定がズレてしまっている。きっちり書きたい場合は 3 番目の書き方のように補正してあげれば良い。とはいえ、3 番目の 2 つ目の文字列も j の位置に引っ張られて A の位置がどうなの?って感じもあるし、フォントをきっちり正しく描画するのはやっぱり難しい。

ちなみに、8.0.0 ではPIL.ImageDraw.textlength()も追加されていて 1/64 pixel 精度でテキスト描画幅が取れるらしい。そんな細かく取って何が嬉しいかというと、部分文字列で幅を取って足し算が出来る…けど、カーニングなども考えないとダメだよ、ってドキュメントに書いてある。結局何に使うのがいいか謎すぎる。

Discussion