Help us understand the problem. What is going on with this article?

ダークモードでもSlack絵文字をちゃんと見たい!

Slackのカスタム絵文字、楽しいですよね。
さて、ダークモードで使ってる方ならあるあるかもしれませんが、ダークモードにしてしまうとこんな問題が発生します。

ダークモードで絵文字一覧を見た図

_人人人人人人人人人人_
> 絵文字が見えない <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

絵文字の背景を透明ではなく白にすることで解決してらっしゃった記事もありましたが、今回は上の画像で黄色く囲んだ「オッ」のように、縁取りをする方法で解決したいと思います。

今回作成したソースコードは、以下のリポジトリに置いてあります。是非使ってみてください!

1. 絵文字を取り込む

まずは、Slackから絵文字画像を取ってきます。これは比較的簡単で、emoji.list APIを使うと取得することができます。

emoji:read のスコープを設定したアプリケーションを作成してトークンを取得したら、以下のアドレスを叩くとJSONが得られます。

https://slack.com/api/emoji.list?token=xoxb-...
{
    "ok": true,
    "emoji": {
        "bowtie": "https:\/\/emoji.slack-edge.com\/xxxxxx\/bowtie\/yyyyyy.png",
        "squirrel": "https:\/\/emoji.slack-edge.com\/xxxxxx\/squirrel\/zzzzzz.png",
    }
}

これをもとにPythonでシュシュっと取ってくるプログラムを書きます。

import os
from time import sleep
import requests


def download_image(url):
    response = requests.get(url)
    if response.status_code != 200:
        print(f"HTTP Error: {response}")
        return None

    content_type = response.headers["content-type"]
    if 'image' not in content_type:
        print(f"Error: {content_type} is not image")
        return None

    return response.content


def make_filename(base_dir, alias, url):
    ext = os.path.splitext(url)[1]
    filename = alias + ext

    fullpath = os.path.join(base_dir, filename)
    return fullpath


def save_image(path, image):
    with open(path, "wb") as fout:
        fout.write(image)


def main():
    TOKEN = os.getenv('SLACK_TOKEN')
    BASE_DIR = './original/'

    os.makedirs(BASE_DIR, exist_ok=True)

    res = requests.get('https://slack.com/api/emoji.list',
                       headers={'Authorization': f'Bearer {TOKEN}'})
    emojis = res.json()['emoji']

    for alias, emoji_url in emojis.items():
        if (emoji_url.startswith('alias:')):
            continue

        img = download_image(emoji_url)
        if img is None:
            continue

        img_path = make_filename(BASE_DIR, alias, emoji_url)
        save_image(img_path, img)

        sleep(1)


if __name__ == "__main__":
    main()

注意すべき点は、エイリアスを設定しているとそれも含まれてしまうので除外する処理が必要です。

2. フチを抽出する

フチの抽出にはラプラシアンフィルタを使います。
今回は画像処理全般をPillowで行いました。当初、OpenCVも試しましたが、一部絵文字のアルファチャンネルが正常に読み込めなかったので諦めました。

f = Image.open('hoge.png')
gf = f.convert('LA')
edge = gf.filter(ImageFilter.FIND_EDGES)

ラプラシアンフィルタの適用例
いい感じですね〜

3. 白でフチ取りする

抽出した輪郭をもとに、白で縁取りをします。
フィルタをかけた後のアルファチャンネルのみ活用し、色は全部白で塗ったものを用意しました。

l, a = edge.split()
_l, _a = np.full_like(a, 255), np.array(a)
img_array = np.stack([_l, _a], 2)
border = Image.fromarray(np.uint8(img_array), "LA")

ただ、残念ながらこのまま重ねても元画像と重なってしまいほとんど縁が見えません。

border_color = border.convert('RGBA')
res = Image.alpha_composite(border_color, f)

見えない

そのため、輪郭を太らせて重ねます。一般的にはモルフォロジー変換で行うことができるようです。
PillowではこのOpenCVにあるようなメソッドはありませんが、MaxFilterを使うことで同等のことができます。理由については、こちらの記事が詳しいです。

ただ、やっていることはあまり変わりませんが、今回はずらして重ねるという力技の方が見た目が良かったので、そちらを採用しました。

border_color = border.convert('RGBA')
diff = [-border_size, border_size]
res = f
for xd in range(-border_size, border_size + 1):
    for yd in range(-border_size, border_size + 1):
        b = border_color.rotate(0, translate=(xd, yd))
        res = Image.alpha_composite(b, res)

フチを重ねた絵文字

4. 微調整

平滑化

輪郭検出後の画像に平滑化フィルタ(ImageFilter.SMOOTH)をかけてあげると、少し綺麗になります。
(左が平滑化しない場合、右がした場合)

平滑化前後の比較

アニメーションGIFの対応

このままだと、アニメGIFを読み込んだ際にエラーで落ちてしまうのでついでに対応してみます。
アニメGIFの場合、各フレームをImageSequence.IteratorImageSequence.all_framesで取得することができます。

f = Image.open(file)
duration, loop = f.info.get('duration', 0), f.info.get('loop', 0)

frames = []
for frame in ImageSequence.Iterator(f):
    bf = make_border(frame.convert('RGBA'), BORDER_SIZE)
    frames.append(bf)

if len(frames) > 1:
    frames[0].save(OUTPUT_DIR / file.name, save_all=True,
        append_images=frames[1:], optimize=False, duration=duration, loop=loop, transparency=255, disposal=2)
else:
    frames[0].save(OUTPUT_DIR / file.name)

これでおおよそのイメージはうまくいきますが、どうやらアニメGIFの読み書き周りはハマりどころが多く、透明色がパレットの255番じゃなかったりして正常に処理できない画像も一部ありました。

5. 置き換えるには?

Slackの絵文字は同じ名前で後から差し替えることができないので、削除->追加の手順を取る必要があります。

APIはEnterprise Gridのみ

カスタム絵文字の登録・削除に関するAPIは、実は提供自体はされています。しかし、これらはEnterprise Gridプランでのみ使うことができるので、おそらく使えない人が多いのではないかと思います。

非公開API or 管理画面からなんとかする

公式APIが使えない場合、管理画面から頑張って登録するか、管理画面で呼んでいるAPIを叩いてあげるしかなさそうです。
非公開APIのため、Qiitaでは触れませんが、一括処理できるスクリプトをリポジトリ内に置いたので、そちらをご覧ください。

ちなみに、一括登録のツールとしてはNeutral Face Emoji Toolsが有名ですが、大量の絵文字を登録しようとするとすぐレートリミットに引っかかるのでご注意ください。

まとめ

今回行った画像処理はごくごく簡単なものですが、意外とアニメGIFの処理やSlack側との出し入れに戸惑いました。
このツールで皆さんも快適にダークモードライフをお過ごしください👍

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした