Python
GoogleAppEngine
PIL

PIL 1.1.7 で日本語縦書き描画を頑張る

要約

  • Google App Engine の Standard Environment (Python2.7) 上のシステムで日本語縦書きテキストの画像を生成したかった。
    • GAE 上では PIL 1.1.7 しか使えないが、縦書きサポートをしていない。
  • 頑張った結果、それなりの縦書き画像を生成できた。

PIL と GAE の制限

Python で画像操作をするライブラリとして老舗なのが PIL (Python Imaging Library) です。しかし、2009年にリリースされた 1.1.7 で開発が止まっており、現在は PIL から fork した Pillow を使うケースが多いかと思います。

しかし、GAE の Standard Environment (SE) では、古い PIL 1.1.7 しかサポートしていません。この PIL 1.1.7 では、TrueType で描画は可能なものの、縦書きのサポートがありません。

SE では pure python のライブラリはユーザが追加することができますが、ネイティブコードを含むライブラリは公式に提供されている物しか使えないという制約があります。(その代わりに、インスタンスの高速起動という大きなメリットを享受できます)

Flexible Environment を使えば、このあたりの制約もなくなるのですが、なんのために GAE を使っているのか分からなくなってきますので、できれば SE ですませたい、というのが、この記事の主題です。

1文字ずつ縦に並べてみる

とりあえず、1文字ずつ縦に並べて書いてみます。
プロポーショナルなレイアウトは諦めて、四角い等幅フォントを使うことで妥協しましょう。フォントはSourceHanSerifのttf版をこちらからDLしてきました。

# coding: utf-8
from PIL import Image, ImageFont, ImageDraw

FONT_SIZE = 60
LINE_HEIGHT = 80
BASE_LINE_OFFSET = -16

def draw_text_tate(image, font, coord, size, text):
    draw = ImageDraw.Draw(image)

    sx, sy = coord
    nx, ny = size
    ix, iy = 0, 0
    for c in text:
        if c == u"\n":
            ix += 1
            iy = 0
            continue

        if iy >= ny:
            ix += 1
            iy = 0
        if ix >= nx:
            # 表示が溢れた
            return False

        x = sx - ix * LINE_HEIGHT - FONT_SIZE
        y = sy + iy * FONT_SIZE

        char_width, char_height = font.getsize(c)
        x += (FONT_SIZE - char_width) / 2
        y += BASE_LINE_OFFSET

        draw.text((x, y), c, font=font)

        iy += 1

    return True

image = Image.new('L', (800, 600), 'white')
font = ImageFont.truetype('SourceHanSerifJP-ExtraLight.ttf', FONT_SIZE)
draw_text_tate(image, font, (780, 20), (9, 9), u"「けれどもほんとうのさいわいは一体何だろう。」ジョバンニが云いました。\n「僕わからない。」カムパネルラがぼんやり云いました。")
image.show()

描画結果

「けれどもほんとうのさいわいは一体何だろう。」ジョバンニが云いました。「僕わからない。」カムパネルラがぼんやり云いました。

想像通りの結果です。。。

縦書き用のフォントを使いたい

普段、何気なく、Word などで縦書きの文章を出力していますが、どういう仕組みなのでしょう。

https://lists.w3.org/Archives/Public/public-html-ig-jp/2011Oct/0005.html
で議論されている内容をチラ見すると、どうやら TrueType ファイルの中に、縦書き用のグリフが含まれているようです。
(実際に自分で取り出す実装をしたい場合はこちらの記事がリファレンスも豊富でとても参考になりそうです)

ただ、この "vert" という feature に PIL 1.1.7 は対応していません。困った。。。

ここで閃きます。もし、最初から縦書きのグリフしか入っていないフォントを作れたら、問答無用で縦書き用の出力ができるのでは!

縦書き専用フォントの作成

武蔵システムのサブセットフォントメーカーを使います。

使い方は簡単。適当なフォントを入力に入れ、文字組方向に「縦書き」を指定して出力するだけです。

ただ、このツールは本来は Web Font などで日本語フォントを利用する際に、ファイルサイズを小さくするために文字セットを絞るツールです。今回は文字セットを絞りたいわけではないのですが、文字集合は何かを指定する必要があります。

私はこちらで公開されていた「Shift JIS に含まれる文字の一覧」を使わせていただきました。丸付き数字などが含まれないとのことですので、必要に応じて適切な文字集合を追加してください。

縦書き専用フォントで出力してみた

「けれどもほんとうのさいわいは一体何だろう。」ジョバンニが云いました。「僕わからない。」カムパネルラがぼんやり云いました。

約物を詰めたい気持ちに駆られたりしますが、ザックリ作る範囲ではこんな物で良いでしょう。

あとは、禁則対応をしたり、どうしても気になるところを文字毎に微調整するコードを書いたりすることで、クォリティはさらに高まるかと思われます。

おまけ: 最新の Pillow を使えば縦書きできるの?

柔軟なフォントレイアウトをサポートする libraqm に対応した Pillow 4.2.0 以降では、おそらくもう少しましな対応があるはずなんですが、ダメかも?

libraqm をインストールするのが意外と面倒で試せていないのですが、 以下のコードでどうなるのか試してみたいところ。features=['vert']だけでも動いてくれると、上記の縦書き用フォントを別に書き出す作業が不要になります。

from PIL import Image, ImageDraw, ImageFont
image = Image.new('L', (600, 600), 'white')
draw = ImageDraw.Draw(image)
font = ImageFont.truetype('何かの日本語フォント.ttf', size=40, layout_engine=ImageFont.LAYOUT_RAQM)
draw.text((280, 0), u'「キャッシュディスペンサー。」', font=font, direction='ttb', features=['vert'])
image.show()