18
8

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 5 years have passed since last update.

Gopher道場Advent Calendar 2018

Day 13

Goでカラー絵文字を使って画像を合成する

Last updated at Posted at 2018-12-12

Gopher道場アドベントカレンダーの13日目の記事です。

概要

この記事ではGo用いてカラー絵文字を含んだテキストをレンダリングする方法について説明します。
最終的には以下のような画像を出力するのを目標とします。

test.jpg

フォントの読み込み

今回は以下のパッケージを用いてTrueType フォントを読み込みます1

まず truetype.Parse に byte スライスを渡します。
ここでは準標準パッケージに含まれている gobold を使います。

ft, err := truetype.Parse(gobold.TTF)
if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(exitFailure)
}

フォントをレンダリングする際のオプションを設定します。
各値に0を指定するとデフォルト値が使われます。

opt := truetype.Options{
    Size:              fontSize,
    DPI:               0,
    Hinting:           0,
    GlyphCacheEntries: 0,
    SubPixelsX:        0,
    SubPixelsY:        0,
}

truetype.NewFace() を使って font.Face を取得します。
この変数を後から draw.Drawer へ渡すことになります。

face := truetype.NewFace(ft, &opt)

絵文字の読み込み

カラー絵文字は OpenType の拡張を使っていますが、
プラットフォームによって使っている規格がバラバラです2
残念ながらカラー絵文字を含む OpenType に対応したライブラリは Go にはありません.

元のデータとしては SVG なり PNG なりの画像なので、
今回は Noto Emoji に収録されている PNG ファイルを利用します。

rune 型の r が与えられたとき、ファイル名は以下のようになります。

path = fmt.Sprintf("%s/emoji_u%.4x.png", imagePath, r)

image.Decode() によって image.Image 型に変換します。
また、フォントの高さと絵文字のサイズを合わせるためにリサイズしておきます。
Metrics についてはこちらを参考にしてください.

fp, err := os.Open(path)
if err != nil {
        fmt.Fprintln(os.Stderr, err)
        continue
}
defer fp.Close()

emoji, _, err := image.Decode(fp)
if err != nil {
        fmt.Fprintln(os.Stderr, err)
        continue
}

// フォントのサイズと絵文字のサイズを合わせるためにリサイズ
size := dr.Face.Metrics().Ascent.Floor() + dr.Face.Metrics().Descent.Floor()
rect := image.Rect(0, 0, size, size)
dst := image.NewRGBA(rect)
draw.ApproxBiLinear.Scale(dst, rect, emoji, emoji.Bounds(), draw.Over, nil)

画像の合成

まず font.Drawer を作成します。
描画先の image.Imagetruetype.NewFace() で取得した font.Face を使います。

img := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight))

dr := &font.Drawer{
    Dst:  img,
    Src:  image.White,
    Face: face,
    Dot:  fixed.Point26_6{},
}

以下のようにしてカラー絵文字と通常の文字を描画することができます。

text := "Hello, world! 👋"

// 描画の初期位置
dr.Dot.X = (fixed.I(imageWidth) - dr.MeasureString(text)) / 2
dr.Dot.Y = fixed.I(textTopMargin)

for _, r := range text {
        path := fmt.Sprintf("%s/emoji_u%.4x.png", imagePath, r)
        _, err = os.Stat(path)

        // 画像ファイルが存在する場合に err == nil
        if err == nil {
                /* 
                  前述した絵文字の読み込みとリサイズの処理...
                */
                // font.Drawer.Dot はグリフの baseline の座標を指している (だいたいグリフの左下の地点)
                // 一方 draw.Draw で画像を描画する場合には左上の座標が必要になる
                p := image.Pt(dr.Dot.X.Floor(), dr.Dot.Y.Floor()-dr.Face.Metrics().Ascent.Floor())
                draw.Draw(img, rect.Add(p), dst, image.ZP, draw.Over)
                dr.Dot.X += fixed.I(size)
        } else {
                // 対応するカラー絵文字が存在しないので TrueType フォントを使用する
                dr.DrawString(string(r))
        }
}

まとめ

プログラム全体はこのようになります。
説明の都合上、main にベタ書きしています。

emoji.go
package main

