【Pythonでサムネ制作】PILで画像の上に文字を重ねて中央表示

著者画像
Toshihiko Arai

ブログやYouTubeで使うサムネを、画像編集ソフトを使わずにプログラミングでサクッと制作できたらいいですよね! とくに私なんかは、このブログサイトのアイキャッチ画像をPythonで制作できたら最高と思うのです。

さて、続編があるかどうかわかりませんが、Pythonでサムネ制作の第一弾としまして「画像の上に文字を重ねて中央表示する」をやってみました。

普段の作業が大幅に短縮できそうです。クリエイター&プログラマーの方必見の内容です!

ゴール

こんな感じのベースとなる画像を用意しまして、、、 元となる画像

それがこうなることを目指して進めていきます! テキストを中央に表示

ちなみにCSSでテキストを重ねるやり方もあります。

つかうもの

Python3をご用意ください。

Pillow

また、Pillow(PIL)という画像処理によく使われるモジュールが必要です。pipなどでインストールしておきましょう。

Pillow is the friendly PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.

https://pillow.readthedocs.io/en/stable/

Pillowのインストール

$ pip3 install Pillow

画像の上に文字を表示

それではさっそく、画像の上に文字を表示してみます。 次のようなプログラムを作成し実行してみてください。

from PIL import Image,ImageFont,ImageDraw
 
image_path = 'fuji.jpg' # 文字を重ねる画像
out_path = 'a.jpg' # 出力先

font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W5.ttc'
font_size = 100
font_color = (255,255,255) #文字の色
text = '新倉山浅間公園'
pos = (200,100)

font = ImageFont.truetype(font_path, font_size)

image = Image.open(image_path)
draw = ImageDraw.Draw(image)

draw.text(pos, text, font=font, fill=font_color)
image.save(out_path)

するとこうなります。画像の上にテキストが表示できましたね!

画像の上に文字を表示

お使いのOSによってフォントの種類や場所は違います。私はmacOSですので、フォントの場所は次のようになってます。

macOSのフォントの保存場所

macOSのフォントの保存場所は主に次の3つです。

フォントが多すぎると画像編集ソフトでいちいち迷うので、使わないフォントは削除するようにしてますが、それでもこれだけたくさんの種類がインストールされてます。

$ ls /System/Library/Fonts/
Apple Braille Outline 6 Dot.ttf         NotoSansKannada.ttc
Apple Braille Outline 8 Dot.ttf         NotoSansMyanmar.ttc
Apple Braille Pinpoint 6 Dot.ttf        NotoSansOriya.ttc
Apple Braille Pinpoint 8 Dot.ttf        NotoSerifMyanmar.ttc
Apple Braille.ttf               Optima.ttc
Apple Color Emoji.ttc               Palatino.ttc
Apple Symbols.ttf               PingFang.ttc
AppleSDGothicNeo.ttc                SFArabic.ttf
AquaKana.ttc                    SFCompact.ttf
ArialHB.ttc                 SFCompactItalic.ttf
Avenir Next Condensed.ttc           SFCompactRounded.ttf
Avenir Next.ttc                 SFNS.ttf
Avenir.ttc                  SFNSItalic.ttf
Courier.ttc                 SFNSMono.ttf
GeezaPro.ttc                    SFNSMonoItalic.ttf
Geneva.ttf                  SFNSRounded.ttf
HelveLTMM                   STHeiti Light.ttc
Helvetica.ttc                   STHeiti Medium.ttc
HelveticaNeue.ttc               Supplemental
Hiragino Sans GB.ttc                Symbol.ttf
Keyboard.ttf                    Thonburi.ttc
Kohinoor.ttc                    Times.ttc
KohinoorBangla.ttc              TimesLTMM
KohinoorGujarati.ttc                ZapfDingbats.ttf
KohinoorTelugu.ttc              ヒラギノ丸ゴ ProN W4.ttc
LastResort.otf                  ヒラギノ明朝 ProN.ttc
LucidaGrande.ttc                ヒラギノ角ゴシック W0.ttc
MarkerFelt.ttc                  ヒラギノ角ゴシック W1.ttc
Menlo.ttc                   ヒラギノ角ゴシック W2.ttc
Monaco.ttf                  ヒラギノ角ゴシック W3.ttc
MuktaMahee.ttc                  ヒラギノ角ゴシック W4.ttc
NewYork.ttf                 ヒラギノ角ゴシック W5.ttc
NewYorkItalic.ttf               ヒラギノ角ゴシック W6.ttc
Noteworthy.ttc                  ヒラギノ角ゴシック W7.ttc
NotoNastaliq.ttc                ヒラギノ角ゴシック W8.ttc
NotoSansArmenian.ttc                ヒラギノ角ゴシック W9.ttc
$ ls /Library/Fonts/
Arial Unicode.ttf   BROOKH2.ttf     BauhausBoldItalic.ttf   BauhausRegular.ttf
BAUHHL.ttf      BauhausBold.ttf     BauhausItalic.ttf
$ ls /Users/mopipico/Library/Fonts/
07にくまるフォント.otf          YAKITORI.TTF
AoyagiKouzanTOTF.otf            aoyagireisyosimo_otf_2_01.otf
HalfFont-Regular.ttf            gomarice_mukasi_mukasi.ttf
KsoKagerou.otf              mokkumokumo.ttf
KsoKagerouN.otf             pupupu-free.otf
KsoTouryu.otf               瀞ノグリッチ黒体H1.otf
KsoTouryuN.otf              瀞ノグリッチ黒体H2.otf
NikkyouSans-mLKax.ttf           瀞ノグリッチ黒体H3.otf
SeimeiKana-Free.otf         瀞ノグリッチ黒体H4.otf

