├── .gitignore
├── LICENSE
├── README.md
├── bin
└── gifter
├── capture.gif
├── examples
├── animation12-x.gif
├── earth.gif
├── giphy.gif
├── githubnyan.gif
├── kick.gif
├── nyan.gif
├── original.gif
├── owl.gif
├── pacman.gif
├── running.gif
├── smugasspacmanbig.gif
├── spaceship.gif
├── starwars.gif
├── starwars2.gif
├── supermario.gif
└── tilt.gif
├── image.go
├── main.go
├── nonsysioctl.go
├── sysioctl.go
└── terminal.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 | sysioctl
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Simo Endre
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | **`Gifter`** is a gif renderer running in terminal. It takes a gif file as input and plays it directly in the terminal window. It's fully customziable by the supported command flags. **`Gifter`** is build on top of termbox-go.
4 |
5 |
6 |
7 |
8 | ## Install
9 | ```
10 | $ go get -u -v github.com/esimov/gifter
11 | ```
12 | > Note: The terminal must have `xterm-256color` mode enabled.
13 |
14 | Prior running the code make sure that `GOPATH` environment variable is set. Check the documentation for help: https://golang.org/doc/code.html#GOPATH.
15 |
16 | ## Run
17 | You can run the code by the following command:
18 | `go run sysioctl.go terminal.go image.go main.go `.
19 | But the more elegant way is to generate the binary file using `go install`. After this you can run the code as:
20 |
21 | ```
22 | $ gifter
23 | ```
24 |
25 | To finish the gif animation press ``, `CTRL-C`, `CTRL-D` or `q` key. You can even set up the number of iterations the gif file should run with the `-loop` flag. The animation will stop after reaching the provided iteration number.
26 |
27 | ## Commands:
28 | Type `gifter --help` for the supported commands:
29 |
30 | ```
31 | Usage of commands:
32 | -cell string
33 | Used unicode character as cell block (default "▄")
34 | -loop uint
35 | Loop count (default 18446744073709551615)
36 | -fps int
37 | Frame rates (default 120)
38 | -out string
39 | Create a new GIF file with the background color removed (default "output.gif")
40 | -rb
41 | Remove GIF background color
42 | ```
43 | **Note:** there is a flickering issue playing non transparent background gif images. For this reason the `-rb` flag is included. When this flag is used a new gif file is generated with the most dominant color removed (which in most cases is the background color). But for the best visual experience it's advised to use gif files with transparent background.
44 |
45 | ## Author
46 |
47 | * Endre Simo ([@simo_endre](https://twitter.com/simo_endre))
48 |
49 | ## License
50 |
51 | Copyright © 2017 Endre Simo
52 |
53 | This software is distributed under the MIT license. See the LICENSE file for the full license text.
54 |
--------------------------------------------------------------------------------
/bin/gifter:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/bin/gifter
--------------------------------------------------------------------------------
/capture.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/capture.gif
--------------------------------------------------------------------------------
/examples/animation12-x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/animation12-x.gif
--------------------------------------------------------------------------------
/examples/earth.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/earth.gif
--------------------------------------------------------------------------------
/examples/giphy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/giphy.gif
--------------------------------------------------------------------------------
/examples/githubnyan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/githubnyan.gif
--------------------------------------------------------------------------------
/examples/kick.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/kick.gif
--------------------------------------------------------------------------------
/examples/nyan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/nyan.gif
--------------------------------------------------------------------------------
/examples/original.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/original.gif
--------------------------------------------------------------------------------
/examples/owl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/owl.gif
--------------------------------------------------------------------------------
/examples/pacman.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/pacman.gif
--------------------------------------------------------------------------------
/examples/running.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/running.gif
--------------------------------------------------------------------------------
/examples/smugasspacmanbig.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/smugasspacmanbig.gif
--------------------------------------------------------------------------------
/examples/spaceship.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/spaceship.gif
--------------------------------------------------------------------------------
/examples/starwars.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/starwars.gif
--------------------------------------------------------------------------------
/examples/starwars2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/starwars2.gif
--------------------------------------------------------------------------------
/examples/supermario.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/supermario.gif
--------------------------------------------------------------------------------
/examples/tilt.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esimov/gifter/f63d9bdcea53eaeab696bcf2b384e489c885069c/examples/tilt.gif
--------------------------------------------------------------------------------
/image.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "image/color"
6 | "image/gif"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | type GifImg struct {
12 | gif.GIF
13 | }
14 |
15 | func NewGifImg(img *gif.GIF) *GifImg {
16 | return &GifImg{*img}
17 | }
18 |
19 | // Load image
20 | func (gifImg *GifImg) Load(filename string) (*gif.GIF, error) {
21 | ext := filepath.Ext(filename)
22 | if len(ext) < 1 {
23 | fmt.Println("Please provide a Gif file")
24 | os.Exit(1)
25 | }
26 | file, err := os.Open(filename)
27 | if err != nil {
28 | return nil, err
29 | }
30 | defer file.Close()
31 | img, err := gif.DecodeAll(file)
32 | if err != nil {
33 | panic(err)
34 | }
35 | return img, err
36 | }
37 |
38 | // Calculates the average RGB color within the given
39 | // rectangle, and returns the [0,1] range of each component.
40 | func (gifImg *GifImg) CellAvgRGB(img *gif.GIF, dominantColor color.RGBA, startX, startY, endX, endY, index int) uint16 {
41 | var total = [3]uint32{}
42 | var count uint32
43 |
44 | for x := startX; x < endX; x++ {
45 | for y := startY; y < endY; y++ {
46 | gf := img.Image[index]
47 | r, g, b, _ := gf.At(x, y).RGBA()
48 | rd, gd, bd, _ := dominantColor.RGBA()
49 | // remove background color
50 | if rd == r && gd == g && bd == b {
51 | r, g, b = 0x00, 0x00, 0x00
52 | }
53 | // reduce color range to fit in range [0,15]
54 | total[0] += r >> 8
55 | total[1] += g >> 8
56 | total[2] += b >> 8
57 | count++
58 | }
59 | }
60 | r := total[0] / count
61 | g := total[1] / count
62 | b := total[2] / count
63 |
64 | // Converts a 32-bit RGB color into a term256 compatible approximation.
65 | rTerm := (((uint16(r) * 5) + 127) / 255) * 36
66 | gTerm := (((uint16(g) * 5) + 127) / 255) * 6
67 | bTerm := (((uint16(b) * 5) + 127) / 255)
68 |
69 | return rTerm + gTerm + bTerm + 16 + 1
70 | }
71 |
72 | // Get the most dominant color in the image
73 | func (gifImg *GifImg) GetDominantColor(img *gif.GIF) color.RGBA {
74 | imgWidth, imgHeight := img.Config.Width, img.Config.Height
75 | firstFrame := img.Image[0]
76 | histogram := make(map[uint32][]color.RGBA)
77 |
78 | for x := 0; x < imgWidth; x++ {
79 | for y := 0; y < imgHeight; y++ {
80 | r, g, b, a := firstFrame.At(x, y).RGBA()
81 | // get the value from the RGBA
82 | r /= 0xff
83 | g /= 0xff
84 | b /= 0xff
85 | a /= 0xff
86 | pixVal := uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a)
87 | // Add the pixel color from the color range to the histogram map, which index is the pixel color converted to uint32.
88 | // This way we will store all the identical pixels to the same indexed entry.
89 | histogram[pixVal] = append(histogram[pixVal], color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)})
90 | }
91 | }
92 |
93 | var maxVal uint32
94 | var dominantColor color.RGBA
95 | // Find which uint32 converted color occurs mostly in the color range
96 | // We lookup for the length of histogram map indexes
97 | for pix := range histogram {
98 | colorRange := len(histogram[pix])
99 | if uint32(colorRange) > maxVal {
100 | maxVal = uint32(colorRange)
101 | // get the first color from the color range
102 | dominantColor = histogram[pix][0]
103 | }
104 | }
105 | return dominantColor
106 | }
107 |
108 | // Scale the generated image to fit between terminal width & height
109 | func (gifImg *GifImg) Scale(imgWidth, imgHeight, termWidth, termHeight int, ratio float64) (float64, float64) {
110 | width := float64(imgWidth) / (float64(termWidth) * ratio)
111 | height := float64(imgHeight) / (float64(termHeight) * ratio)
112 |
113 | // Avoid deadlock
114 | if width < 1.0 || height < 1.0 { // if image aspect ratio is below 1
115 | width, height = 1.0, 2.0
116 | }
117 | return width, height
118 | }
119 |
120 | // Set terminal cell's dimension
121 | func (gifImg *GifImg) CellSize(x, y int, scaleX, scaleY, ratio float64) (int, int, int, int) {
122 | startX, startY := float64(x)*scaleX, float64(y)*scaleY
123 | endX, endY := startX+scaleX, startY+scaleY*ratio
124 | return int(startX), int(startY), int(endX), int(endY)
125 | }
126 |
127 | func maxValue(values ...float64) float64 {
128 | var max float64
129 | for _, val := range values {
130 | if val > max {
131 | max = val
132 | }
133 | }
134 | return max
135 | }
136 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "image/color"
7 | "image/gif"
8 | "log"
9 | "math"
10 | "math/rand"
11 | "os"
12 | "os/signal"
13 | "sync"
14 | "syscall"
15 | "time"
16 | "unicode/utf8"
17 |
18 | "github.com/nsf/termbox-go"
19 | )
20 |
21 | const HelpBanner = `
22 | Render gif files in terminal.
23 |
24 | Usage: gifter
25 |
26 | Supported Commands:
27 |
28 | `
29 |
30 | var (
31 | wg sync.WaitGroup
32 | gifImg *GifImg
33 | terminal *Terminal
34 | termWidth = Window.Width
35 | termHeight = Window.Height
36 | ratio = Window.Ratio
37 |
38 | // Flags
39 | out string
40 | cell string
41 | rb bool
42 | delay int
43 | count uint64
44 |
45 | fs flag.FlagSet
46 | )
47 |
48 | func init() {
49 | // Flags
50 | fs = *flag.NewFlagSet("Commands", flag.ExitOnError)
51 | fs.BoolVar(&rb, "rb", false, "Remove background color")
52 | fs.StringVar(&out, "out", "output.gif", "Create a new GIF file with the dominant (background) color removed")
53 | fs.StringVar(&cell, "cell", "▄", "Used unicode character as cell block")
54 | fs.IntVar(&delay, "delay", 120, "Animation speed (delay between frame rates)")
55 | fs.Uint64Var(&count, "loop", math.MaxUint64, "Loop count")
56 |
57 | fs.Usage = func() {
58 | fmt.Fprintf(os.Stderr, HelpBanner)
59 | fs.PrintDefaults()
60 | }
61 |
62 | if len(os.Args) <= 1 || os.Args[1] == "--help" || os.Args[1] == "-h" {
63 | fmt.Fprintf(os.Stderr, HelpBanner)
64 | fs.PrintDefaults()
65 |
66 | fmt.Println("Exit the animation by pressing or 'q'.\n")
67 | os.Exit(0)
68 | }
69 |
70 | fs.Parse(os.Args[2:])
71 | }
72 |
73 | func main() {
74 | rand.Seed(time.Now().UTC().UnixNano())
75 | signalChan := make(chan os.Signal, 2)
76 | signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
77 |
78 | if delay <= 0 {
79 | delay = 1
80 | }
81 |
82 | terminal = &Terminal{}
83 |
84 | img := loadGif(os.Args[1])
85 | gifImg = &GifImg{}
86 |
87 | termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
88 | err := termbox.Init()
89 | if err != nil {
90 | log.Fatalf("Unable to initialize termbox: %v", err)
91 | }
92 | defer termbox.Close()
93 | termbox.SetOutputMode(termbox.Output256)
94 |
95 | if rb {
96 | dominantColor := gifImg.GetDominantColor(img)
97 | for idx := 0; idx < len(img.Image); idx++ {
98 | for x := 0; x < img.Config.Width; x++ {
99 | for y := 0; y < img.Config.Height; y++ {
100 | gf := img.Image[idx]
101 | r, g, b, a := gf.At(x, y).RGBA()
102 | rd, gd, bd, _ := dominantColor.RGBA()
103 | // remove background color
104 | if rd == r && gd == g && bd == b {
105 | r, g, b = 0x00, 0x00, 0x00
106 | }
107 | gf.Set(x, y, color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)})
108 | }
109 | }
110 | }
111 | file, err := os.Create(out)
112 | if err != nil {
113 | fmt.Println(err)
114 | os.Exit(1)
115 | }
116 | defer file.Close()
117 |
118 | // Write out the data into the new GIF file
119 | err = gif.EncodeAll(file, img)
120 | if err != nil {
121 | fmt.Println(err)
122 | os.Exit(1)
123 | }
124 | img = loadGif(out)
125 | }
126 | // Render the gif image
127 | draw(img)
128 | }
129 |
130 | func loadGif(fileName string) *gif.GIF {
131 | img, err := gifImg.Load(fileName)
132 | if err != nil {
133 | fmt.Printf("%v\n", err)
134 | os.Exit(1)
135 | }
136 | return img
137 | }
138 |
139 | // Render gif on terminal window
140 | func draw(img *gif.GIF) {
141 | var startX, startY, endX, endY int
142 | var loopCount uint64
143 |
144 | ticker := time.Tick(time.Millisecond * time.Duration(delay))
145 | imgWidth, imgHeight := img.Config.Width, img.Config.Height
146 | scaleX, scaleY := gifImg.Scale(imgWidth, imgHeight, termWidth, termHeight, ratio)
147 | dominantColor := gifImg.GetDominantColor(img)
148 |
149 | eventQueue := make(chan termbox.Event)
150 | go func() {
151 | for {
152 | eventQueue <- termbox.PollEvent()
153 | }
154 | }()
155 |
156 | // This where the magic happens
157 | loop:
158 | for {
159 | if loopCount >= count {
160 | os.Remove(out)
161 | break loop
162 | }
163 | for idx := 0; idx < len(img.Image); idx++ {
164 | select {
165 | case ev := <-eventQueue:
166 | switch ev.Type {
167 | case termbox.EventKey:
168 | if ev.Ch == 'q' || ev.Key == termbox.KeyEsc || ev.Key == termbox.KeyCtrlC || ev.Key == termbox.KeyCtrlD {
169 | os.Remove(out)
170 | break loop
171 | }
172 | case termbox.EventResize:
173 | //break loop
174 | }
175 | default:
176 | wg.Add(1)
177 | <-ticker
178 | go func(idx int) {
179 | defer wg.Done()
180 | for x := 0; x < img.Config.Width; x++ {
181 | for y := 0; y < img.Config.Height; y++ {
182 | if img.Config.Width <= img.Config.Height {
183 | startX, startY, endX, endY = gifImg.CellSize(x, y, scaleX, scaleY*ratio, ratio)
184 | } else {
185 | startX, startY, endX, endY = gifImg.CellSize(x, y, scaleX, scaleY, ratio)
186 | }
187 | col := gifImg.CellAvgRGB(img, dominantColor, startX, startY, endX, (startY+endY)/2, idx)
188 | colorUp := termbox.Attribute(col)
189 |
190 | col = gifImg.CellAvgRGB(img, dominantColor, startX, (startY+endY)/2, endX, endY, idx)
191 | colorDown := termbox.Attribute(col)
192 |
193 | r, _ := utf8.DecodeRuneInString(cell)
194 | termbox.SetCell(x, y, r, colorDown, colorUp)
195 | }
196 | }
197 | termbox.Flush()
198 | }(idx)
199 | }
200 | wg.Wait()
201 | time.Sleep(10 * time.Millisecond)
202 | }
203 | loopCount++
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/nonsysioctl.go:
--------------------------------------------------------------------------------
1 | // +build windows plan9 solaris
2 |
3 | package main
4 |
5 | func getWinsize() (*winsize, error) {
6 | ws := new(winsize)
7 |
8 | ws.Col = 80
9 | ws.Row = 24
10 |
11 | return ws, nil
12 | }
13 |
--------------------------------------------------------------------------------
/sysioctl.go:
--------------------------------------------------------------------------------
1 | // +build !windows,!plan9,!solaris
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "runtime"
9 | "syscall"
10 | "unsafe"
11 | )
12 |
13 | func getWinsize() (*winsize, error) {
14 | ws := new(winsize)
15 |
16 | var _TIOCGWINSZ int64
17 |
18 | switch runtime.GOOS {
19 | case "linux":
20 | _TIOCGWINSZ = 0x5413
21 | case "darwin":
22 | _TIOCGWINSZ = 1074295912
23 | }
24 |
25 | r1, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
26 | uintptr(syscall.Stdin),
27 | uintptr(_TIOCGWINSZ),
28 | uintptr(unsafe.Pointer(ws)),
29 | )
30 |
31 | if int(r1) == -1 {
32 | fmt.Println("Error:", os.NewSyscallError("GetWinsize", errno))
33 | return nil, os.NewSyscallError("GetWinsize", errno)
34 | }
35 | return ws, nil
36 | }
37 |
--------------------------------------------------------------------------------
/terminal.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "os"
8 | "strings"
9 | )
10 |
11 | type winsize struct {
12 | Rows uint16
13 | Cols uint16
14 | Width uint16
15 | Height uint16
16 | }
17 |
18 | type Terminal struct {
19 | Width int
20 | Height int
21 | Ratio float64
22 | }
23 |
24 | const defaultRatio float64 = 1.0 // The terminal's default cursor width/height ratio
25 |
26 | var (
27 | screen *bytes.Buffer = new(bytes.Buffer)
28 | output *bufio.Writer = bufio.NewWriter(os.Stdout)
29 | Window *Terminal = getTerminal()
30 | )
31 |
32 | func init() {
33 | // Clear console
34 | output.WriteString("\033[2J")
35 | // Remove blinking cursor
36 | output.WriteString("\033[?25l")
37 | }
38 |
39 | // Get terminal size
40 | func getTerminal() *Terminal {
41 | var whRatio float64
42 | ws, err := getWinsize()
43 | if err != nil {
44 | panic(err)
45 | }
46 | whRatio = defaultRatio
47 | if ws.Width > 0 && ws.Height > 0 {
48 | whRatio = float64(ws.Height/ws.Rows) / float64(ws.Width/ws.Cols) * 0.5
49 | }
50 | return &Terminal{
51 | Width: int(ws.Cols),
52 | Height: int(ws.Rows),
53 | Ratio: whRatio,
54 | }
55 | }
56 |
57 | // Flush buffer and ensure that it will not overflow screen
58 | func (terminal *Terminal) Flush() {
59 | for idx, str := range strings.Split(screen.String(), "\n") {
60 | if idx > Window.Height {
61 | return
62 | }
63 | output.WriteString(str + "\n")
64 | }
65 |
66 | screen.Reset()
67 | output.Flush()
68 | terminal.MoveCursor(0, 0)
69 | }
70 |
71 | // Move cursor to given position
72 | func (terminal *Terminal) MoveCursor(x int, y int) {
73 | fmt.Fprintf(screen, "\033[%d;%dH", x, y)
74 | }
75 |
--------------------------------------------------------------------------------