├── LICENSE ├── README.md ├── _example ├── goswebcam │ ├── go.mod │ ├── go.sum │ └── main.go └── sixelview │ ├── go.mod │ ├── go.sum │ └── main.go ├── cmd ├── goscat │ └── main.go ├── gosd │ └── main.go ├── gosgif │ └── main.go ├── gosl │ ├── main.go │ └── public │ │ ├── data01.png │ │ ├── data02.png │ │ └── data03.png └── gosr │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum └── sixel.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yasuhiro Matsumoto 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 | # go-sixel 2 | 3 | DRCS Sixel Encoder/Decoder 4 | 5 | ![](http://go-gyazo.appspot.com/75ec3ce96dfc573e.png) 6 | 7 | ## Installation 8 | 9 | ``` 10 | $ go get github.com/mattn/go-sixel 11 | ``` 12 | 13 | You can install gosr (go sixel renderer), gosd (go sixel decoder) with following installation instruction. 14 | 15 | ``` 16 | $ go get github.com/mattn/go-sixel/cmd/gosr 17 | $ go get github.com/mattn/go-sixel/cmd/gosd 18 | ``` 19 | 20 | |Command|Description | 21 | |-------|--------------------| 22 | |gosr |Image renderer | 23 | |gosd |Decoder to png | 24 | |goscat |Render cats | 25 | |gosgif |Render animation GIF| 26 | |gosl |Run SL | 27 | 28 | ## Usage 29 | 30 | Encode 31 | ``` 32 | $ cat foo.png | gosr - 33 | ``` 34 | 35 | Decode 36 | 37 | ``` 38 | $ cat foo.drcs | gosd > foo.png 39 | ``` 40 | 41 | Use as library 42 | 43 | ```go 44 | img, _, _ := image.Decode(filename) 45 | sixel.NewEncoder(os.Stdout).Encode(img) 46 | ``` 47 | 48 | ## License 49 | 50 | MIT 51 | 52 | ## Author 53 | 54 | Yasuhiro Matsumoto (a.k.a mattn) 55 | -------------------------------------------------------------------------------- /_example/goswebcam/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-sixel/_example/goswebcam 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/mattn/go-sixel v0.0.1 7 | gocv.io/x/gocv v0.31.0 8 | ) 9 | 10 | require github.com/soniakeys/quant v1.0.0 // indirect 11 | replace github.com/mattn/go-sixel => ../.. 12 | -------------------------------------------------------------------------------- /_example/goswebcam/go.sum: -------------------------------------------------------------------------------- 1 | github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs= 2 | github.com/mattn/go-sixel v0.0.1 h1:rhJSpux2xjsmXdXqY694uiEC0Rwxt6jYoq7Bahqo2xs= 3 | github.com/mattn/go-sixel v0.0.1/go.mod h1:zlzhYSuMbLdRdrxfutExxGpC+Pf2uUTJ6GpVQ4LB5dc= 4 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 5 | github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 6 | github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 7 | gocv.io/x/gocv v0.31.0 h1:BHDtK8v+YPvoSPQTTiZB2fM/7BLg6511JqkruY2z6LQ= 8 | gocv.io/x/gocv v0.31.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= 9 | -------------------------------------------------------------------------------- /_example/goswebcam/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "github.com/mattn/go-sixel" 11 | 12 | "gocv.io/x/gocv" 13 | ) 14 | 15 | func main() { 16 | webcam, err := gocv.VideoCaptureDevice(0) 17 | if err != nil { 18 | log.Fatal(err.Error()) 19 | } 20 | defer webcam.Close() 21 | 22 | webcam.Set(gocv.VideoCaptureFrameWidth, 300) 23 | webcam.Set(gocv.VideoCaptureFrameHeight, 200) 24 | 25 | loop := true 26 | sc := make(chan os.Signal, 1) 27 | signal.Notify(sc, os.Interrupt) 28 | go func() { 29 | <-sc 30 | loop = false 31 | }() 32 | 33 | im := gocv.NewMat() 34 | 35 | fmt.Print("\u001B[?25l") 36 | defer fmt.Print("\u001B[?25h") 37 | fmt.Print("\x1b[s") 38 | enc := sixel.NewEncoder(os.Stdout) 39 | for loop { 40 | if ok := webcam.Read(&im); !ok { 41 | continue 42 | } 43 | img, err := im.ToImage() 44 | if err != nil { 45 | continue 46 | } 47 | fmt.Print("\x1b[u") 48 | err = enc.Encode(img) 49 | if err != nil { 50 | break 51 | } 52 | time.Sleep(10 * time.Millisecond) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /_example/sixelview/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-sixel/_example/sixelview 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5 7 | github.com/mattn/go-sixel v0.0.1 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-pointer v0.0.1 // indirect 12 | github.com/soniakeys/quant v1.0.0 // indirect 13 | github.com/stretchr/testify v1.8.0 // indirect 14 | ) 15 | replace github.com/mattn/go-sixel => ../.. 16 | -------------------------------------------------------------------------------- /_example/sixelview/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5 h1:GMB3MVJnxysGrSvjWGsgK8L3XGI3F4etQQq37Py6W5A= 5 | github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5/go.mod h1:PwzwfeB5syFHXORC3MtPylVcjIoTDT/9cvkKpEndGVI= 6 | github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 7 | github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 8 | github.com/mattn/go-sixel v0.0.1 h1:rhJSpux2xjsmXdXqY694uiEC0Rwxt6jYoq7Bahqo2xs= 9 | github.com/mattn/go-sixel v0.0.1/go.mod h1:zlzhYSuMbLdRdrxfutExxGpC+Pf2uUTJ6GpVQ4LB5dc= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 13 | github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 16 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 18 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /_example/sixelview/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/mattn/go-gtk/gdkpixbuf" 7 | "github.com/mattn/go-gtk/gtk" 8 | "github.com/mattn/go-sixel" 9 | "image" 10 | "image/png" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | type Image struct { 16 | name string 17 | img image.Image 18 | } 19 | 20 | func main() { 21 | var images []Image 22 | if len(os.Args) == 1 { 23 | var in image.Image 24 | err := sixel.NewDecoder(os.Stdin).Decode(&in) 25 | if err != nil { 26 | fmt.Fprintln(os.Stderr, err) 27 | os.Exit(1) 28 | } 29 | images = append(images, Image{ 30 | name: "stdin", 31 | img: in}) 32 | 33 | } else { 34 | for _, arg := range os.Args[1:] { 35 | f, err := os.Open(arg) 36 | if err != nil { 37 | fmt.Fprintln(os.Stderr, err) 38 | os.Exit(1) 39 | } 40 | var in image.Image 41 | err = sixel.NewDecoder(f).Decode(&in) 42 | if err != nil { 43 | fmt.Fprintln(os.Stderr, err) 44 | os.Exit(1) 45 | } 46 | f.Close() 47 | images = append(images, Image{ 48 | name: filepath.Base(f.Name()), 49 | img: in}) 50 | } 51 | } 52 | 53 | gtk.Init(nil) 54 | 55 | window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL) 56 | window.Connect("destroy", gtk.MainQuit) 57 | notebook := gtk.NewNotebook() 58 | for _, img := range images { 59 | var buf bytes.Buffer 60 | err := png.Encode(&buf, img.img) 61 | if err != nil { 62 | fmt.Fprintln(os.Stderr, err) 63 | os.Exit(1) 64 | } 65 | loader, gerr := gdkpixbuf.NewLoaderWithType("png") 66 | if gerr != nil { 67 | fmt.Fprintln(os.Stderr, gerr) 68 | os.Exit(1) 69 | } 70 | _, gerr = loader.Write(buf.Bytes()) 71 | if gerr != nil { 72 | fmt.Fprintln(os.Stderr, gerr) 73 | os.Exit(1) 74 | } 75 | 76 | gimg := gtk.NewImage() 77 | gimg.SetFromPixbuf(loader.GetPixbuf()) 78 | notebook.AppendPage(gimg, gtk.NewLabel(img.name)) 79 | } 80 | window.Add(notebook) 81 | window.SetTitle("SixelViewer") 82 | window.ShowAll() 83 | gtk.Main() 84 | } 85 | -------------------------------------------------------------------------------- /cmd/goscat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "image" 7 | _ "image/gif" 8 | _ "image/jpeg" 9 | _ "image/png" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "github.com/mattn/go-sixel" 15 | ) 16 | 17 | type item struct { 18 | ID string `json:"id"` 19 | URL string `json:"url"` 20 | Width int `json:"width"` 21 | Height int `json:"height"` 22 | } 23 | 24 | func main() { 25 | resp, err := http.Get("https://api.thecatapi.com/v1/images/search") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | defer resp.Body.Close() 30 | var items []item 31 | err = json.NewDecoder(resp.Body).Decode(&items) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | resp, err = http.Get(items[0].URL) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | defer resp.Body.Close() 40 | 41 | img, _, err := image.Decode(resp.Body) 42 | if err != nil { 43 | log.Fatal(err, items[0].URL) 44 | } 45 | 46 | buf := bufio.NewWriter(os.Stdout) 47 | defer buf.Flush() 48 | 49 | enc := sixel.NewEncoder(buf) 50 | enc.Dither = true 51 | err = enc.Encode(img) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/gosd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | "image/png" 8 | "os" 9 | 10 | "github.com/mattn/go-sixel" 11 | ) 12 | 13 | func main() { 14 | flag.Usage = func() { 15 | fmt.Println("Usage of " + os.Args[0] + ": gosd [filename]") 16 | flag.PrintDefaults() 17 | } 18 | flag.Parse() 19 | var img image.Image 20 | err := sixel.NewDecoder(os.Stdin).Decode(&img) 21 | if err != nil { 22 | fmt.Fprintln(os.Stderr, err) 23 | os.Exit(1) 24 | } 25 | 26 | if flag.NArg() == 0 { 27 | err = png.Encode(os.Stdout, img) 28 | if err != nil { 29 | fmt.Fprintln(os.Stderr, err) 30 | os.Exit(1) 31 | } 32 | } else { 33 | f, err := os.Create(flag.Arg(0)) 34 | if err != nil { 35 | fmt.Fprintln(os.Stderr, err) 36 | os.Exit(1) 37 | } 38 | err = png.Encode(f, img) 39 | if err != nil { 40 | fmt.Fprintln(os.Stderr, err) 41 | os.Exit(1) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cmd/gosgif/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color/palette" 7 | "image/draw" 8 | "image/gif" 9 | "io" 10 | "log" 11 | "math" 12 | "os" 13 | "strings" 14 | "syscall" 15 | "time" 16 | "unsafe" 17 | 18 | "github.com/mattn/go-sixel" 19 | ) 20 | 21 | type window struct { 22 | Row uint16 23 | Col uint16 24 | Xpixel uint16 25 | Ypixel uint16 26 | } 27 | 28 | func main() { 29 | var r io.Reader 30 | if len(os.Args) > 1 { 31 | f, err := os.Open(os.Args[1]) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | defer f.Close() 36 | r = f 37 | } else { 38 | r = os.Stdin 39 | } 40 | 41 | g, err := gif.DecodeAll(r) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | fmt.Print("\x1b[s") 46 | enc := sixel.NewEncoder(os.Stdout) 47 | enc.Width = g.Config.Width 48 | enc.Height = g.Config.Height 49 | 50 | var w window 51 | _, _, err = syscall.Syscall(syscall.SYS_IOCTL, 52 | os.Stdout.Fd(), 53 | syscall.TIOCGWINSZ, 54 | uintptr(unsafe.Pointer(&w)), 55 | ) 56 | if w.Xpixel > 0 && w.Ypixel > 0 && w.Col > 0 && w.Row > 0 { 57 | height := float64(w.Ypixel) / float64(w.Row) 58 | lines := int(math.Ceil(float64(enc.Height) / height)) 59 | fmt.Print(strings.Repeat("\n", lines)) 60 | fmt.Printf("\x1b[%dA", lines) 61 | fmt.Print("\x1b[s") 62 | } 63 | 64 | var back draw.Image 65 | if g.BackgroundIndex != 0 { 66 | back = image.NewPaletted(g.Image[0].Bounds(), palette.WebSafe) 67 | } 68 | 69 | for { 70 | t := time.Now() 71 | for j := 0; j < len(g.Image); j++ { 72 | fmt.Print("\x1b[u") 73 | if back != nil { 74 | draw.Draw(back, back.Bounds(), &image.Uniform{g.Image[j].Palette[g.BackgroundIndex]}, image.Pt(0, 0), draw.Src) 75 | draw.Draw(back, back.Bounds(), g.Image[j], image.Pt(0, 0), draw.Src) 76 | err = enc.Encode(back) 77 | } else { 78 | err = enc.Encode(g.Image[j]) 79 | } 80 | if err != nil { 81 | return 82 | } 83 | span := time.Second * time.Duration(g.Delay[j]) / 100 84 | if time.Now().Sub(t) < span { 85 | time.Sleep(span) 86 | } 87 | t = time.Now() 88 | } 89 | if g.LoopCount != 0 { 90 | g.LoopCount-- 91 | if g.LoopCount == 0 { 92 | break 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cmd/gosl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "image/png" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/mattn/go-sixel" 13 | ) 14 | 15 | //go:embed public 16 | var fs embed.FS 17 | 18 | func loadImage(fs embed.FS, n string) []byte { 19 | f, err := fs.Open(n) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer f.Close() 24 | img, err := png.Decode(f) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | var buf bytes.Buffer 29 | err = sixel.NewEncoder(&buf).Encode(img) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | return buf.Bytes() 34 | } 35 | 36 | func main() { 37 | var img [4][]byte 38 | 39 | img[0] = loadImage(fs, "public/data01.png") 40 | img[1] = loadImage(fs, "public/data02.png") 41 | img[2] = loadImage(fs, "public/data03.png") 42 | img[3] = img[1] 43 | 44 | w := os.Stdout 45 | w.Write([]byte("\x1b[?25l\x1b[s")) 46 | for i := 0; i < 70; i++ { 47 | w.Write([]byte("\x1b[u")) 48 | w.Write([]byte(strings.Repeat(" ", i))) 49 | w.Write(img[i%4]) 50 | w.Sync() 51 | time.Sleep(100 * time.Millisecond) 52 | } 53 | w.Write([]byte("\r\x1b[?25h")) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/gosl/public/data01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/go-sixel/674f6ccf2e8cf1fc65f4368044d0727db1509f36/cmd/gosl/public/data01.png -------------------------------------------------------------------------------- /cmd/gosl/public/data02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/go-sixel/674f6ccf2e8cf1fc65f4368044d0727db1509f36/cmd/gosl/public/data02.png -------------------------------------------------------------------------------- /cmd/gosl/public/data03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattn/go-sixel/674f6ccf2e8cf1fc65f4368044d0727db1509f36/cmd/gosl/public/data03.png -------------------------------------------------------------------------------- /cmd/gosr/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-sixel/cmd/gosr 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 7 | github.com/mattn/go-sixel v0.0.4 8 | ) 9 | 10 | replace github.com/mattn/go-sixel => ../.. 11 | -------------------------------------------------------------------------------- /cmd/gosr/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= 2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 3 | github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 4 | github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 5 | -------------------------------------------------------------------------------- /cmd/gosr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "image" 8 | _ "image/gif" 9 | _ "image/jpeg" 10 | _ "image/png" 11 | "math" 12 | "os" 13 | 14 | "github.com/BurntSushi/graphics-go/graphics" 15 | "github.com/mattn/go-sixel" 16 | ) 17 | 18 | var ( 19 | fBlur = flag.String("blur", "", "Blur image by [Dev,Size]") 20 | fResize = flag.String("resize", "", "Resize image by [WxH]") 21 | fRotate = flag.Float64("rotate", 0.0, "Rotate image by [N] deg") 22 | ) 23 | 24 | func render(filename string) error { 25 | var f *os.File 26 | var err error 27 | if filename != "-" { 28 | f, err = os.Open(filename) 29 | if err != nil { 30 | return err 31 | } 32 | defer f.Close() 33 | } else { 34 | f = os.Stdin 35 | } 36 | img, _, err := image.Decode(f) 37 | if err != nil { 38 | return err 39 | } 40 | if *fResize != "" { 41 | var w, h uint 42 | fmt.Sscanf(*fResize, "%dx%d", &w, &h) 43 | rx := float64(img.Bounds().Dx()) / float64(w) 44 | ry := float64(img.Bounds().Dy()) / float64(h) 45 | if rx < ry { 46 | w = uint(float64(img.Bounds().Dx()) / ry) 47 | } else { 48 | h = uint(float64(img.Bounds().Dy()) / rx) 49 | } 50 | tmp := image.NewNRGBA64(image.Rect(0, 0, int(w), int(h))) 51 | err = graphics.Scale(tmp, img) 52 | if err != nil { 53 | return err 54 | } 55 | img = tmp 56 | } 57 | if *fRotate != 0.0 { 58 | d := math.Sqrt(math.Pow(float64(img.Bounds().Dx()), 2) + math.Pow(float64(img.Bounds().Dy()), 2)) 59 | sin, cos := math.Sincos(math.Atan2(float64(img.Bounds().Dx()), float64(img.Bounds().Dy())) + *fRotate) 60 | if sin < cos { 61 | sin = cos 62 | } else { 63 | cos = sin 64 | } 65 | tmp := image.NewNRGBA64(image.Rect(0, 0, int(cos*d), int(sin*d))) 66 | err = graphics.Rotate(tmp, img, &graphics.RotateOptions{*fRotate}) 67 | if err != nil { 68 | return err 69 | } 70 | img = tmp 71 | } 72 | if *fBlur != "" { 73 | var d float64 74 | var s int 75 | fmt.Sscanf(*fBlur, "%f,%d", &d, &s) 76 | tmp := image.NewNRGBA64(img.Bounds()) 77 | err = graphics.Blur(tmp, img, &graphics.BlurOptions{d, s}) 78 | if err != nil { 79 | return err 80 | } 81 | img = tmp 82 | } 83 | buf := bufio.NewWriter(os.Stdout) 84 | defer buf.Flush() 85 | 86 | enc := sixel.NewEncoder(buf) 87 | enc.Dither = true 88 | return enc.Encode(img) 89 | } 90 | 91 | func main() { 92 | flag.Usage = func() { 93 | fmt.Println("Usage of " + os.Args[0] + ": gosr [images]") 94 | flag.PrintDefaults() 95 | } 96 | flag.Parse() 97 | if flag.NArg() == 0 { 98 | flag.Usage() 99 | os.Exit(1) 100 | } 101 | for _, arg := range flag.Args() { 102 | err := render(arg) 103 | if err != nil { 104 | fmt.Fprintln(os.Stderr, err) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-sixel 2 | 3 | go 1.16 4 | 5 | require github.com/soniakeys/quant v1.0.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 2 | github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 3 | -------------------------------------------------------------------------------- /sixel.go: -------------------------------------------------------------------------------- 1 | package sixel 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "image/draw" 11 | "io" 12 | "os" 13 | 14 | "github.com/soniakeys/quant/median" 15 | ) 16 | 17 | // Encoder encode image to sixel format 18 | type Encoder struct { 19 | w io.Writer 20 | 21 | // Dither, if true, will dither the image when generating a paletted version 22 | // using the Floyd–Steinberg dithering algorithm. 23 | Dither bool 24 | 25 | // Width is the maximum width to draw to. 26 | Width int 27 | // Height is the maximum height to draw to. 28 | Height int 29 | 30 | // Colors sets the number of colors for the encoder to quantize if needed. 31 | // If the value is below 2 (e.g. the zero value), then 255 is used. 32 | // A color is always reserved for alpha, so 2 colors give you 1 color. 33 | Colors int 34 | } 35 | 36 | // NewEncoder return new instance of Encoder 37 | func NewEncoder(w io.Writer) *Encoder { 38 | return &Encoder{w: w} 39 | } 40 | 41 | const ( 42 | specialChNr = byte(0x6d) 43 | specialChCr = byte(0x64) 44 | ) 45 | 46 | // Encode do encoding 47 | func (e *Encoder) Encode(img image.Image) error { 48 | nc := e.Colors // (>= 2, 8bit, index 0 is reserved for transparent key color) 49 | if nc < 2 { 50 | nc = 255 51 | } 52 | 53 | width, height := img.Bounds().Dx(), img.Bounds().Dy() 54 | if width == 0 || height == 0 { 55 | return nil 56 | } 57 | if e.Width > 0 { 58 | width = e.Width 59 | } 60 | if e.Height > 0 { 61 | height = e.Height 62 | } 63 | 64 | var paletted *image.Paletted 65 | 66 | // fast path for paletted images 67 | if p, ok := img.(*image.Paletted); ok && len(p.Palette) < int(nc) { 68 | paletted = p 69 | } else { 70 | // make adaptive palette using median cut alogrithm 71 | q := median.Quantizer(nc - 1) 72 | paletted = q.Paletted(img) 73 | 74 | if e.Dither { 75 | // copy source image to new image with applying floyd-stenberg dithering 76 | draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{}) 77 | } else { 78 | draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Over) 79 | } 80 | } 81 | 82 | // use on-memory output buffer for improving the performance 83 | var w io.Writer 84 | if _, ok := e.w.(*os.File); ok { 85 | w = bytes.NewBuffer(make([]byte, 0, 1024*32)) 86 | } else { 87 | w = e.w 88 | } 89 | // DECSIXEL Introducer(\033P0;0;8q) + DECGRA ("1;1): Set Raster Attributes 90 | w.Write([]byte{0x1b, 0x50, 0x30, 0x3b, 0x30, 0x3b, 0x38, 0x71, 0x22, 0x31, 0x3b, 0x31}) 91 | 92 | for n, v := range paletted.Palette { 93 | r, g, b, _ := v.RGBA() 94 | r = r * 100 / 0xFFFF 95 | g = g * 100 / 0xFFFF 96 | b = b * 100 / 0xFFFF 97 | // DECGCI (#): Graphics Color Introducer 98 | fmt.Fprintf(w, "#%d;2;%d;%d;%d", n+1, r, g, b) 99 | } 100 | 101 | buf := make([]byte, width*nc) 102 | cset := make([]bool, nc) 103 | ch0 := specialChNr 104 | for z := 0; z < (height+5)/6; z++ { 105 | // DECGNL (-): Graphics Next Line 106 | if z > 0 { 107 | w.Write([]byte{0x2d}) 108 | } 109 | for p := 0; p < 6; p++ { 110 | y := z*6 + p 111 | for x := 0; x < width; x++ { 112 | _, _, _, alpha := img.At(x, y).RGBA() 113 | if alpha != 0 { 114 | idx := paletted.ColorIndexAt(x, y) + 1 115 | cset[idx] = false // mark as used 116 | buf[width*int(idx)+x] |= 1 << uint(p) 117 | } 118 | } 119 | } 120 | for n := 1; n < nc; n++ { 121 | if cset[n] { 122 | continue 123 | } 124 | cset[n] = true 125 | // DECGCR ($): Graphics Carriage Return 126 | if ch0 == specialChCr { 127 | w.Write([]byte{0x24}) 128 | } 129 | // select color (#%d) 130 | if n >= 100 { 131 | digit1 := n / 100 132 | digit2 := (n - digit1*100) / 10 133 | digit3 := n % 10 134 | c1 := byte(0x30 + digit1) 135 | c2 := byte(0x30 + digit2) 136 | c3 := byte(0x30 + digit3) 137 | w.Write([]byte{0x23, c1, c2, c3}) 138 | } else if n >= 10 { 139 | c1 := byte(0x30 + n/10) 140 | c2 := byte(0x30 + n%10) 141 | w.Write([]byte{0x23, c1, c2}) 142 | } else { 143 | w.Write([]byte{0x23, byte(0x30 + n)}) 144 | } 145 | cnt := 0 146 | for x := 0; x < width; x++ { 147 | // make sixel character from 6 pixels 148 | ch := buf[width*n+x] 149 | buf[width*n+x] = 0 150 | if ch0 < 0x40 && ch != ch0 { 151 | // output sixel character 152 | s := 63 + ch0 153 | for ; cnt > 255; cnt -= 255 { 154 | w.Write([]byte{0x21, 0x32, 0x35, 0x35, s}) 155 | } 156 | if cnt == 1 { 157 | w.Write([]byte{s}) 158 | } else if cnt == 2 { 159 | w.Write([]byte{s, s}) 160 | } else if cnt == 3 { 161 | w.Write([]byte{s, s, s}) 162 | } else if cnt >= 100 { 163 | digit1 := cnt / 100 164 | digit2 := (cnt - digit1*100) / 10 165 | digit3 := cnt % 10 166 | c1 := byte(0x30 + digit1) 167 | c2 := byte(0x30 + digit2) 168 | c3 := byte(0x30 + digit3) 169 | // DECGRI (!): - Graphics Repeat Introducer 170 | w.Write([]byte{0x21, c1, c2, c3, s}) 171 | } else if cnt >= 10 { 172 | c1 := byte(0x30 + cnt/10) 173 | c2 := byte(0x30 + cnt%10) 174 | // DECGRI (!): - Graphics Repeat Introducer 175 | w.Write([]byte{0x21, c1, c2, s}) 176 | } else if cnt > 0 { 177 | // DECGRI (!): - Graphics Repeat Introducer 178 | w.Write([]byte{0x21, byte(0x30 + cnt), s}) 179 | } 180 | cnt = 0 181 | } 182 | ch0 = ch 183 | cnt++ 184 | } 185 | if ch0 != 0 { 186 | // output sixel character 187 | s := 63 + ch0 188 | for ; cnt > 255; cnt -= 255 { 189 | w.Write([]byte{0x21, 0x32, 0x35, 0x35, s}) 190 | } 191 | if cnt == 1 { 192 | w.Write([]byte{s}) 193 | } else if cnt == 2 { 194 | w.Write([]byte{s, s}) 195 | } else if cnt == 3 { 196 | w.Write([]byte{s, s, s}) 197 | } else if cnt >= 100 { 198 | digit1 := cnt / 100 199 | digit2 := (cnt - digit1*100) / 10 200 | digit3 := cnt % 10 201 | c1 := byte(0x30 + digit1) 202 | c2 := byte(0x30 + digit2) 203 | c3 := byte(0x30 + digit3) 204 | // DECGRI (!): - Graphics Repeat Introducer 205 | w.Write([]byte{0x21, c1, c2, c3, s}) 206 | } else if cnt >= 10 { 207 | c1 := byte(0x30 + cnt/10) 208 | c2 := byte(0x30 + cnt%10) 209 | // DECGRI (!): - Graphics Repeat Introducer 210 | w.Write([]byte{0x21, c1, c2, s}) 211 | } else if cnt > 0 { 212 | // DECGRI (!): - Graphics Repeat Introducer 213 | w.Write([]byte{0x21, byte(0x30 + cnt), s}) 214 | } 215 | } 216 | ch0 = specialChCr 217 | } 218 | } 219 | // string terminator(ST) 220 | w.Write([]byte{0x1b, 0x5c}) 221 | 222 | // copy to given buffer 223 | if _, ok := e.w.(*os.File); ok { 224 | w.(*bytes.Buffer).WriteTo(e.w) 225 | } 226 | 227 | return nil 228 | } 229 | 230 | // Decoder decode sixel format into image 231 | type Decoder struct { 232 | r io.Reader 233 | } 234 | 235 | // NewDecoder return new instance of Decoder 236 | func NewDecoder(r io.Reader) *Decoder { 237 | return &Decoder{r} 238 | } 239 | 240 | // Decode do decoding from image 241 | func (e *Decoder) Decode(img *image.Image) error { 242 | buf := bufio.NewReader(e.r) 243 | _, err := buf.ReadBytes('\x1B') 244 | if err != nil { 245 | if err == io.EOF { 246 | err = nil 247 | } 248 | return err 249 | } 250 | c, err := buf.ReadByte() 251 | if err != nil { 252 | return err 253 | } 254 | switch c { 255 | case 'P': 256 | _, err := buf.ReadString('q') 257 | if err != nil { 258 | return err 259 | } 260 | default: 261 | return errors.New("Invalid format: illegal header") 262 | } 263 | colors := map[uint]color.Color{ 264 | // 16 predefined color registers of VT340 265 | 0: sixelRGB(0, 0, 0), 266 | 1: sixelRGB(20, 20, 80), 267 | 2: sixelRGB(80, 13, 13), 268 | 3: sixelRGB(20, 80, 20), 269 | 4: sixelRGB(80, 20, 80), 270 | 5: sixelRGB(20, 80, 80), 271 | 6: sixelRGB(80, 80, 20), 272 | 7: sixelRGB(53, 53, 53), 273 | 8: sixelRGB(26, 26, 26), 274 | 9: sixelRGB(33, 33, 60), 275 | 10: sixelRGB(60, 26, 26), 276 | 11: sixelRGB(33, 60, 33), 277 | 12: sixelRGB(60, 33, 60), 278 | 13: sixelRGB(33, 60, 60), 279 | 14: sixelRGB(60, 60, 33), 280 | 15: sixelRGB(80, 80, 80), 281 | } 282 | dx, dy := 0, 0 283 | dw, dh, w, h := 0, 0, 200, 200 284 | pimg := image.NewNRGBA(image.Rect(0, 0, w, h)) 285 | var cn uint 286 | data: 287 | for { 288 | c, err = buf.ReadByte() 289 | if err != nil { 290 | if err == io.EOF { 291 | err = nil 292 | } 293 | return err 294 | } 295 | if c == '\r' || c == '\n' || c == '\b' { 296 | continue 297 | } 298 | switch { 299 | case c == '\x1b': 300 | c, err = buf.ReadByte() 301 | if err != nil { 302 | return err 303 | } 304 | if c == '\\' { 305 | break data 306 | } 307 | case c == '"': 308 | params := []int{} 309 | for { 310 | var i int 311 | n, err := fmt.Fscanf(buf, "%d", &i) 312 | if err == io.EOF { 313 | return err 314 | } 315 | if n == 0 { 316 | i = 0 317 | } 318 | params = append(params, i) 319 | c, err = buf.ReadByte() 320 | if err != nil { 321 | return err 322 | } 323 | if c != ';' { 324 | break 325 | } 326 | } 327 | if len(params) >= 4 { 328 | if w < params[2] { 329 | w = params[2] 330 | } 331 | if h < params[3]+6 { 332 | h = params[3] + 6 333 | } 334 | pimg = expandImage(pimg, w, h) 335 | } 336 | err = buf.UnreadByte() 337 | if err != nil { 338 | return err 339 | } 340 | case c == '$': 341 | dx = 0 342 | case c == '!': 343 | err = buf.UnreadByte() 344 | if err != nil { 345 | return err 346 | } 347 | var nc uint 348 | var c byte 349 | n, err := fmt.Fscanf(buf, "!%d%c", &nc, &c) 350 | if err != nil { 351 | return err 352 | } 353 | if n != 2 || c < '?' || c > '~' { 354 | return fmt.Errorf("invalid format: illegal repeating data tokens '!%d%c'", nc, c) 355 | } 356 | if w <= dx+int(nc)-1 { 357 | w *= 2 358 | pimg = expandImage(pimg, w, h) 359 | } 360 | m := byte(1) 361 | c -= '?' 362 | for p := 0; p < 6; p++ { 363 | if c&m != 0 { 364 | for q := 0; q < int(nc); q++ { 365 | pimg.Set(dx+q, dy+p, colors[cn]) 366 | } 367 | if dh < dy+p+1 { 368 | dh = dy + p + 1 369 | } 370 | } 371 | m <<= 1 372 | } 373 | dx += int(nc) 374 | if dw < dx { 375 | dw = dx 376 | } 377 | case c == '-': 378 | dx = 0 379 | dy += 6 380 | if h <= dy+6 { 381 | h *= 2 382 | pimg = expandImage(pimg, w, h) 383 | } 384 | case c == '#': 385 | err = buf.UnreadByte() 386 | if err != nil { 387 | return err 388 | } 389 | var nc, csys uint 390 | var r, g, b uint 391 | var c byte 392 | n, err := fmt.Fscanf(buf, "#%d%c", &nc, &c) 393 | if err != nil { 394 | return err 395 | } 396 | if n != 2 { 397 | return fmt.Errorf("invalid format: illegal color specifier '#%d%c'", nc, c) 398 | } 399 | if c == ';' { 400 | n, err := fmt.Fscanf(buf, "%d;%d;%d;%d", &csys, &r, &g, &b) 401 | if err != nil { 402 | return err 403 | } 404 | if n != 4 { 405 | return fmt.Errorf("invalid format: illegal color specifier '#%d;%d;%d;%d;%d'", nc, csys, r, g, b) 406 | } 407 | if csys == 1 { 408 | colors[nc] = sixelHLS(r, g, b) 409 | } else { 410 | colors[nc] = sixelRGB(r, g, b) 411 | } 412 | } else { 413 | err = buf.UnreadByte() 414 | if err != nil { 415 | return err 416 | } 417 | } 418 | cn = nc 419 | if _, ok := colors[cn]; !ok { 420 | return fmt.Errorf("invalid format: undefined color number %d", cn) 421 | } 422 | default: 423 | if c >= '?' && c <= '~' { 424 | if w <= dx { 425 | w *= 2 426 | pimg = expandImage(pimg, w, h) 427 | } 428 | m := byte(1) 429 | c -= '?' 430 | for p := 0; p < 6; p++ { 431 | if c&m != 0 { 432 | pimg.Set(dx, dy+p, colors[cn]) 433 | if dh < dy+p+1 { 434 | dh = dy + p + 1 435 | } 436 | } 437 | m <<= 1 438 | } 439 | dx++ 440 | if dw < dx { 441 | dw = dx 442 | } 443 | break 444 | } 445 | return errors.New("invalid format: illegal data tokens") 446 | } 447 | } 448 | rect := image.Rect(0, 0, dw, dh) 449 | tmp := image.NewNRGBA(rect) 450 | draw.Draw(tmp, rect, pimg, image.Point{0, 0}, draw.Src) 451 | *img = tmp 452 | return nil 453 | } 454 | 455 | func sixelRGB(r, g, b uint) color.Color { 456 | return color.NRGBA{uint8(r * 0xFF / 100), uint8(g * 0xFF / 100), uint8(b * 0xFF / 100), 0xFF} 457 | } 458 | 459 | func sixelHLS(h, l, s uint) color.Color { 460 | var r, g, b, max, min float64 461 | 462 | /* https://wikimedia.org/api/rest_v1/media/math/render/svg/17e876f7e3260ea7fed73f69e19c71eb715dd09d */ 463 | /* https://wikimedia.org/api/rest_v1/media/math/render/svg/f6721b57985ad83db3d5b800dc38c9980eedde1d */ 464 | if l > 50 { 465 | max = float64(l) + float64(s)*(1.0-float64(l)/100.0) 466 | min = float64(l) - float64(s)*(1.0-float64(l)/100.0) 467 | } else { 468 | max = float64(l) + float64(s*l)/100.0 469 | min = float64(l) - float64(s*l)/100.0 470 | } 471 | 472 | /* sixel hue color ring is roteted -120 degree from nowdays general one. */ 473 | h = (h + 240) % 360 474 | 475 | /* https://wikimedia.org/api/rest_v1/media/math/render/svg/937e8abdab308a22ff99de24d645ec9e70f1e384 */ 476 | switch h / 60 { 477 | case 0: /* 0 <= hue < 60 */ 478 | r = max 479 | g = min + (max-min)*(float64(h)/60.0) 480 | b = min 481 | break 482 | case 1: /* 60 <= hue < 120 */ 483 | r = min + (max-min)*(float64(120-h)/60.0) 484 | g = max 485 | b = min 486 | break 487 | case 2: /* 120 <= hue < 180 */ 488 | r = min 489 | g = max 490 | b = min + (max-min)*(float64(h-120)/60.0) 491 | break 492 | case 3: /* 180 <= hue < 240 */ 493 | r = min 494 | g = min + (max-min)*(float64(240-h)/60.0) 495 | b = max 496 | break 497 | case 4: /* 240 <= hue < 300 */ 498 | r = min + (max-min)*(float64(h-240)/60.0) 499 | g = min 500 | b = max 501 | break 502 | case 5: /* 300 <= hue < 360 */ 503 | r = max 504 | g = min 505 | b = min + (max-min)*(float64(360-h)/60.0) 506 | break 507 | default: 508 | } 509 | return sixelRGB(uint(r), uint(g), uint(b)) 510 | } 511 | 512 | func expandImage(pimg *image.NRGBA, w, h int) *image.NRGBA { 513 | b := pimg.Bounds() 514 | if w < b.Max.X { 515 | w = b.Max.X 516 | } 517 | if h < b.Max.Y { 518 | h = b.Max.Y 519 | } 520 | tmp := image.NewNRGBA(image.Rect(0, 0, w, h)) 521 | draw.Draw(tmp, b, pimg, image.Point{0, 0}, draw.Src) 522 | return tmp 523 | } 524 | --------------------------------------------------------------------------------