slackAppで絵文字ジェネレータっぽいのを作る(Python&Gyazo&slack-bolt)
目次
概要
目標
salckでテキストを送ったらその絵文字画像を生成してほしい
- 完成例
使うもの
-
pillow(pythonのライブラリ)
- 絵文字画像生成を担当
-
gyazo-API
- 生成した絵文字画像をslackに共有する
-
slack_bolt
- slack上のメッセ―ジを受信、送信する
構成
- 絵文字画像を生成するemojicreate.py
- 生成した絵文字画像をgyazoに送り、そのリンクを取得するusegyazo.py
- アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行うapp.py
からなる
絵文字画像を生成する
まず該当コード
gitfrom PIL import Image, ImageDraw, ImageFont
import math
import traceback
# テキストの文字数が自身の平方数以下となる平方根nを見つける(10文字ならn=4)
def create(text):
row_text = text
print(row_text)
n = math.ceil(math.sqrt(len(row_text)))
print(n)
# nだけテキストを分割し、文字列末尾に\nを入れる
textlist = []
i = 0
while i < n:
textlist_element = row_text[i * n:(i * n) + n]
if textlist_element == "":
break
textlist.append(textlist_element)
i += 1
print(textlist)
newlined_text = ""
for j in textlist:
newlined_text += j + "\n"
print(newlined_text)
newlined_text = newlined_text[:-1]
# テキストの矩形最大幅がimage.size[0]*img_fraction を上回るまでフォントサイズを挙げていく
out = Image.new("RGB", (256, 256), (256, 256, 256))
d = ImageDraw.Draw(out)
fontsize = 1
img_fraction = 0.9
cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
anchor="mm", font=cur_fnt, spacing=5)
# 矩形サイズ取得
while out.size[0] * img_fraction > (cur_size[2] - cur_size[0]) and out.size[0] * img_fraction > (cur_size[3] - cur_size[1]):
fontsize += 5
cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
cur_size = textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
anchor="mm", font=cur_fnt, spacing=5)
if fontsize <= 5:
fontsize = 6
fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize-5)
print(cur_size)
print(out.size)
# # アンカーポイントは画像の中心、アンカーはmmにして描画
textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
anchor="mm", font=fnt, spacing=5)
print(textbbox)
d.multiline_text(xy=(128, 128), text=newlined_text,
anchor="mm", font=fnt, spacing=5, fill="black")
out.save("sample.png")
詳細
絵文字画像が正方形なので、文字も正方形に収まる必要がある。
具体的には、mの長さの文字列が与えられた時、(n-1)^2 < m <= n^2 を満たす
n行n列の空間に文字を収めたい(この空間をテキストボックスと呼ぶことにする)。
コードでは n = math.ceil(math.sqrt(len(row_text)))
でnを求め
改行コードを適切に入れることでn行n列のテキストボックスに納めている
- イメージ
ありがとう
↓
1 | 2 | 3 | |
---|---|---|---|
1 | あ | り | が |
2 | と | う | |
3 |
gitfrom PIL import Image, ImageDraw, ImageFont
import math
import traceback
# テキストの文字数が自身の平方数以下となる平方根nを見つける(10文字ならn=4)
def create(text):
row_text = text
print(row_text)
n = math.ceil(math.sqrt(len(row_text)))
print(n)
# nだけテキストを分割し、文字列末尾に\nを入れる
textlist = []
i = 0
while i < n:
textlist_element = row_text[i * n:(i * n) + n]
if textlist_element == "":
break
textlist.append(textlist_element)
i += 1
print(textlist)
newlined_text = ""
for j in textlist:
newlined_text += j + "\n"
print(newlined_text)
newlined_text = newlined_text[:-1]
Image.new()でベースとなる背景画像のImageオブジェクトを作成
更にImageオブジェクトを引数に指定して書き込みのためのDrawオブジェクトを生成。
out = Image.new("RGB", (256, 256), (256, 256, 256))
d = ImageDraw.Draw(out)
テキストボックスに文字列を収められたので、次はフォントサイズをベース画像に合うよう調整したい。
ImageDrawモジュールのmultiline_textbbox
関数は
テキスト、フォント、フォントサイズ、テキストの(ベース画像上での)位置を指定するとテキストボックスの(ベース画像上での)左上の頂点と右下の頂点の座標を返してくれる
cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text, anchor="mm", font=cur_fnt, spacing=5)
補足
テキストの位置というのはアンカーとアンカーポイントで決まる。
アンカーというのが自分には馴染みがなかったけど
簡単に言うとテキストの位置を決める基準のようなもの。
アンカーポイントというのを座標(x1,y1)に指定し、アンカーをmmという二文字で指定したとする。
するとx=x1にテキストの水平中央、y=y1にテキストの垂直中央がくるようにテキストの位置が決定される。
アンカーは 2文字の文字列で指定される。最初の文字は水平配置で、2番目の文字は垂直方向の配置を意味している。mmだとmiddle&middleということになる。
アンカーには他にも色々あるけど公式の図が分かりやすい
因みにさっきのmultiline_textbbox
関数の引数xy
とanchor
はそれぞれアンカーポイントとアンカーに対応している
テキストボックスの幅と高さが分かったら、幅と高さがベース画像からはみ出ない程度にフォントサイズを調整していく。
フォントサイズは1からはじめ、whileを回す
# テキストの矩形最大幅がimage.size[0]*img_fraction を上回るまでフォントサイズを挙げていく
fontsize = 1
img_fraction = 0.9
cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
cur_size = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
anchor="mm", font=cur_fnt, spacing=5)
# 矩形サイズ取得
while out.size[0] * img_fraction > (cur_size[2] - cur_size[0]) and out.size[0] * img_fraction > (cur_size[3] - cur_size[1]):
# テキストボックスの幅、高さがベース画像の辺の長さを越えるまでfontsizeを5ずつプラス
fontsize += 5
cur_fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize)
cur_size = textbbox = d.multiline_textbbox(xy=(128, 128), text=newlined_text,
anchor="mm", font=cur_fnt, spacing=5)
if fontsize <= 5:
fontsize = 6
fnt = ImageFont.truetype("logotypejp_corpmin.ttf", size=fontsize-5)
# # アンカーポイントは画像の中心、アンカーはmmにして描画
d.multiline_text(xy=(128, 128), text=newlined_text,
anchor="mm", font=fnt, spacing=5, fill="black")
最後に保存して終了
out.save("sample.png")
生成した絵文字画像をgyazoに送り、そのリンクを取得する
まずは該当コード
import requests as req
import traceback
class usegyazo:
def upload(self):
try:
with open('sample.png', 'br') as f:
f_bi = f.read()
XLSX_MIMETYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
files = {'imagedata': ("sample.png", f_bi, XLSX_MIMETYPE)}
headers = {
"access_token": アクセストークン}
res = req.post(
url="https://upload.gyazo.com/api/upload", files=files, data=headers)
if 'json' in res.headers.get('content-type'):
result = res.json()
print(result)
return(result["permalink_url"])
else:
result = res.text
except Exception as err:
print(err)
print(traceback.format_exc())
詳細
gyazoAPIではhttps://upload.gyazo.com/api/upload
に
POSTすることで画像をアップロードすることができる。
必須のパラメータはaccess_token(string)とimagedata(binary)。レスポンスとして画像のidやアップロード日時、共有用のurl(permalink_url)が返ってくる。
今回はslackに画像を共有したいのでpermalink_urlを得る。
注意点はmultipart/form-data を使うこと。imagedataに画像のバイナリを渡しただけでは不正とみなされる
アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行う
まずは該当コード
import requests as req
import emojicreate as ec
import logging
import traceback
import usegyazo as ga
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
slackBotToken = ボットトークン
slackAppToken = アプリトークン
app = App(token=slackBotToken)
ga = ga.usegyazo()
# "emoji "というメッセージをコマンドとしてハンドリングしている
@app.message("emoji ")
def handle_message_events(message, say):
try:
messagelist = message["text"].split(" ")
if messagelist[0] == "emoji":
try:
content = message["text"][6:]
if content == "" or content == " " or content == " ":
say("単語を指定してください")
except IndexError as err:
say("単語を指定してください")
text = content
# 絵文字画像を作る
say(text)
ec.create(text=text)
# gyazoにアップロード&リンク取得
emoji_url = ga.upload()
# slackに投稿
say(emoji_url)
except Exception as err:
print(err)
print(traceback.format_exc())
@app.error
def custom_error_handler(error, body, logger):
logger.exception(f"Error: {error}")
logger.info(f"Request body: {body}")
if __name__ == "__main__":
handler = SocketModeHandler(app, slackAppToken)
handler.start()
詳細
app.message(特定の文字列)
を使うと特定の文字列が含まれるメッセージをハンドリングしてくれる。
ここでは emoji[半角スペース]絵文字用テキスト
をコマンドにしたいので@app.message("emoji ")
としている。
メッセージの詳細(tsとかtextとかの情報が辞書で構造化されている)を取得出来たら必要な情報を抜きだしてsay(メッセージ内容)
で応答する
補足
slackAPIには数種類あるけど今回使ったのはwebsocketを用いたSocketModeに分類される。
websocketを用いる前世代のAPIであるRTMよりかは権限がちゃんと管理できるようになっていて、
HTTPのエンドポイントを用意する方法よりかはセキュリティ的に安全とのこと
公式
結果
最後にapp.pyを走らせると無事絵文字ジェネレータっぽいbotが稼働するようになる
長文もいける
参考
Intro to Socket Mode
Slack ソケットモードの最も簡単な始め方
Using Socket Mode