Slackに安西先生ジェネレータを召喚するレシピ

# はじめに

テキストだけでダメ出しされると凹みます。絵文字があれば大分雰囲気は変わります。Slackならもっと何かできるんじゃない？と思っていると「[安西先生ジェネレータ](http://53labo.net/anzai/)やりたいっす」 の後輩の一言。それ、いいね！

以下、[ブラックジャックによろしく](http://mangaonweb.com/company/download.html)（タイトル：ブラックジャックによろしく、著作者名：佐藤秀峰）のデータを利用して解説していきます（二次利用フリー、ありがとうございます）。

# やりたいこと

SlackからHubotに対してセリフをカタカタすると、事前に用意しておいた画像のフキダシへ文字を入れてアップロードしてくれる。利用する画像も指定できるように。

実際にできたものはこんな感じです。

![sample.png](https://qiita-image-store.s3.amazonaws.com/0/60193/3f7bef34-a5a4-fb32-d52d-c8ec029fa0b5.png)

# どうやったか

前提として、Slackと連携できているHubotがすでにあるものとします。

## 準備

画像の加工（フキダシへの文字入れ）にはCanvasを利用しました。Hubotの動作環境としてUbuntu14.04を利用していたので、Cairoなどを導入してからCanvas（[node-canvas](https://github.com/Automattic/node-canvas)）をインストールしました。

```
$ sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
$ npm install --save canvas
```

## 画像

利用したい画像を用意し、hubot/scripts/meme/ 内へ配置しておきます。

![phone.jpg](https://qiita-image-store.s3.amazonaws.com/0/60193/1005d881-1a61-ae21-00b9-4d159b8a2649.jpeg)

## Hubotスクリプト

指定された画像をCanvasへ書き込み、フキダシへ文字を入れます。そしてこれをPNGファイルとして一時保存し、Slackへアップロードします。[^1]

[^1]: 一時ファイル経由ではなくcanvas.pngStream()を利用したかったのですが、何故かうまく行きませんでした...

```coffee:hubot/scripts/meme.coffee
Fs = require 'fs'
Canvas = require 'canvas'
Request = require 'request'

module.exports = (robot) ->
  robot.respond /meme (\S+) (\S.*)$/i, (msg) ->
    name = msg.match[1]
    text = msg.match[2]

    # 画像の情報を取得
    if collage = findCollage(name)
      path = './scripts/meme/'

      # 画像の読み込み
      Fs.readFile path + collage.image, (err, data) ->
        if err
          msg.send '指定された画像の読み込みに失敗しました...'
        else
          # 画像をCanvasへ書き込み
          image = new Canvas.Image
          image.src = data

          canvas = new Canvas(image.width, image.height)
          context = canvas.getContext('2d')
          context.drawImage(image, 0, 0, image.width, image.height)

          # フキダシへ文字入れ
          context.font = '15px "TakaoGothic"'
          result = fillTextVertically(context, text, collage.lower, collage.upper)

          if result > 0
            msg.send "#{collage.name}のセリフは#{result}文字以内です..."
          else
            # PNGファイルとして一時保存
            file = msg.message.user.name + '.png'
            buf = canvas.toBuffer()
            Fs.writeFile path + file, buf, (err) ->
              if err
                msg.send '指定された画像の書き込みに失敗しました...'
              else
                # 一時保存したファイルをアップロード
                url = 'https://slack.com/api/files.upload'
                data =
                  token:    process.env.HUBOT_SLACK_TOKEN
                  filename: collage.name + '.png'
                  file:     Fs.createReadStream(path + file)
                  channels: msg.envelope.room
                Request.post {url: url, formData: data}

                # 一時保存したファイルを消去
                Fs.unlink path + file
    else
      msg.send '不明な画像が指定されました...'
```

環境変数HUBOT_SLACK_TOKENへはHubotのAPI Tokenを指定しておきます。

用意した画像の情報は、以下のようにまとめておきました。

```coffee:hubot/scripts/meme.coffee
findCollage = (name) ->
  collages =
    phone: {name: 'ブラックジャックによろしく 佐藤秀峰 - 電話', image: 'phone.jpg', lower: {x: 107, y: 9}, upper: {x: 207, y: 154}}
#    xxxx: {name: 'テンプレート', image: 'xxxx.jpg', lower: {x: 0, y: 0}, upper: {x: 100, y: 100}}

  if name of collages
    return collages[name]
  else
    return null
```

lower、upperはフキダシ内の文字の書き込み領域です。矩形領域として左上、右下の座標を指定しました。

![box.png](https://qiita-image-store.s3.amazonaws.com/0/60193/d5a0038d-71e8-bdc2-a1bc-e0c7f7e64de8.png)

最後に、フキダシへの文字入れ処理です。

```coffee:hubot/scripts/meme.coffee
fillTextVertically = (context, text, lower, upper) ->
  size = context.measureText('あ').width # 代表文字
  count =
    x: Math.floor((upper.x - lower.x) / size)
    y: Math.floor((upper.y - lower.y) / size)

  if text.length > count.x * count.y
    return count.x * count.y

  text = text.replace /[A-Za-z0-9]/g, (s) ->
    String.fromCharCode(s.charCodeAt(0) + 0xFEE0) # 半角->全角変換

  lines = []
  for i in [0...Math.ceil(text.length / count.y)]
    j = i * count.y
    line = text.slice(j, j + count.y)
    lines.push(line)

  x = upper.x - (upper.x - lower.x - lines   .length * size) / 2 - size
  y = lower.y + (upper.y - lower.y - lines[0].length * size) / 2 + size

  for line, i in lines
    for s, j in line
      context.fillText(s, x - size * i, y + size * j)

  return 0
```

全角、縦書きで文字を書いていきます。書き込み領域を超える場合は、書き込み可能な最大文字数を返して終了するので、以下のように怒られます。

![error.png](https://qiita-image-store.s3.amazonaws.com/0/60193/4a20268c-3d1d-a61c-d911-8264e94b22c5.png)

# どうなったか

![result.png](https://qiita-image-store.s3.amazonaws.com/0/60193/905f59da-9a85-0305-b659-1e5bde812e17.png)