闇の Slack 魔術に対抗する Python 防衛術

この記事は、Slack Advent CalendarJX 通信社 Advent Calendarの最終日です。

メリークリスマス! 素敵なクリスマスをお過ごしでしょうか。取締役の小笠原(@yamitzky)です。

突然ですが、みなさん、ダークモードは好きですか? ダークモードは昨今のソフトウェアのトレンドで、Slack のデスクトップ版も今年の 9 月にダークモードに対応しました。

slackhq.com

しかし Slack をダークモードに設定すると、透過背景・黒文字なカスタム絵文字が見づらいという問題がありました。Slack が仕事のワークフローの中心にある JX 通信社にとって、これは死活問題です。

f:id:yamitzky:20191225003235p:plain

そこで、Slack の闇の魔術(ダークモード)に Python で対抗し、これらの絵文字が見えるようにしたいと思います!

方針

基本方針は簡単です。黒背景に黒文字だと見づらいのが原因なので、背景を白でベタ塗りした、ダークモードフレンドリーな絵文字を作ります。

f:id:yamitzky:20191225003312p:plain

この問題を分解し、次のような処理を Python で行うことを考えます。

  • Slack の API を使い、絵文字の一覧を取得
  • 取得した絵文字のうち、透過背景・黒文字なものを抽出
  • 白背景を合成した絵文字を生成

本当であれば、Python 経由で絵文字を更新したいところですが、公式の API ドキュメントに記載はなく、xoxs から始まるトークンでしか実行できないようです(参考issue)。そのため、本稿では触れません。

絵文字一覧の取得

Slack には emoji.list という API があるので、これを通信ライブラリの requests で実行します。

import requests
# TOKEN には Slack API のトークンを入れる
res = requests.get('https://slack.com/api/emoji.list', headers={'Authorization': f'Bearer {TOKEN}'})
emojis = res.json()['emoji']

emojis 変数は、次のような "絵文字名": "画像URL" という形式の dict 型です。また、結果には alias も含んでいます。

{
    ...
    "saiko": "https://emoji.slack-edge.com/XXXXXXX/saiko/0123456789abcdef.png",
    "shussha": "alias:syussha",  # alias のパターン
    ...
}

絵文字の URL から、画像データ(バイナリ)を取得します。

img_bytes = requests.get(url).content

闇絵文字判定

続いては、取得した画像データが、「黒文字・透過」の絵文字かどうかを判定します。

今回は、Python での画像処理に PillowNumPy を利用します。Pillow は PIL=Python Image Library のフォークで、画像処理用のライブラリです。NumPy は行列計算などに使われるライブラリです。これらライブラリを組み合わせ、取得した絵文字のバイナリ(png)を配列(高さ×幅×RGBA)に展開して操作していきます。

from io import BytesIO
from PIL import Image
import numpy as np
img = Image.open(BytesIO(img_bytes))
img_arr = np.asarray(img)  # img_arr.shape == (128, 128, 4)

透過画像かどうかの判定は簡単です。img_arr は RGBA=(赤, 緑, 青, 透明度) の 4 チャンネルのため、Aの最小値が 0 であれば透明です。

transparent = len(img_arr.shape) == 3 and img_arr.shape[2] == 4 and img_arr[:, :, 3].min() == 0

次に、暗い文字かどうかを判定します。この判定はいろいろ調整した結果、次のように判定することにしました。

gray = img_arr[:, :, :3].max(axis=2).astype(float)  # 簡易的なグレースケールとして、RGBの最大値を取ります
gray *= (img_arr[:, :, 3] / 255)  # 透過度を反映したグレー値を計算します
dark = np.percentile(gray[img_arr[:, :, 3] > 0], 80) <= 85  # 80%パーセンタイルが、85=255/3 の明るさ以下の場合、暗いと判定

シンプルなロジックですが、比較的精度良く闇絵文字を判定できています。

f:id:yamitzky:20191225003455p:plain

白背景の合成

最後に、闇絵文字と判定されたものに白背景を付与します。

white = Image.fromarray(np.ones(img_arr.shape, dtype=img_arr.dtype) * 255)  # 真っ白な画像を作る
white.paste(img, (0, 0), img.split()[3])  # 白背景に、元の画像を貼り付ける
white.save(f'result/{name}.png')

完成!

こうして、無事に闇の魔術に対抗することができました。安心して Slack をダークモードにして使えそうです。

f:id:yamitzky:20191225003505p:plain

完成した Python スクリプトは Gist に添付しましたので、ぜひ参考にしてください。

gist.github.com