フォントのタイプ

フォントのタイプは大雑把に次のとおりです。

拡張子 意味
.ttf TrueTypeフォント
.otf OpenTypeフォント
.ttc TrueTypeフォントを1つのファイルにまとめたもの
.otc OpenTypeフォントを1つのファイルにまとめたもの

他にもフォントタイプには、ウェブブラウザでよく使われるeot, woff, woff2などがあります。

画像の「中央」にテキストを表示したい

先ほどのプログラムでは位置を自分で指定しなければなりませんでした。これだと画像の「中央」にテキストを表示したい場合に具合が悪いです。 そこで画像サイズとテキストの縦横幅を取得して、計算して「中央」にテキストを表示できるようにしてみました。

from PIL import Image,ImageFont,ImageDraw
 
image_path = 'fuji.jpg' # 文字を重ねる画像
out_path = 'b.jpg' # 出力先

font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W5.ttc'
font_size = 100
font_color = (255,255,255) #文字の色
text = '新倉山浅間公園'

font = ImageFont.truetype(font_path, font_size)

image = Image.open(image_path)
draw = ImageDraw.Draw(image)

(font_w, font_h), (offset_x, offset_y) = font.font.getsize(text)
img_w, img_h  = image.size

print("{},{},{},{}".format(font_w, font_h, img_w, img_h))


pos = ((img_w - font_w) / 2, (img_h - font_h) / 2)

draw.text(pos, text, font=font, fill=font_color)
image.save(out_path)
テキストを中央に表示

はい。いいかんじでキレイに画像の中央にテキストを表示できましたね。

この部分でフォントの縦横幅を取得しているのがポイントです。

(font_w, font_h), (offset_x, offset_y) = font.font.getsize(text)
img_w, img_h  = image.size

offset_xoffset_y に関しては下記URLで詳しく説明されてますのでご参考に。

https://stackoverflow.com/questions/43060479/how-to-get-the-font-pixel-height-using-pils-imagefont-class

【おまけ】テキストに枠線をつけたい

さいごに、テキストの枠線を表現できたら良いですよね?borderとかstroke、outlineとか呼び方はさまざまありますが、よく使われるアレです。

枠線を処理するにはこんな感じのプログラムでいけちゃいます。

from PIL import Image,ImageFont,ImageDraw
 
image_path = 'fuji.jpg' # 文字を重ねる画像
out_path = 'c.jpg' # 出力先

font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W7.ttc'
font_size = 100
font_color = (255,255,255) # 文字の色
stroke_color = (33, 33, 33) # 枠線の色
text = '新倉山浅間公園'

stroke_width = 10

font = ImageFont.truetype(font_path, font_size)

image = Image.open(image_path)
draw = ImageDraw.Draw(image)

font_w, font_h = font.getsize(text, stroke_width=stroke_width)
img_w, img_h  = image.size

print("{},{},{},{}".format(font_w, font_h, img_w, img_h))

pos = ((img_w - font_w) / 2, (img_h - font_h) / 2)

draw.text(
    pos, text,
    font=font, 
    fill=font_color,
    stroke_width=stroke_width,
    stroke_fill=stroke_color)
image.save(out_path)

デザイン性はともあれ、なんとか枠線がつきました!

テキストに枠線を追加

いかがだったでしょうか? Pillowを使って簡単に画像に文字を重ねることができました。この他にも画像に画像を重ねたり、図形を描画したりもできるはずです。

【Pythonでサムネ制作②】PILで画像の上に透過画像を重ねる

