├── .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 |

Gifter

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 | Sample gif 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 | --------------------------------------------------------------------------------