0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

slackAppで絵文字ジェネレータっぽいのを作る(Python&Gyazo&slack-bolt)

Last updated at Posted at 2021-03-26

slackAppで絵文字ジェネレータっぽいのを作る(Python&Gyazo&slack-bolt)

目次

概要

目標

salckでテキストを送ったらその絵文字画像を生成してほしい

  • 完成例

image (33).png

使うもの

構成

  • 絵文字画像を生成するemojicreate.py
  • 生成した絵文字画像をgyazoに送り、そのリンクを取得するusegyazo.py
  • アプリを立ち上げslack上でコマンドの受信、gyazoリンクの送信を行うapp.py
    からなる

絵文字画像を生成する

まず該当コード

emojicreate.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ということになる。
アンカーには他にも色々あるけど公式の図が分かりやすい
image.png

因みにさっきのmultiline_textbbox関数の引数xyanchorはそれぞれアンカーポイントとアンカーに対応している

テキストボックスの幅と高さが分かったら、幅と高さがベース画像からはみ出ない程度にフォントサイズを調整していく。
フォントサイズは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に送り、そのリンクを取得する

まずは該当コード

usegyazo.py
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リンクの送信を行う

まずは該当コード

app.py
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が稼働するようになる
image.png
長文もいける

参考

Intro to Socket Mode
Slack ソケットモードの最も簡単な始め方
Using Socket Mode

Text anchors(Pillow公式)
ImageDrawモジュール(Pillow公式)

Gyazo API

コトバンク-「観光」

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?