次に、こんなことやります。

文言の修正や色使いなどをYAMLで管理して、YAMLが更新されたらWatchdogでイベントフックしてコンパイルさせてます。

画像の上に画像を重ねる

それではさっそく、画像の上に画像を重ねていきます。まずは「ベースとなる画像」とその上に「重ねたい画像」を用意します。

ベースとなる画像

↑この画像に↓こちらのロゴ画像を重ねていきます。ロゴ画像はPNGファイルで作られており、実際の背景は透過であります。

重ねたい画像

プログラムはこんな感じです。めちゃ簡単ですね。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)

# base.paste(logo, (0, 0))
base.paste(logo, (0, 0), logo)
base.save(out_path)

プログラムを実行すると↓こんな感じで画像の上に画像を重ねることができました。

出力結果

プログラミングのポイント

プログラム中の paste() は、画像に画像を重ねるためのメソッドです。重ねたい画像を第一引数に指定し、位置をタプルで第二引数にしていします。

また、paste() メソッドの第三引数はマスク画像を指定するためのものです。透過画像自身をマスク画像とすることで、透明部分が表現できているワケですね!

仮に、paste() メソッドの第三引数を省略して出力してみますと、↓下の写真のように透過部分が黒く塗りつぶされてしまいます。

マスクをかけないと透過が有効にならない

画像の位置を調整

次に重なる画像の位置を調整してみます。左下にロゴ画像を配置したいので、↓こんな感じでプログラミングしてみました。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)

base_w, base_h  = base.size
logo_w, logo_h  = logo.size

# print('base_w:{}, base_h:{}'.format(base_w, base_h))
# print('logo_w:{}, logo_h:{}'.format(logo_w, logo_h))

base.paste(logo, (0, base_h - logo_h), logo)
base.save(out_path)

プログラムを実行します。左下にロゴが表示されましたね。

出力結果

画像の大きさをリサイズして重ねる

今度は重なる画像の大きさをリサイズして重ねてみましょう。画像を少し大きくして重ねるにはこんな感じのプログラミングになります。

from PIL import Image
 
base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル

base = Image.open(base_path)
logo = Image.open(logo_path)


base_w, base_h  = base.size
logo_w, logo_h  = logo.size

scale = 1.5
logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
logo_resized_w, logo_resized_h  = logo_resized.size

base.paste(logo_resized, (0, base_h - logo_resized_h), logo_resized)
base.save(out_path)

はい。↓ロゴが少し大きくなって重ねることができました。 出力結果

プログラミングのポイント

resize() メソッドにタプルでリサイズしたいwidth、heightの値を渡します。ただしint型でないとエラーになりますのでご注意ください。 ちなみに、画像の一部分をくり抜きたい場合は crop() を使います。

透過画像と文字を画像の上に重ねる

応用編としまして、テキストも組み合わせて、透過画像と文字を画像の上に重ねてみましょう。

位置決めなどはハードコーディングで調整する必要がありますが、できるだけ関数にまとめました。

from PIL import Image,ImageFont,ImageDraw


def overlay_text(base_img, text, font_path, font_size, font_color, stroke_color, stroke_width):
    font = ImageFont.truetype(font_path, font_size)
    draw = ImageDraw.Draw(base_img)

    font_w, font_h = font.getsize(text, stroke_width=stroke_width)
    base_img_w, base_img_h  = base_img.size
    margin_left = 40
    margin_bottom = 50
    
    pos = (margin_left, base_img_h - font_h - margin_bottom)

    draw.text(
        pos, text,
        font=font, 
        fill=stroke_color,
        stroke_width=stroke_width * 2,
        stroke_fill=font_color)

    draw.text(
        pos, text,
        font=font, 
        fill=font_color,
        stroke_width=stroke_width,
        stroke_fill=stroke_color)

    return base_img

def overlay_logo(base_img, path, scale):
    logo = Image.open(path)
    base_w, base_h  = base_img.size
    logo_w, logo_h  = logo.size

    logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
    logo_resized_w, logo_resized_h  = logo_resized.size
    margin_bottom = 170
    base_img.paste(logo_resized, (0, base_h - logo_resized_h - margin_bottom), logo_resized)
    return base_img


base_path = 'panda.jpg' # ベース画像
logo_path = 'python-logo.png' # 重ねる透過画像
out_path = 'out.jpg' # 出力ファイル


text = 'Pandasでデータ解析・使い方'
font_path = '/System/Library/Fonts/ヒラギノ角ゴシック W7.ttc'
font_size = 100
font_color = (255,248,196) # 文字の色
stroke_color = (112, 96, 85) # 枠線の色
stroke_width = 10


