【golang】SNSでの議論をマシにするサービス途中段階 ~第1弾(サーバでのpng画像生成編)~

こんにちは。

普段私はtwitterを見ないのですが(一度アカウントを消した)、自分の気になったトピックについて、世論を知るためにtwitterを眺めることがあります。そうすると、極論が流れてきたり、偏見が多く見られていたり、「~~すべき!」と言いながらも精神論すぎて具体性のかけらも無いものが散見されます。そんな意見が目に入る度に、「ああ、見なきゃよかったな」と不快な気持ちになるものです。自業自得ですが。

SNS上での議論の質を改善するために、以前私は、下記の様なサービスを導入できる可能性について話しました。
ume-boshi.hatenablog.jp


今回はそのサービスについて、画像生成サーバの試作を行なったご報告と、一部実装方法を説明していこうと思います。サービスに興味が無い人も、golangにおけるpng画像の処理については大いに参考にできると思いますので、ぜひ読んでみてください。


生成した画像

今回は最低限の機能として、jsonで受け取った{Hashtag, Age, Position, Gender, Background1~3, TwitterId}のデータを下記のようにpng画像生成するようにします。

下記の様なrequestをPOSTすると、

f:id:ume-boshi:20210405045314j:plain:w400
Talend APIで動作テスト

↓ のような画像がresponseされます。

f:id:ume-boshi:20210405045353j:plain:w400
Talend APIでは受け取った画像をすぐに見られる
ちなみに、IMOFTHというのは、"It's My Opinion For The Hashtag"の頭を取ったものです。仮称ですが、いいのが思いつかないんですよねぇ。



これを実際にtwitterに無理やり張り付けてみると、下記のようになります。

f:id:ume-boshi:20210405050132j:plain:w400
PC版は大きすぎるように見える

f:id:ume-boshi:20210405050124j:plain:h400
スマホ版はちょうどいいサイズ


実装について

実装環境

OS:Windows10
実装言語:go言語 (1.15.5)
テスト方法:Talend API
画像処理に使用したライブラリ:

メイン機能

サーバを動かす為に、最低限の機能として下記の内容を実装しました。

  • jsonデータの受け取り機能
  • png画像の入出力機能(svg画像はうまくできなかった)
  • 画像への日本語文字追加機能
  • httpでpng画像を送信する機能

jsonデータの受け取り機能

jsonデータの受け取りについては、多くの記事がネットに掲載されていますが、一応私も掲載しておきます。このとき、実装の簡便さのために"github.com/julienschmidt/httprouter"のパッケージを使用しています。

// jsonで受け取るデータについて、構造体を事前に作成する必要がある
type CardInfo struct {
    Hashtag     string `json:"hashtag,omitempty"`
    Age         int    `json:"age,omitempty"`
    Position    string `json:"position,omitempty"`
    Gender      int    `json:"gender,omitempty"`
    Background1 string `json:"background1,omitempty"`
    Background2 string `json:"background2,omitempty"`
    Background3 string `json:"background3,omitempty"`
    TwitterId   string `json:"twitterId,omitempty"`
}

// 画像を生成する機能の一部抜粋
func CreateImage(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    log.Println("createImage")
    var input CardInfo

    // POST以外だった場合、エラーを吐く
    if r.Method != "POST" {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    // Bodyから受信内容を読み取る
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(err)
    }
    defer r.Body.Close()

    // Jsonデータをinput変数に読み込む
    err = json.Unmarshal(body, &input)
    log.Println("request:", input)
    log.Println("Hashtag:", input.Hashtag)
    
    // ︙
}

// httpの受信内容に合わせてroutingする
func Build() *httprouter.Router {
    router := httprouter.New()

    router.POST("/createImage", CreateImage)

    router.NotFound = http.FileServer(http.Dir("html/hoge.html"))
    router.MethodNotAllowed = http.FileServer(http.Dir("html/fuga.html"))

    return router
}

// サーバを起動する
func main() {
    r := Build()
    log.Fatal(http.ListenAndServe(":8080", r))
}


httpでpng画像を送信する機能