import (
	"bytes"
	"fmt"
	"image"
	"image/jpeg"
	_ "image/png"
	"io"
	"os"

	"github.com/golang/freetype/truetype"
	"golang.org/x/image/draw"
	"golang.org/x/image/font"
	"golang.org/x/image/font/gofont/gobold"
	"golang.org/x/image/math/fixed"
)

const (
	exitSuccess = 0
	exitFailure = 1
)

const (
	imagePath     = "./noto-emoji/png/128"
	fontSize      = 64  // point
	imageWidth    = 640 // pixel
	imageHeight   = 120 // pixel
	textTopMargin = 80  // fixed.I
)

func main() {
	// TrueType フォントの読み込み
	ft, err := truetype.Parse(gobold.TTF)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(exitFailure)
	}

	opt := truetype.Options{
		Size:              fontSize,
		DPI:               0,
		Hinting:           0,
		GlyphCacheEntries: 0,
		SubPixelsX:        0,
		SubPixelsY:        0,
	}

	img := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight))
	face := truetype.NewFace(ft, &opt)

	dr := &font.Drawer{
		Dst:  img,
		Src:  image.White,
		Face: face,
		Dot:  fixed.Point26_6{},
	}

	text := "Hello, world! 👋"

	// 描画の初期位置
	dr.Dot.X = (fixed.I(imageWidth) - dr.MeasureString(text)) / 2
	dr.Dot.Y = fixed.I(textTopMargin)

	// 一文字ずつ描画していく
	for _, r := range text {
		path := fmt.Sprintf("%s/emoji_u%.4x.png", imagePath, r)
		_, err = os.Stat(path)

		// 画像ファイルが存在する場合に err == nil
		if err == nil {
			// 画像の読み込み
			fp, err := os.Open(path)
			if err != nil {
				fmt.Fprintln(os.Stderr, err)
				continue
			}
			defer fp.Close()

			emoji, _, err := image.Decode(fp)
			if err != nil {
				fmt.Fprintln(os.Stderr, err)
				continue
			}

			// フォントのサイズと絵文字のサイズを合わせるためにリサイズ
			size := dr.Face.Metrics().Ascent.Floor() + dr.Face.Metrics().Descent.Floor()
			rect := image.Rect(0, 0, size, size)
			dst := image.NewRGBA(rect)
			draw.ApproxBiLinear.Scale(dst, rect, emoji, emoji.Bounds(), draw.Over, nil)

			// font.Drawer.Dot はグリフの baseline の座標を指している (だいたいグリフの左下の地点)
			// 一方 draw.Draw で画像を描画する場合には左上の座標が必要になる
			p := image.Pt(dr.Dot.X.Floor(), dr.Dot.Y.Floor()-dr.Face.Metrics().Ascent.Floor())
			draw.Draw(img, rect.Add(p), dst, image.ZP, draw.Over)
			dr.Dot.X += fixed.I(size)
		} else {
			// 対応するカラー絵文字が存在しないので TrueType フォントを使用する
			dr.DrawString(string(r))
		}
	}

	// JPEG に変換して stdout に出力
	buf := &bytes.Buffer{}
	err = jpeg.Encode(buf, img, nil)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(exitFailure)
	}

	_, err = io.Copy(os.Stdout, buf)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(exitFailure)
	}
}

実行する場合には上記のコードをコピペして、以下のようにします。

$ git clone https://github.com/googlei18n/noto-emoji.git
$ go run emoji.go > hello.jpg

注意点

  • alias を考慮していない
    • 例えば U+1F64B は U+1F64B U+200D U+2640 と同じ画像を使用するように指示がある (emoji_u1f64b_200d_2642.png というファイルはあるが emoji_u1f64b.png は存在しない)
    • aliasに対応したバージョンはここにあります
  • 結合絵文字に対応していない
    • 複数の Unicode Codepoint を用いて1つの絵文字を表すもの (👩‍🚀 や 🇯🇵 や 👧🏽 など)
    • 大変なのでやっていません :innocent:
  1. 準標準パッケージの godoc.org/golang.org/x/image/font/opentype というのもあるのですが、使ってみたところ panic: not implemented というエラーが出たので断念しました…。

  2. 詳細はこの記事をご覧ください。

18
8
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
18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?