Python フォント一覧を画像で出力する方法。

Python の画像処理ライブラリPillowを使って、OSにインストールされているフォントを
ひつとつひとつ画像に描画して、一覧出力する方法を紹介します。

私の場合、Macで実行していますが、たぶんWindowsでも動くと思います。

実行環境

  • macOS Catalina 10.15
  • Python 3.9.2
  • Pillow 8.2

フォントのインストール場所

OS毎にフォントがインストールされている場所は異なります。

Mac

  • /System/Library/Fonts
  • /Library/Fonts
  • ~/Library/Fonts

Windows

  • %WINDIR%\fonts

Linux

  • /usr/share/fonts
  • XDG_DATA_DIRS環境変数で定義されたディレクトリのfonts配下

インストールディレクトリの取得

前述した、フォントのインストール場所を取得するコードは下記のようになります。

def font_installed_dirs() -> list[str]:
    """
    インストールされているフォントのディレクトリ取得
    """

    if sys.platform == "win32":
        d = os.environ.get("WINDIR")
        return [os.path.join(d, "fonts")]

    if sys.platform in ("linux", "linux2"):
        dirs = os.environ.get("XDG_DATA_DIRS", "")
        if not dirs:
            dirs = "/usr/share"
        return [os.path.join(d, "fonts") for d in dirs.split(":")]

    if sys.platform == "darwin":
        return ["/Library/Fonts",
                "/System/Library/Fonts",
                os.path.expanduser("~/Library/Fonts")]

    raise Exception(f"unsupported platform:{sys.platform}")

フォントファイル一覧取得

インストールされているフォントファイル一覧を取得してみましょう。
先程の、ディレクトリ配下のファイル一覧を取得するだけです。

def font_path_list() -> list[str]:
    """
    インストールされているフォントのPATHリストを取得
    """

    # フォントがインストールされているディレクトリリスト取得
    dirs = font_installed_dirs()

    # os.walkを使って、ファイル一覧を取得する
    l: list[str] = []
    for d in dirs:
        for parent, _, filenames in os.walk(d):
            for name in filenames:
                l.append(os.path.join(parent, name))

    return l

インストールディレクトリは、前述したfont_installed_dirs関数を使って取得。
os.walkを使ってサブディレクトリも含めて、全ファイルの一覧を取得します。

フォントの描画

取得したフォントファイルを使って、サンプルのテキストを画像に描画していきます。
画像ライブラリは、Pillowを使います。

単純に画像にテキストを描画するのは下記のとおり。

# ファイル名は正確に
font_path = 'ヒラギノ丸ゴ ProN W4.ttc'
font_size = 28

# 適当なサイズのイメージを作成
image = Image.new('RGBA', (600, 200), 'white')
draw = ImageDraw.Draw(image)

# 第一引数には、
# フォントファイルのフルパスか、ファイル名を指定すればよい。
font = ImageFont.truetype(font_path, font_size)

# フォントを使って描画
draw.text((0, 0),
          f"{font_path}\nサンプルのテキストを表示\nfont_size:{font_size}",
          font=font,
          fill='black')

# 画像ファイルに保存
image.save("sample.png", "PNG")

ImageFont.truetype関数は、
TrueTypeフォントか、OpenTypeフォントをロードする為の関数です。 

フォントを指定する時は、フォントのファイル名(拡張子含む)か、フルパスで指定できます。
OSのインストールディレクトリから自動で取得してくれます。

フォント名は正確に

フォントによっては、ファイル名の濁点が全角文字で表記されているものもあるので、注意して指定する必要があります。

Macの場合だけだと思いますが、
Finderで見てるファイル名と、実際のファイル名が違っているので、
本当のパス名称で正確に指定する必要があります。

Finderで見たファイル名

lsコマンドで見たファイル名

濁点が全角になっているのが本物のファイル名です。
(プログラム中で指定する時はこちらで指定する)