POSTの結果、最終的にpng画像を返答します。httpでpng画像を送信する手法について、ネットに情報が少なく少し試行錯誤したのでコードを掲載しておきます。使用するパッケージは"bytes"と"image/png"、"github.com/julienschmidt/httprouter"ぐらいだったと思います。

    // ︙

    // 画像生成処理(image.RGBA形式のデータがcard変数に代入される)
    card, _ := drawFrame(input)

    // 画像を送信用のバッファに代入
    buffer := new(bytes.Buffer)
    if err := png.Encode(buffer, card); err != nil {
        log.Println("error:png\n", err)
    }
    // log.Println(buffer.Bytes())

    // base64形式で送信する場合は、text/template パッケージを用いて送信できる
    // str := base64.StdEncoding.EncodeToString(buffer.Bytes())
    // if tmpl, err := template.New("image").Parse(ImageTemplate); err != nil {
    //     log.Println("unable to parse image template.")
    // } else {
    //     data := map[string]interface{}{"Image": str}
    //     if err = tmpl.Execute(w, data); err != nil {
    //         log.Println("unable to execute template.")
    //     }
    // }

    // jpeg送信時と比べてimage/pngになりますが、ほぼ変化なし
    w.Header().Set("Content-Type", "image/png")
    w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes())))
    if _, err := w.Write(buffer.Bytes()); err != nil {
        log.Println("unable to write image.")
    }
}


png画像の入出力機能

まず、新規画像ファイルの生成は下記のコードです。これにより、main.goと同じディレクトリに、out.pngが生成されます。この際、まだ画像サイズ等は決まっておらず、書き出しの際に決定されるのだと考えられます。

   fso, err := os.Create("out.png")
    defer fso.Close()
    if err != nil {
        log.Println("create error:", err)
    }
    defer fso.Close()

上記は空のpngを生成する手法でしたが、次は既存の画像を読み込む処理です。ついでにimage.RGBAデータへの干渉方法もここに抜粋しています。
ここで読み込んでいるimage_base.pngは、inkscapeで作ったこんな画像です ↓

f:id:ume-boshi:20210405050255p:plain:w400
プログラムで毎回配置してもいいが、処理時間がかかりそう。

    // 画像サイズを決定
    imageWidth := 1200
    imageHeight := 675

    fb, err := os.Open("image_base.png")
    defer fb.Close()
    if err != nil {
        log.Println("create error:", err)
    }
    defer fb.Close()

    img2, err := png.Decode(fb)
    if err != nil {
        log.Println("decode error:", err)
        os.Exit(1)
    }

    // image.RGBA形式のデータを生成
    m := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight)) // 16:9 のpng画像を生成

    // 水色の枠を重畳
    // c := color.RGBA{50, 200, 255, 255}     // RGBA で色を指定(水色=枠)
    // c2 := color.RGBA{245, 245, 245, 255}   // RGBA で色を指定(グレー=背景)
    // draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.ZP, draw.Src)  // 青い画像を描画
    // rct := image.Rectangle{image.Point{25, 25}, image.Point{1200 - 25, 675 - 25}} // test.jpg をのせる位置を指定する(中央に配置する為に横:25 縦:25 の位置を指定)
    // draw.Draw(m, rct, &image.Uniform{c2}, image.Point{0, 0}, draw.Src)  // 合成する画像を描画

    // 背景画像の描画
    base := image.NewRGBA(image.Rectangle{image.Point{0, 0}, img2.Bounds().Size()})
    draw.Draw(base, img2.Bounds(), img2, image.Point{0, 0}, draw.Src) // 元になる画像を描画する
    pos := image.Rectangle{image.Point{0, 0}, img2.Bounds().Size()}   // 元画像への描画位置を決める
    draw.Draw(m, pos, img2, image.Point{0, 0}, draw.Src)              // 乗せる画像を描画

そして最後に画像の書き出しです。image.RGBA形式のデータがm変数に代入されており、それをout.pngに対してEncodeしています。out.pngファイルの内容が正常に変化していれば成功です。ちなみにこのm変数は、「httpでpng画像を送信する機能」で登場したcard変数と同じものです。

   if err := png.Encode(fso, m); err != nil {
        log.Println("error:png\n", err)
    }


