Pillow の ImageDraw#textsize() が間違った値を返している件

Pillow 2.2.0 以降には ImageDraw#textsize() にバグがあり、TrueType フォントを指定していると正しい高さが返りません。

どういう問題なのか

テキストとそれを囲む四角を描画する次のコードを実行してみます。

from PIL import Image, ImageDraw, ImageFont

image = Image.new('RGB', (512, 256), (255, 255, 255))
drawer = ImageDraw.Draw(image)
font = ImageFont.truetype('/Library/Fonts/Osaka.ttf', 32)

# drawing text
STRING = 'Hello, python language!'
drawer.text((10, 10), STRING, fill='black', font=font)

# drawing rectangle surrounding text
size = drawer.textsize(STRING, font=font)
drawer.rectangle((10, 10, 10 + size[0], 10 + size[1]), outline='black')

image.save('example.png', 'PNG')

すると次のような図が出力されます。
f:id:tk0miya:20140712002748p:plain
カンマや y, g と言った文字の下辺が枠の外から飛び出しています。

この問題は TrueType フォントを表す PIL.ImageFont.FreeTypeFont クラスが文字の大きさを返す際に
offset 値を含めないサイズを返してしまっているために発生しています。

この問題はすでに github 上では修正されているのですが、
現在リリースされている Pillow-2.5.1 ではこの問題は解決されていません。

回避策

Pillow-2.2.0 以降には FreeTypeFont クラスに getoffset() というメソッドがあるので、この値を用いて補正します。

どのバージョンの Pillow でも動くようにするには以下のようなコードを書きます*1

from PIL import PILLOW_VERSION
from PIL.ImageFont import FreeTypeFont

def textsize(drawer, text, font=None):
    size = drawer.textsize(text, font)
    # Avoid offset problem in Pillow (>= 2.2.0, < 2.6.0)
    if isinstance(font, FreeTypeFont) and "2.2.0" <= PILLOW_VERSION < "2.6.0":
        offset = font.getoffset(text)
        size = (size[0] + offset[0], size[1] + offset[1])

    return size

まとめ

しばらくはバグったままなので、adhoc なコードを使って回避して生きていきましょう。

懺悔

このバグは日本語フォントが欠ける問題を直したところに、他の人が offset 値の調整を加えることで発生しているので
僕は(間接的ではあるものの)バグの生みの親ということになります。

大変申し訳無いです。

*1:まだ 2.6.0 はリリースされていませんが、次のリリースで直る想定で書かれています