【Pythonでサムネ制作】PILで画像の上に文字を重ねて中央表示
ブログや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つです。
/System/Library/Fonts
/Library/Fonts
/Users/ユーザ名/Library/Fonts
フォントが多すぎると画像編集ソフトでいちいち迷うので、使わないフォントは削除するようにしてますが、それでもこれだけたくさんの種類がインストールされてます。
$ 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_x
と offset_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で管理してWatchdogで自動生成
文言の修正や色使いなどを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
プログラムの詳細は解説しませんが、一つ一つの技術は難しいものではありません。