GolangでASTをGifに変換する(Delay対応付き)

自分一人で何番煎じかわからないネタですが、ASTをGifに変換してみたいと思います。類似ネタのMLTをGifに変換する記事は以下のリンクを参照してください。
[GolangでMLTをGifに変換する](https://qiita.com/xiaca/items/2f42c4688800ca2e3168)

## ASTとは？
おそらくAsciiArt Story Textの略です、たぶん。
やる夫スレなどのストーリ専用のファイル形式です。拡張子はast。

MLTが[SPLIT]でAAを区切っていたのに対し、ASTは[AA]で区切ります。いちばんの違いは[AA]の後ろに[]で好きな名前をつけられることです。例えば、[AA][やる夫]のように。

## ASTをGifに変換する

さて、基本的な処理は別記事のMLTをGifに変換するのと同じ内容です。が、しかし、せっかくASTなので、Delayに対応してみたいと思います。つまり、[AA][(Delayさせたい数値)]とすることでDelayに対応しました。ソースコードは以下。

```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"
	"strconv"
	"strings"
)

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

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

type Aa struct {
	Name  string `json:"name,omitempty"`
	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, ".ast") == -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 ast Ast
	namepos := strings.LastIndex(name, ".")
	new_name := name[:namepos]

	ast.Name = new_name

	aa := &Aa{}
	for idx, l := range lines {
		l, _ = sjis_to_utf8(l)
		l, _ = escape_html_special_caharacter(l)
		if strings.Index(l, "[AA][") == 0 {
			str := l[5:]
			aa.Name = str[:len(str)-1]
			if aa.Value != "" {
				ast.Aa = append(ast.Aa, aa)
			}
			aa = &Aa{}
		} else {
			aa.Value = aa.Value + l + "\n"
		}
		if idx == len(lines)-1 {
			if aa.Value != "" {
				ast.Aa = append(ast.Aa, aa)
			}
		}
	}

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

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

	b := ast.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 *Ast) GenerateGifImage() []byte {
	outGif := &gif.GIF{}
	delay := 0
	sWidth, sHeight := calcrateMaxWidthAndHeight(this.Aa, FONT_SIZE, LINE_HEIGHT)
	for _, a := range this.Aa {
		lines := strings.Split(a.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)
		delay, _ = strconv.Atoi(a.Name)
		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
}
```

上記、ソースコードを実行してみた結果です。3枚に1枚Delayが入っているのがわかると思います。
![オマエモナー.gif](https://qiita-image-store.s3.amazonaws.com/0/165335/ab2493d1-6518-bd13-60aa-79906c309f71.gif)

## ソースコード
例によってGitHubにソースコードをあげています。
https://github.com/AAHub/AST2Gif

## まとめ
以上、Delayに対応したGifを作ることができました。もし、何か応用できそうなことがあったら幸いです。参考になったなら一声かけてもらったりいいね押してもらったりすると喜びます。
それでは。