全体のソース
動機
GIFの開始と終わりを判別するために、GIFの最初の方に”START”みたいなラベルを挿入したい。
ImageMagickには依存したくなかったのでGolangの標準パッケージでなんとかしてみた。
環境
$ go version go version go1.16.2 darwin/arm64
GIFの分割
GIFファイルをフレームごとにpngファイルに分割する
go - How to split gif into images - Stack Overflow
func splitGif(reader io.Reader) (names []string, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("Error while decoding: %s", r) } }() gif, err := gif.DecodeAll(reader) if err != nil { return []string{""}, err } imgWidth, imgHeight := getGifDimensions(gif) overpaintImage := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight)) draw.Draw(overpaintImage, overpaintImage.Bounds(), gif.Image[0], image.ZP, draw.Src) var ns []string for i, srcImg := range gif.Image { draw.Draw(overpaintImage, overpaintImage.Bounds(), srcImg, image.ZP, draw.Over) file, err := os.Create(fmt.Sprintf("%s%d%s", "temp", i, ".png")) if err != nil { return []string{""}, err } err = png.Encode(file, overpaintImage) if err != nil { return []string{""}, err } ns = append(ns, file.Name()) file.Close() } return ns, nil } func getGifDimensions(gif *gif.GIF) (x, y int) { var lowestX int var lowestY int var highestX int var highestY int for _, img := range gif.Image { if img.Rect.Min.X < lowestX { lowestX = img.Rect.Min.X } if img.Rect.Min.Y < lowestY { lowestY = img.Rect.Min.Y } if img.Rect.Max.X > highestX { highestX = img.Rect.Max.X } if img.Rect.Max.Y > highestY { highestY = img.Rect.Max.Y } } return highestX - lowestX, highestY - lowestY }
呼び出し
func main() { // 対象のGIFを読み込む filename := os.Args[1] f, err := os.Open(filename) if err != nil { log.Fatalf("cannot open file %q: %v", filename, err) } defer f.Close() // GIF分割 names, err := splitGif(f) if err != nil { log.Fatalf(err.Error()) } }
画像に文字をいれる
フォントファイルはmain.go
と同じ階層に設置しておく。851MkPOP_002.ttfをフォントとして利用させてもらった。
func addLabel(file *os.File, text string) { img, err := png.Decode(file) if err != nil { log.Fatalf("failed to decode image: %s", err.Error()) } fmt.Println(img.Bounds()) dst := image.NewRGBA(img.Bounds()) draw.Draw(dst, dst.Bounds(), img, image.Point{}, draw.Src) // col := color.RGBA{255, 255, 255, 1.0} opt := truetype.Options{ Size: 40, } ft := loadFont() face := truetype.NewFace(ft, &opt) x, y := 100, 100 dot := fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 26)} d := &font.Drawer{ Dst: dst, Src: image.White, Face: face, Dot: dot, } d.DrawString(text) newFile, err := os.Create(file.Name()) if err != nil { log.Fatalf("failed to create file: %s", err.Error()) } defer newFile.Close() b := bufio.NewWriter(newFile) if err := png.Encode(b, dst); err != nil { log.Fatalf("failed to encode image: %s", err.Error()) } b.Flush() } func loadFont() (font *truetype.Font) { ttf, err := ioutil.ReadFile("851MkPOP_002.ttf") if err != nil { log.Fatalf("failed to load font: %s", err.Error()) } ft, err := truetype.Parse(ttf) if err != nil { log.Fatalf("failed to parse font: %s", err.Error()) } return ft }
呼び出し
func main() { // 対象のGIFを読み込む // ...略 // GIF分割 // ...略 // 最初の5フレームに文字を挿入 for i := 0; i < 5; i++ { f1, err := os.Open(names[i]) if err != nil { log.Fatalf("failed to open file: %s", err.Error()) } defer f1.Close() addLabel(f1, "START") } }
GIFの結合
pngファイルを結合してGIFに戻す。image.NewPaletted(png.Bounds(), palette.Plan9)
でパレットを作成してGIFを作成という流れになるが、このときに画像の全体の色が若干くすむので完全に元通りのGIFにはならない。
func makeGif(names []string) { outGif := &gif.GIF{} for _, name := range names { fmt.Println(name) f, err := os.Open(name) if err != nil { panic(err) } defer f.Close() png, _, err := image.Decode(f) if err != nil { panic(err) } palettedImage := image.NewPaletted(png.Bounds(), palette.Plan9) draw.Draw(palettedImage, palettedImage.Rect, png, png.Bounds().Min, draw.Over) outGif.Image = append(outGif.Image, palettedImage) outGif.Delay = append(outGif.Delay, 0) } f, _ := os.OpenFile("out.gif", os.O_WRONLY|os.O_CREATE, 0600) defer f.Close() gif.EncodeAll(f, outGif) }
後片付け(GIF分割時に生成したpngファイルの削除)
func removeTempFile() { files, err := filepath.Glob("temp*") if err != nil { panic(err) } for _, f := range files { if err := os.Remove(f); err != nil { panic(err) } } }
これをmain関数終了時とCtrl-Cで終了されたときに発火するようにしておく。
func main() { defer removeTempFile() go func() { trap := make(chan os.Signal, 1) signal.Notify(trap, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT) s := <-trap fmt.Printf("Received signal %s\n", s) removeTempFile() os.Exit(1) }() }
全体のソース
終わり
ここまでやったところでGIF Breweryで文字を挿入することができることが発覚した。しかもFadeIn/OutもできてGIFの画質も落ちないので完璧だった。GIF Brewery最高。