Gopher道場アドベントカレンダーの13日目の記事です。
概要
この記事ではGo用いてカラー絵文字を含んだテキストをレンダリングする方法について説明します。
最終的には以下のような画像を出力するのを目標とします。
フォントの読み込み
今回は以下のパッケージを用いて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.Image
と truetype.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
にベタ書きしています。
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つの絵文字を表すもの (👩🚀 や 🇯🇵 や 👧🏽 など)
- 大変なのでやっていません
-
準標準パッケージの godoc.org/golang.org/x/image/font/opentype というのもあるのですが、使ってみたところ
panic: not implemented
というエラーが出たので断念しました…。 ↩