画像への日本語文字追加機能

golangの標準状態ではフォントの問題で、画像に日本語を描画できないようです。そこで、下記の様にフォントを設定して描画します。このサイトを参考に実装しました。ttf形式のフォントしか使えないようなので、今回はIPAが提供しているオープンソースライセンスの、IPAexのゴシックフォントを用いました。

使用したパッケージは"github.com/golang/freetype/truetype""golang.org/x/image/font""golang.org/x/image/math/fixed"あたりです。

   // フォントファイルを読み込み
    log.Println("start font setup")
    ftBinary, err := ioutil.ReadFile("ipaexg.ttf")
    if err != nil {
        log.Println("font error", err)
        // fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    ft, err := truetype.Parse(ftBinary)
    if err != nil {
        log.Println("font error", err)
        os.Exit(1)
    }

    // フォントの設定
    opt := truetype.Options{
        Size:              80,
        DPI:               0,
        Hinting:           0,
        GlyphCacheEntries: 0,
        SubPixelsX:        0,
        SubPixelsY:        0,
    }

    // image.RGBAに出力する用の設っぽい(?)
    face := truetype.NewFace(ft, &opt)
    fdr := &font.Drawer{
        Dst:  m,   // ここは文字を書き込みたいimage.RGBAの変数!!
        Src:  image.Black,
        Face: face,
        Dot:  fixed.Point26_6{},
    }

    textLeftMargin := 170
    messageLeftMargin := 60
    textRightMargin := 1130

    // 高さ180、左から170の位置に出力
    drawTextOnImage(fdr, input.Hashtag, textLeftMargin, 180)
    // 高さ185の位置に、右寄せで 右から1130の位置に出力
    drawTextOnImage(fdr, strconv.Itoa(input.Age), textRightMargin-fdr.MeasureString(strconv.Itoa(input.Age)).Ceil(), 185)

    //フォントサイズの変更
    opt.Size = 50
    face = truetype.NewFace(ft, &opt)
    fdr.Face = face
    drawTextOnImage(fdr, "背景1: "+input.Background1, messageLeftMargin, 305)

    // フォントサイズを小さく、文字色を白に変更
    opt.Size = 40
    face = truetype.NewFace(ft, &opt)
    fdr = &font.Drawer{
        Dst:  m,
        Src:  image.White,
        Face: face,
        Dot:  fixed.Point26_6{},
    }
    // 中央寄せで文字出力
    drawTextOnImage(fdr, input.TwitterId, (imageWidth-fdr.MeasureString(input.TwitterId).Ceil())/2, 565)


おわりに

web開発をしたのは半年ぶりぐらいでしたが、実装は3月初旬には終えており、1週間もかかっていません。しかし、デザイン面でのセンスが足りず、人前に見せられる状態じゃありませんでした。。。
web開発って、こういうフロント面が億劫なんですよね。基板設計はどうやっても綺麗に見えるから好きなんですけど、デザインセンスが問われる場面はどうも苦手です。

デザインを修正する気が湧いたのは4月に入ってからなので、本記事の投稿がずいぶん遅くなってしまいました。

それでも、これからはフロント側の実装を進めなければならないので、覚悟を決めてやろうと思います。

参考になりそうな文献メモ

golangのdraw系の処理に関しては この記事に色々まとめられていました。 golangjpegを入出力操作する場合、この公式ドキュメントが参考になるかもしれません。

今回は使用していませんが、golangで画像処理できそうなパッケージが、他にも2つほどありました。

まず、golangOpenCVを使える「gocv」というパッケージがあるようです。これについては公式ドキュメントこの記事にまとめられています。
また、webサーバ界では有名(?)なImageMagickgolangでは使用できるようです。パッケージ名はimagickと呼ばれています。ImageMagickにどの程度セキュリティ的な問題があるか知りませんが、サーバで文字列を受け取って画像生成する分には大丈夫なんじゃないですかね。

svg画像の操作について、svgoというパッケージが存在していましたが、image.RGBAに干渉できそうにありませんでした。svg画像を無理やり描画するパッケージを作ったら人気が出そうですね。やりませんけど。