GolangでMLTをGifに変換する

別記事にてMLTを画像に変換する処理を行いましたが、これを少し改造すればGifの出力も行えそうです。

MLTの説明や画像化の方法については下のリンクを参照。
[GolangでMLTを画像に変換する](https://qiita.com/xiaca/items/c9b949d8950c233022da)

## ソースコード
Gifへの変換は以下のことを行います。

1. inputフォルダの中身をみてファイルなら変換、ディレクトリなら再帰処理
2. ファイルの場合、1れつずつ読み込みSPLITがきたらひとつのAAとしてまとめる
3. 各文字列をsjis→utf-8に変換。
4. 各文字列のHTML特殊文字「`&hearts;` 」を「❤︎」に置き換え
5. 文字列をimageオブジェクトに変換
6. MLT内のAAで最大の高さ、幅のAAを探す
7. imageオブジェクトを全て結合しGifとして出力


```golang
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/fogleman/gg"
	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/transform"
	"image"
	"image/color/palette"
	"image/draw"
	"image/gif"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

const (
	S           = 1024
	FONT_SIZE   = 16.0
	LINE_HEIGHT = 18.0
)

type Mlt struct {
	Name    string `json:"name,omitempty"`
	Path    string `json:"path,omitempty"`
	Updated string `json:"updated,omitempty"`
	Aa      []*Aa  `json:"aa,omitempty"`
}

type Aa struct {
	Value string `json:"value,omitempty"`
}

type Character struct {
	Value   string `json:"value"`
	Code    string `json:"code"`
	Unicode string `json:"unicode"`
}

func main() {
	dirwalk("./input")
}

func dirwalk(dir string) {
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		panic(err)
	}

	for _, file := range files {
		if file.IsDir() {
			dirwalk(filepath.Join(dir, file.Name()))
			continue
		}
		path := filepath.Join(dir, file.Name())
		fromFile(file.Name(), path)
	}
}

func fromFile(name string, filePath string) {
	if name == ".DS_Store" {
		return
	}
	if strings.LastIndex(name, ".mlt") == -1 {
		return
	}
	// ファイルを開く
	f, err := os.Open(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "File %s could not read: %v\n", filePath, err)
		os.Exit(1)
	}

	// 関数return時に閉じる
	defer f.Close()

	// Scannerで読み込む
	lines := make([]string, 0, 300) // ある程度行数が事前に見積もれるようであれば、makeで初期capacityを指定して予めメモリを確保しておくことが望ましい
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if serr := scanner.Err(); serr != nil {
		fmt.Fprintf(os.Stderr, "File %s scan error: %v\n", filePath, err)
	}

	var mlt Mlt
	namepos := strings.LastIndex(name, ".")
	new_name := name[:namepos]

	mlt.Name = new_name

	aa := &Aa{}
	for idx, l := range lines {
		l, _ = sjis_to_utf8(l)
		l, _ = escape_html_special_caharacter(l)
		if l == "[SPLIT]" {
			if aa.Value != "" {
				mlt.Aa = append(mlt.Aa, aa)
			}
			aa = &Aa{}
		} else {
			aa.Value = aa.Value + l + "\n"
		}
		if idx == len(lines)-1 {
			if aa.Value != "" {
				mlt.Aa = append(mlt.Aa, aa)
			}
		}
	}

	pos := strings.LastIndex(filePath, ".")
	fileName := filePath[:pos]

	pos = strings.Index(fileName, "/")
	path := fileName[(pos + 1):]
	mlt.Path = path

	b := mlt.GenerateGifImage()
	var buf bytes.Buffer
	buf.Write(b)

	firpos := strings.LastIndex(path, "/")
	gifPath := ""
	gifName := path
	if firpos > 0 {
		gifPath = path[:firpos]
		gifName = path[firpos:]
	}
	if err := os.MkdirAll("output/"+gifPath, 0777); err != nil {
		fmt.Println(err)
	}
	err = ioutil.WriteFile("output/"+gifPath+"/"+gifName+".gif", buf.Bytes(), 0644)
	if err != nil {
		fmt.Println("err:", err)
	}
	return
}

func (this *Mlt) GenerateGifImage() []byte {
	outGif := &gif.GIF{}
	delay := 0
	sWidth, sHeight := calcrateMaxWidthAndHeight(this.Aa, FONT_SIZE, LINE_HEIGHT)
	for _, m := range this.Aa {
		lines := strings.Split(m.Value, "\n")
		img, _ := ConvertTextToImage(lines, sWidth, sHeight)
		pimg := image.NewPaletted(img.Bounds(), palette.WebSafe)
		draw.FloydSteinberg.Draw(pimg, img.Bounds(), img, image.ZP)
		outGif.Image = append(outGif.Image, pimg)
		outGif.Delay = append(outGif.Delay, delay)
	}
	buf := new(bytes.Buffer)
	err := gif.EncodeAll(buf, outGif)
	if err != nil {
		fmt.Println(err)
	}
	ret := buf.Bytes()
	return ret
}

func calcrateMaxWidthAndHeight(aa []*Aa, fontSize float64, lineHeight float64) (int, int) {
	const S = 1024
	sWidth := 0.0
	sHeight := 0.0
	for _, a := range aa {
		lines := strings.Split(a.Value, "\n")
		measure := gg.NewContext(S, S)
		if err := measure.LoadFontFace("./Saitamaar.ttf", fontSize); err != nil {
			return 0, 0
		}
		maxWidth := 0.0
		maxHeight := float64(int(lineHeight) * (len(lines) + 1))
		for _, line := range lines {
			w, _ := measure.MeasureString(line)
			if maxWidth <= w {
				maxWidth = w
			}
		}

		if maxWidth >= sWidth {
			sWidth = maxWidth
		}
		if maxHeight >= sHeight {
			sHeight = maxHeight
		}
	}
	return int(sWidth), int(sHeight)
}

func ConvertTextToImage(lines []string, sWidth int, sHeight int) (image.Image, error) {
	width := sWidth
	height := sHeight
	dc := gg.NewContext(width, height)
	if err := dc.LoadFontFace("./Saitamaar.ttf", FONT_SIZE); err != nil {
		return nil, err
	}
	dc.SetRGB(1, 1, 1)
	dc.Clear()
	dc.SetHexColor("#333333")
	for idx, line := range lines {
		i := float64(idx + 1)
		dc.DrawString(line, 10, LINE_HEIGHT*i)
	}
	dc.Clip()
	img := dc.Image()
	return img, nil
}

func sjis_to_utf8(str string) (string, error) {
	ret, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
	if err != nil {
		return "", err
	}
	return string(ret), err
}

func escape_html_special_caharacter(str string) (string, error) {
	jsonBlob := []byte(`[{"value":"ƒ","code":"&fnof;","unicode":"&#402;"},{"value":"ε","code":"&epsilon;","unicode":"&#917;"},{"value":"κ","code":"&kappa;","unicode":"&#922;"},{"value":"ο","code":"&omicron;","unicode":"&#927;"},{"value":"υ","code":"&upsilon;","unicode":"&#933;"},{"value":"α","code":"&alpha;","unicode":"&#945;"},{"value":"ζ","code":"&zeta;","unicode":"&#950;"},{"value":"λ","code":"&lambda;","unicode":"&#955;"},{"value":"π","code":"&pi;","unicode":"&#960;"},{"value":"υ","code":"&upsilon;","unicode":"&#965;"},{"value":"?","code":"&thetasym;","unicode":"&#977;"},{"value":"′","code":"&prime;","unicode":"&#8242;"},{"value":"ℑ","code":"&image;","unicode":"&#8465;"},{"value":"↑","code":"&uarr;","unicode":"&#8593;"},{"value":"⇐","code":"&larr;","unicode":"&#8656;"},{"value":"∀","code":"&forall;","unicode":"&#8704;"},{"value":"∈","code":"&isin;","unicode":"&#8712;"},{"value":"−","code":"&minus;","unicode":"&#8722;"},{"value":"∠","code":"&ang;","unicode":"&#8736;"},{"value":"∫","code":"&int;","unicode":"&#8747;"},{"value":"≠","code":"&ne;","unicode":"&#8800;"},{"value":"⊃","code":"&sup;","unicode":"&#8835;"},{"value":"⊗","code":"&otimes;","unicode":"&#8855;"},{"value":"?","code":"&lfloor;","unicode":"&#8970;"},{"value":"♠","code":"&spades;","unicode":"&#9824;"},{"value":"α","code":"&alpha;","unicode":"&#913;"},{"value":"ζ","code":"&zeta;","unicode":"&#918;"},{"value":"λ","code":"&lambda;","unicode":"&#923;"},{"value":"π","code":"&pi;","unicode":"&#928;"},{"value":"φ","code":"&phi;","unicode":"&#934;"},{"value":"β","code":"&beta;","unicode":"&#946;"},{"value":"η","code":"&eta;","unicode":"&#951;"},{"value":"μ","code":"&mu;","unicode":"&#956;"},{"value":"ρ","code":"&rho;","unicode":"&#961;"},{"value":"φ","code":"&phi;","unicode":"&#966;"},{"value":"?","code":"&upsih;","unicode":"&#978;"},{"value":"″","code":"&prime;","unicode":"&#8243;"},{"value":"ℜ","code":"&real;","unicode":"&#8476;"},{"value":"→","code":"&rarr;","unicode":"&#8594;"},{"value":"⇑","code":"&uarr;","unicode":"&#8657;"},{"value":"∂","code":"&part;","unicode":"&#8706;"},{"value":"∉","code":"&notin;","unicode":"&#8713;"},{"value":"∗","code":"&lowast;","unicode":"&#8727;"},{"value":"∧","code":"&and;","unicode":"&#8743;"},{"value":"∴","code":"&there4;","unicode":"&#8756;"},{"value":"≡","code":"&equiv;","unicode":"&#8801;"},{"value":"⊄","code":"&nsub;","unicode":"&#8836;"},{"value":"⊥","code":"&perp;","unicode":"&#8869;"},{"value":"?","code":"&rfloor;","unicode":"&#8971;"},{"value":"♣","code":"&clubs;","unicode":"&#9827;"},{"value":"β","code":"&beta;","unicode":"&#914;"},{"value":"η","code":"&eta;","unicode":"&#919;"},{"value":"μ","code":"&mu;","unicode":"&#924;"},{"value":"ρ","code":"&rho;","unicode":"&#929;"},{"value":"χ","code":"&chi;","unicode":"&#935;"},{"value":"γ","code":"&gamma;","unicode":"&#947;"},{"value":"θ","code":"&theta;","unicode":"&#952;"},{"value":"ν","code":"&nu;","unicode":"&#957;"},{"value":"ς","code":"&sigmaf;","unicode":"&#962;"},{"value":"χ","code":"&chi;","unicode":"&#967;"},{"value":"?","code":"&piv;","unicode":"&#982;"},{"value":"‾","code":"&oline;","unicode":"&#8254;"},{"value":"™","code":"&trade;","unicode":"&#8482;"},{"value":"↓","code":"&darr;","unicode":"&#8595;"},{"value":"⇒","code":"&rarr;","unicode":"&#8658;"},{"value":"∃","code":"&exist;","unicode":"&#8707;"},{"value":"∋","code":"&ni;","unicode":"&#8715;"},{"value":"√","code":"&radic;","unicode":"&#8730;"},{"value":"∨","code":"&or;","unicode":"&#8744;"},{"value":"∼","code":"&sim;","unicode":"&#8764;"},{"value":"≤","code":"&le;","unicode":"&#8804;"},{"value":"⊆","code":"&sube;","unicode":"&#8838;"},{"value":"⋅","code":"&sdot;","unicode":"&#8901;"},{"value":"?","code":"&lang;","unicode":"&#9001;"},{"value":"♥","code":"&hearts;","unicode":"&#9829;"},{"value":"γ","code":"&gamma;","unicode":"&#915;"},{"value":"θ","code":"&theta;","unicode":"&#920;"},{"value":"ν","code":"&nu;","unicode":"&#925;"},{"value":"σ","code":"&sigma;","unicode":"&#931;"},{"value":"ψ","code":"&psi;","unicode":"&#936;"},{"value":"δ","code":"&delta;","unicode":"&#948;"},{"value":"ι","code":"&iota;","unicode":"&#953;"},{"value":"ξ","code":"&xi;","unicode":"&#958;"},{"value":"σ","code":"&sigma;","unicode":"&#963;"},{"value":"ψ","code":"&psi;","unicode":"&#968;"},{"value":"•","code":"&bull;","unicode":"&#8226;"},{"value":"⁄","code":"&frasl;","unicode":"&#8260;"},{"value":"ℵ","code":"&alefsym;","unicode":"&#8501;"},{"value":"↔","code":"&harr;","unicode":"&#8596;"},{"value":"⇓","code":"&darr;","unicode":"&#8659;"},{"value":"∅","code":"&empty;","unicode":"&#8709;"},{"value":"∏","code":"&prod;","unicode":"&#8719;"},{"value":"∝","code":"&prop;","unicode":"&#8733;"},{"value":"∩","code":"&cap;","unicode":"&#8745;"},{"value":"∝","code":"&cong;","unicode":"&#8773;"},{"value":"≥","code":"&ge;","unicode":"&#8805;"},{"value":"⊇","code":"&supe;","unicode":"&#8839;"},{"value":"?","code":"&lceil;","unicode":"&#8968;"},{"value":"?","code":"&rang;","unicode":"&#9002;"},{"value":"♦","code":"&diams;","unicode":"&#9830;"},{"value":"δ","code":"&delta;","unicode":"&#916;"},{"value":"ι","code":"&iota;","unicode":"&#921;"},{"value":"ξ","code":"&xi;","unicode":"&#926;"},{"value":"τ","code":"&tau;","unicode":"&#932;"},{"value":"ω","code":"&omega;","unicode":"&#937;"},{"value":"ε","code":"&epsilon;","unicode":"&#949;"},{"value":"κ","code":"&kappa;","unicode":"&#954;"},{"value":"ο","code":"&omicron;","unicode":"&#959;"},{"value":"τ","code":"&tau;","unicode":"&#964;"},{"value":"ω","code":"&omega;","unicode":"&#969;"},{"value":"…","code":"&hellip;","unicode":"&#8230;"},{"value":"℘","code":"&weierp;","unicode":"&#8472;"},{"value":"←","code":"&larr;","unicode":"&#8592;"},{"value":"↵","code":"&crarr;","unicode":"&#8629;"},{"value":"⇔","code":"&harr;","unicode":"&#8660;"},{"value":"∇","code":"&nabla;","unicode":"&#8711;"},{"value":"∑","code":"&sum;","unicode":"&#8721;"},{"value":"∞","code":"&infin;","unicode":"&#8734;"},{"value":"∪","code":"&cup;","unicode":"&#8746;"},{"value":"≈","code":"&asymp;","unicode":"&#8776;"},{"value":"⊂","code":"&sub;","unicode":"&#8834;"},{"value":"⊕","code":"&oplus;","unicode":"&#8853;"},{"value":"?","code":"&rceil;","unicode":"&#8969;"},{"value":"◊","code":"&loz;","unicode":"&#9674;"}]`)
	var characters []Character
	if err := json.Unmarshal(jsonBlob, &characters); err != nil {
		fmt.Println(err)
		return str, err
	}

	for _, character := range characters {
		str = strings.Replace(str, character.Code, character.Value, -1)
	}
	return str, nil
}
```

上記ソースコードを動作させるとMLTの全AAを合わせてGifアニメにすることができます。
![オマエモナー.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/80813baf-0e84-8300-6175-1a46cd02ff60.gif)

## リポジトリ
GitHubにソースコードをアップしているのでよかったら見てみてね。
https://github.com/AAHub/MLT2Gif/tree/master

## まとめ
前回の記事とだいぶ似た内容になってしまいましたが以上です。