base = Image.open(base_path)
base = overlay_logo(base, logo_path, 1.0)
base = overlay_text(base, text, font_path, font_size, font_color, stroke_color, stroke_width)
base.save(out_path)

はい。↓サムネの完成でございます。

出力結果

これでサムネを半自動化で作れるようになりましたね!

プログラミングのポイント

テキストの枠線を二重に重ねるには、 draw.text()stroke_width の太さを変えて二回呼び出しているのがポイントです。

YAMLで管理してWatchdogで自動生成

さいごに「おまけ」としまして、このサイトを生成している自作ジェネレータのサムネ自動化プログラムをご紹介します。ここまで解説した技術を組み合わせてサムネを自動生成させてます。


import yaml
import os
import conf
from PIL import Image,ImageFont,ImageDraw
import time
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler


class WatchdogHandler(PatternMatchingEventHandler):
    def __init__(self, callback, patterns):
        super(WatchdogHandler, self).__init__(patterns=patterns)
        self.callback = callback

    def __callback_handler(self, func,*args):
        return func(*args)

    def on_modified(self, event):
        print(event)
        self.__callback_handler(self.callback)


def watch(path, command, extensions):
    event_handler = WatchdogHandler(command, extensions)
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()



def __get_fit_font_size(text, font_path, stroke_width, font_size = 200, limit_width = 1920, margin = 200):
    font = ImageFont.truetype(font_path, font_size)
    w, h = font.getsize(text, stroke_width=stroke_width)
    if w < (limit_width - margin):
        return font_size
    print('{}, {}, {}, {}'.format(text, font_path, stroke_width, font_size - 5))
    return __get_fit_font_size(text, font_path, stroke_width, font_size - 5)


def __overlay_text(base_img, text, font_path, font_size, font_color, stroke_color, stroke_width):
    font = ImageFont.truetype(font_path, font_size)
    draw = ImageDraw.Draw(base_img)

    font_w, font_h = font.getsize(text, stroke_width=stroke_width)
    base_img_w, base_img_h  = base_img.size
    margin_left = 40
    margin_bottom = 50
    
    pos = ((base_img_w - font_w)/2, (base_img_h - font_h)/2)

    draw.text(
        pos, text,
        font=font, 
        fill=font_color,
        stroke_width=stroke_width,
        stroke_fill=stroke_color)

    return base_img

def __overlay_logo(base_img, path, scale):
    logo = Image.open(path)
    base_w, base_h  = base_img.size
    logo_w, logo_h  = logo.size
    logo_resized = logo.resize((int(logo_w * scale), int(logo_h * scale))) # リサイズ
    logo_resized_w, logo_resized_h  = logo_resized.size
    margin_right = 20
    margin_bottom = 50
    base_img.paste(logo_resized, (base_w - logo_resized_w - margin_right, base_h - logo_resized_h - margin_bottom), logo_resized)
    return base_img


def make(target_dir):
    yml_path = target_dir + '/' + 'eyecatch.yml'
    with open(yml_path) as file:
        cf = yaml.safe_load(file)

    base_path = target_dir + '/' + cf['base_path']
    logo_path = conf.LOGO_DIR + '/' + cf['logo_path']
    out_path = target_dir + '/' + cf['out_path']

    text = cf['text']
    font_path = cf['font_path']

    font_color = tuple(cf['font_color'])
    stroke_color = tuple(cf['stroke_color'])
    stroke_width = cf['stroke_width']

    font_size = __get_fit_font_size(text, font_path, stroke_width)
    base = Image.open(base_path)
    base = __overlay_logo(base, logo_path, 0.9)
    base = __overlay_text(base, text, font_path, font_size, font_color, stroke_color, stroke_width)
    base.save(out_path)


class WatchdogHandler(PatternMatchingEventHandler):
    def __init__(self, patterns):
        super(WatchdogHandler, self).__init__(patterns=patterns)

    def on_modified(self, event):
        target_dir = os.path.dirname(event.src_path)
        make(target_dir)





if __name__ == "__main__":
    dir_to_watch = conf.SOURCES_DIR + '/blog'
    extensions = ["*.yml"]

    event_handler = WatchdogHandler(extensions)
    observer = Observer()
    observer.schedule(event_handler, dir_to_watch, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

↓ YAMLファイルはこんな感じです。

common_yaml: develop.yml
text: "画像を重ねる"
subtext: "Pythonでサムネ制作への道"
icon_path: file-format.png
stroke_width: 10

プログラムの詳細は解説しませんが、一つ一つの技術は難しいものではありません。

Amazonで探す