もし、ファイル名が間違ってる場合は下記のようなエラーがでると思います。
フォントが見つからずに読み込みに失敗する。

Traceback (most recent call last):
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 853, in truetype
return freetype(font)
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 850, in freetype
return FreeTypeFont(font, size, index, encoding, layout_engine)
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 209, in init
self.font = core.getfont(
OSError: cannot open resource

完成コード

最終的な完成コードを掲載しておきます。

import shutil
import sys
import os

from PIL import ImageDraw, Image, ImageFont

def font_installed_dirs() -> list[str]:
    """
    インストールされているフォントのディレクトリ取得
    """

    if sys.platform == "win32":
        d = os.environ.get("WINDIR")
        return [os.path.join(d, "fonts")]

    if sys.platform in ("linux", "linux2"):
        dirs = os.environ.get("XDG_DATA_DIRS", "")
        if not dirs:
            dirs = "/usr/share"
        return [os.path.join(d, "fonts") for d in dirs.split(":")]

    if sys.platform == "darwin":
        return ["/Library/Fonts",
                "/System/Library/Fonts",
                os.path.expanduser("~/Library/Fonts")]

    raise Exception(f"unsupported platform:{sys.platform}")

def font_path_list() -> list[str]:
    """
    インストールされているフォントのPATHリストを取得
    """

    # フォントがインストールされているディレクトリリスト取得
    dirs = font_installed_dirs()

    # os.walkを使って、ファイル一覧を取得する
    l: list[str] = []
    for d in dirs:
        for parent, _, filenames in os.walk(d):
            for name in filenames:
                l.append(os.path.join(parent, name))

    return l

def show_fonts():
    """
    フォント毎のサンプル画像を出力します。
    """
    font_size = 28

    # 出力ディレクトリの再作成
    output_dir = os.path.join("out", "font_samples")
    shutil.rmtree(output_dir)
    os.mkdir(output_dir)

    for f in font_path_list():
        font_name = os.path.basename(f)
        image = Image.new('RGBA', (600, 200), 'white')
        draw = ImageDraw.Draw(image)

        try:
            font = ImageFont.truetype(f, font_size)
            draw.text((0, 0),
                      f"{font_name}\nサンプルのテキストを表示\nfont_size:{font_size}",
                      font=font,
                      fill='black')
        except OSError as err:
            # エラーがでた場合、スキップ
            print(f"failed to draw with font:{f} caused by {err}", file=sys.stderr)
            continue

        # 画像保存
        image.save(os.path.join(output_dir, font_name + ".png"), "PNG")

        # パスをテキストで保存
        with open(os.path.join(output_dir, font_name + ".txt"), "w") as fp:
            fp.write(f"{f}\n")
            fp.write(f"{font_name}\n")

if __name__ == '__main__':
    show_fonts()

フォントによっては、下記のようなエラーがでる事があります。
Pillowが対応できないフォントのようで、
今回のサンプルでは、try-exceptして、処理をスキップするようにしています。

Traceback (most recent call last):
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 887, in truetype
return freetype(os.path.join(walkroot, walkfilename))
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 850, in freetype
return FreeTypeFont(font, size, index, encoding, layout_engine)
File "/Users/wiz/sample/venv/lib/python3.9/site-packages/PIL/ImageFont.py", line 209, in init
self.font = core.getfont(
OSError: invalid pixel size

まとめ

上記のスクリプトを実行すると、カレントディレクトリに、out/font_samplesというディレクトリが作成され、pngとtxtファイルがフォント毎に出力されます。

あと、サンプル描画した際に、日本語部分がブランクになるのは日本語に対応していないフォントという事です。
たとえば、Impactフォントではアルファベットは出力されますが、日本語はブランクになります。

一般的なアプリであればその場合、代替のフォントで描画するようにしているのでしょう。

Python/Pillowでそのあたりよしなにやってくれる方法があるのかどうか。。

タイトルとURLをコピーしました