├── .gitignore ├── data ├── serio.ttf ├── wsc.jpg ├── wsc.png └── open.sans.700.ttf ├── demo ├── screenshot.01.png ├── screenshot.02.png └── screenshot.03.png ├── glide.yaml ├── README.md ├── Makefile └── src ├── imgfetch └── imgfetch.go └── ansimage └── ansimage.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | pkg/* 3 | vendor/* 4 | src/vendor 5 | -------------------------------------------------------------------------------- /data/serio.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/data/serio.ttf -------------------------------------------------------------------------------- /data/wsc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/data/wsc.jpg -------------------------------------------------------------------------------- /data/wsc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/data/wsc.png -------------------------------------------------------------------------------- /data/open.sans.700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/data/open.sans.700.ttf -------------------------------------------------------------------------------- /demo/screenshot.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/demo/screenshot.01.png -------------------------------------------------------------------------------- /demo/screenshot.02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/demo/screenshot.02.png -------------------------------------------------------------------------------- /demo/screenshot.03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haoel/imgfetch/HEAD/demo/screenshot.03.png -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: . 2 | import: 3 | - package: github.com/disintegration/imaging 4 | version: ^1.5.0 5 | - package: github.com/golang/freetype 6 | subpackages: 7 | - truetype 8 | - package: github.com/lucasb-eyer/go-colorful 9 | version: ^1.0.0 10 | - package: golang.org/x/crypto 11 | subpackages: 12 | - ssh/terminal 13 | - package: golang.org/x/image 14 | subpackages: 15 | - bmp 16 | - font 17 | - tiff 18 | - webp 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Fetch 2 | 3 | Image Fetch is a personal programming excise which tries to copy the idea from **[neofetch](https://github.com/dylanaraps/neofetch)**. 4 | 5 | The major difference are: 6 | 7 | - The Image Fetch try to transform an image file to text file with true color. 8 | - The Image Fetch only print very simple system information - `hostname` `username` and `OS type` 9 | 10 | It borrow the code from **[PIXterm](https://github.com/eliukblau/pixterm)**, which is quite geek. I learn a lot from it. 11 | 12 | To use this you need to compile it by yourself, it's quite easy 13 | 14 | ``` 15 | make vget && make 16 | ``` 17 | 18 | After compile it successfully, you can simply run the following command: 19 | 20 | ``` 21 | build/bin/imgfetch build/data/wsc.png 22 | ``` 23 | 24 | The output is something like below: (it uses the famous MEME in China recently, **I hope it can bring your server or laptop much more performance and reliability** ) 25 | 26 | ![](./demo/screenshot.01.png) 27 | 28 | 29 | another style `build/bin/imgfetch -d 1 build/data/wsc.png` 30 | 31 | ![](./demo/screenshot.02.png) 32 | 33 | 34 | another style `build/bin/imgfetch -d 2 build/data/wsc.png` 35 | 36 | ![](./demo/screenshot.03.png) 37 | 38 | 39 | if you like it. free feel raise the pull request to make more fun... 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default build clean depend vget vclean glide 2 | 3 | MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 4 | MKFILE_DIR := $(dir $(MKFILE_PATH)) 5 | 6 | GOPATH := ${MKFILE_DIR} 7 | export GOPATH 8 | GOBUILD := ${GOPATH}build/ 9 | GOBIN := ${GOBUILD}bin 10 | export GOBIN 11 | 12 | GLIDE_CONF=${MKFILE_DIR}glide.yaml 13 | GLIDE=${GOBIN}/glide 14 | GLIDE_SYS := $(shell command -v glide) 15 | TARGET=${MKFILE_DIR}build/bin/imgfetch 16 | SOURCE=${MKFILE_DIR}src/imgfetch/imgfetch.go 17 | ALL_SOURCE=$(shell find ${MKFILE_DIR}src -type f -name "*.go") 18 | 19 | default: ${TARGET} 20 | 21 | 22 | ${TARGET}: ${ALL_SOURCE} 23 | @echo "-------------- building ---------------" 24 | mkdir -p ${MKFILE_DIR}build/bin/ 25 | cd ${MKFILE_DIR} && go build -i -v -ldflags "-s -w" -o ${TARGET} ${SOURCE} 26 | mkdir -p ${GOBUILD}data/ && cp ${MKFILE_DIR}data/* ${GOBUILD}data/ 27 | 28 | build: default 29 | 30 | clean: 31 | @rm -rf ${TARGET} && rm -rf ${MKFILE_DIR}pkg && rm -rf ${GOBUILD}data/ 32 | 33 | ${GLIDE}: 34 | if [ -f ${GLIDE_SYS} ]; then \ 35 | mkdir -p ${GOBIN}; \ 36 | ln -s ${GLIDE_SYS} ${GLIDE}; \ 37 | fi 38 | if [ ! -f ${GLIDE} ]; then \ 39 | GOPATH=${GOBUILD}; \ 40 | go get -v github.com/Masterminds/glide; \ 41 | rm -rf ${GOBUILD}pkg ${GOBUILD}src; \ 42 | fi 43 | 44 | 45 | vget: ${GLIDE} 46 | if [ ! -f ${GLIDE_CONF} ]; then ${GLIDE} init; fi 47 | ${GLIDE} install 48 | ln -s ${MKFILE_DIR}vendor ${MKFILE_DIR}src/vendor 49 | 50 | vupd: ${GLIDE} 51 | rm -rf $(HOME)/.glide/cache 52 | ${GLIDE} update && ${GLIDE} install 53 | #rm -rf ${MKFILE_DIR}src/vendor ${GLIDE} ${MKFILE_DIR}build/pkg ${MKFILE_DIR}build/src 54 | ln -s ${MKFILE_DIR}vendor ${MKFILE_DIR}src/vendor 55 | 56 | vclean: 57 | rm -f ${MKFILE_DIR}glide.lock 58 | rm -rf ${MKFILE_DIR}src/vendor ${GLIDE} ${MKFILE_DIR}build/pkg ${MKFILE_DIR}build/src 59 | rm -dRf ${MKFILE_DIR}src/vendor ${MKFILE_DIR}vendor 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/imgfetch/imgfetch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/user" 10 | "path/filepath" 11 | _ "regexp" 12 | "runtime" 13 | _ "strings" 14 | "image" 15 | "image/draw" 16 | "image/color" 17 | "image/png" 18 | 19 | "ansimage" 20 | 21 | "github.com/lucasb-eyer/go-colorful" 22 | "golang.org/x/crypto/ssh/terminal" 23 | 24 | "golang.org/x/image/font" 25 | "github.com/golang/freetype" 26 | "github.com/golang/freetype/truetype" 27 | ) 28 | 29 | 30 | var ( 31 | flagDither uint 32 | flagMatte string 33 | flagRows uint 34 | flagCols uint 35 | flagFont string 36 | ) 37 | 38 | var ( 39 | imgWidth, imgHeight int 40 | termWidth, termHeight int 41 | srcImageFile string 42 | infoImageFile string 43 | concatImageFile string 44 | workDir string 45 | dataDir string 46 | ) 47 | 48 | var dpi = flag.Float64("dpi", 256, "screen resolution") 49 | 50 | 51 | func init() { 52 | configureFlags() 53 | } 54 | 55 | func main() { 56 | validateFlags() 57 | srcImageFile = flag.CommandLine.Arg(0) 58 | termWidth, termHeight = GetTermSize() 59 | imgWidth, imgHeight = GetImageSize(srcImageFile) 60 | workDir = getBinaryDir() 61 | dataDir = workDir+"/../data/" 62 | infoImageFile = dataDir+"info.image.png" 63 | concatImageFile = dataDir+"fetch.image.png" 64 | 65 | createInfoImage() 66 | concatImage(srcImageFile, infoImageFile) 67 | RenderTerm(concatImageFile) 68 | } 69 | 70 | func throwError(code int, v ...interface{}) { 71 | log.New(os.Stderr, "Error: ", log.LstdFlags).Println(v...) 72 | os.Exit(code) 73 | } 74 | 75 | func configureFlags() { 76 | flag.CommandLine.Usage = func() { 77 | 78 | _, file := filepath.Split(os.Args[0]) 79 | fmt.Print("USAGE:\n\n") 80 | fmt.Printf(" %s [options] image/url\n\n", file) 81 | 82 | fmt.Print(" Supported image formats: JPEG, PNG, GIF, BMP, TIFF, WebP.\n\n") 83 | //fmt.Print(" Supported URL protocols: HTTP, HTTPS.\n\n") 84 | 85 | fmt.Print("OPTIONS:\n\n") 86 | flag.CommandLine.SetOutput(os.Stdout) 87 | flag.CommandLine.PrintDefaults() 88 | flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors 89 | fmt.Print(" -help\n\tprints this message :D LOL\n") 90 | fmt.Println() 91 | } 92 | 93 | flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors 94 | flag.CommandLine.Init(os.Args[0], flag.ExitOnError) 95 | 96 | flag.CommandLine.UintVar(&flagDither, "d", 0, "dithering `mode`:\n \t0 - no dithering (default)\n \t1 - with blocks\n \t2 - with chars") 97 | flag.CommandLine.StringVar(&flagMatte, "m", "", "matte `color` for transparency or background\n\t(optional, hex format, default: 000000)") 98 | flag.CommandLine.UintVar(&flagRows, "tr", 24, "terminal `rows` (optional, >=2)") 99 | flag.CommandLine.UintVar(&flagCols, "tc", 80, "terminal `columns` (optional, >=2)") 100 | flag.CommandLine.StringVar(&flagFont, "f", "open.sans.700.ttf", "the `font family` file path (optional, default: open.sans.700.ttf)") 101 | 102 | flag.CommandLine.Parse(os.Args[1:]) 103 | } 104 | 105 | func validateFlags() { 106 | 107 | 108 | if flagDither != 0 && flagDither != 1 && flagDither != 2 { 109 | flag.CommandLine.Usage() 110 | os.Exit(2) 111 | } 112 | 113 | 114 | if (flagRows > 0 && flagRows < 2) || (flagCols > 0 && flagCols < 2) { 115 | flag.CommandLine.Usage() 116 | os.Exit(2) 117 | } 118 | 119 | // this is image filename 120 | if flag.CommandLine.Arg(0) == "" { 121 | flag.CommandLine.Usage() 122 | os.Exit(2) 123 | } 124 | } 125 | 126 | func getBinaryDir() string { 127 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | return dir 132 | } 133 | 134 | func isTerminal() bool { 135 | return terminal.IsTerminal(int(os.Stdout.Fd())) 136 | } 137 | 138 | func getTerminalSize() (width, height int, err error) { 139 | if isTerminal() { 140 | return terminal.GetSize(int(os.Stdout.Fd())) 141 | } 142 | // fallback when piping to a file! 143 | return 80, 24, nil // VT100 terminal size 144 | } 145 | 146 | func concatImage(leftFile, rightFile string) { 147 | 148 | // Load two images files 149 | left_fp, err := os.Open(leftFile) 150 | right_fp, err := os.Open(rightFile) 151 | if err != nil { 152 | throwError(255, err) 153 | } 154 | left_img, _, err := image.Decode(left_fp) 155 | right_img, _, err := image.Decode(right_fp) 156 | if err != nil { 157 | throwError(255, err) 158 | } 159 | 160 | 161 | ////starting position of the second image (bottom left) 162 | right_sp := image.Point{left_img.Bounds().Dx(), 0} 163 | 164 | //new rectangle for the second image 165 | right_rect := image.Rectangle{right_sp, right_sp.Add(right_img.Bounds().Size())} 166 | 167 | //rectangle for the big image 168 | rect := image.Rectangle{image.Point{0, 0}, right_rect.Max} 169 | 170 | //create the new Image file 171 | rgba := image.NewRGBA(rect) 172 | 173 | draw.Draw(rgba, left_img.Bounds(), left_img, image.Point{0, 0}, draw.Src) 174 | draw.Draw(rgba, right_rect, right_img, image.Point{0, 0}, draw.Src) 175 | 176 | // Encode as PNG. 177 | f, _ := os.Create(concatImageFile) 178 | png.Encode(f, rgba) 179 | f.Close() 180 | } 181 | 182 | func addLabel(img *image.RGBA, x, y int, size float64, c color.RGBA, label string) { 183 | /* col := color.RGBA{200, 100, 0, 255} 184 | point := fixed.Point26_6{fixed.Int26_6(x * 64), fixed.Int26_6(y * 64)} 185 | 186 | d := &font.Drawer{ 187 | Dst: img, 188 | Src: image.NewUniform(col), 189 | Face: basicfont.Face7x13, 190 | Dot: point, 191 | } 192 | d.DrawString(label)*/ 193 | 194 | ctx := freetype.NewContext() 195 | 196 | ctx.SetDPI(*dpi) 197 | ctx.SetClip(img.Bounds()) 198 | ctx.SetDst(img) 199 | ctx.SetHinting(font.HintingFull) 200 | 201 | // set font color, size and family 202 | ctx.SetSrc(image.NewUniform(c)) 203 | //c.SetSrc(image.White) 204 | ctx.SetFontSize(size) 205 | fontFam, err := getFontFamily() 206 | if err != nil { 207 | fmt.Println("get font family error") 208 | } 209 | ctx.SetFont(fontFam) 210 | 211 | pt := freetype.Pt(x, y) 212 | 213 | _, err = ctx.DrawString(label, pt) 214 | if err != nil { 215 | fmt.Printf("draw error: %v \n", err) 216 | } 217 | } 218 | 219 | func getFontFamily() (*truetype.Font, error) { 220 | 221 | fontBytes, err := ioutil.ReadFile(dataDir + flagFont) 222 | if err != nil { 223 | fmt.Println("read file error:", err) 224 | return &truetype.Font{}, err 225 | } 226 | 227 | f, err := freetype.ParseFont(fontBytes) 228 | if err != nil { 229 | throwError(255, err) 230 | return &truetype.Font{}, err 231 | } 232 | 233 | return f, err 234 | } 235 | 236 | func createInfoImage() { 237 | width, height := imgWidth, imgHeight 238 | 239 | 240 | upLeft := image.Point{0, 0} 241 | lowRight := image.Point{width, height} 242 | 243 | img := image.NewRGBA(image.Rectangle{upLeft, lowRight}) 244 | 245 | 246 | host, _ := os.Hostname() 247 | addLabel(img, 10, 50, 11, color.RGBA{R: 255, G: 255, B: 255, A: 255}, host); 248 | 249 | user, _ := user.Current() 250 | addLabel(img, 10, 120, 14, color.RGBA{R: 255, G: 255, B: 255, A: 255}, user.Username); 251 | 252 | addLabel(img, 10, 180, 9, color.RGBA{R: 255, G: 255, B: 255, A: 255}, runtime.GOOS +"/"+runtime.GOARCH); 253 | 254 | //addLabel(img, 10, 200, 11, color.RGBA{R: 255, G: 255, B: 255, A: 255}, ""); 255 | 256 | 257 | 258 | // Encode as PNG. 259 | f, _ := os.Create(infoImageFile) 260 | png.Encode(f, img) 261 | f.Close() 262 | } 263 | 264 | func GetTermSize() (w, h int) { 265 | // get terminal size 266 | tx, ty, err := getTerminalSize() 267 | if err != nil { 268 | throwError(1, err) 269 | } 270 | 271 | // use custom terminal size (if applies) 272 | if ty--; flagRows != 0 { // no custom rows? subtract 1 for prompt spacing 273 | ty = int(flagRows) + 1 // weird, but in this case is necessary to add 1 :O 274 | } 275 | if flagCols != 0 { 276 | tx = int(flagCols) 277 | } 278 | return tx, ty 279 | } 280 | 281 | 282 | func GetImageSize(imageFile string) (int, int) { 283 | file, err := os.Open(imageFile) 284 | defer file.Close() 285 | if err != nil { 286 | fmt.Fprintf(os.Stderr, "%v\n", err) 287 | } 288 | 289 | image, _, err := image.DecodeConfig(file) 290 | if err != nil { 291 | fmt.Fprintf(os.Stderr, "%s: %v\n", imageFile, err) 292 | } 293 | return image.Width, image.Height 294 | } 295 | 296 | func RenderTerm(file string) { 297 | var ( 298 | pix *ansimage.ANSImage 299 | err error 300 | ) 301 | 302 | tx, ty := termWidth*2, termHeight 303 | 304 | // get scale mode from flag 305 | sm := ansimage.ScaleMode(0) 306 | 307 | // get dithering mode from flag 308 | dm := ansimage.DitheringMode(flagDither) 309 | 310 | // set image scale factor for ANSIPixel grid 311 | sfy, sfx := ansimage.BlockSizeY, ansimage.BlockSizeX // 8x4 --> with dithering 312 | if ansimage.DitheringMode(flagDither) == ansimage.NoDithering { 313 | sfy, sfx = 2, 1 // 2x1 --> without dithering 314 | } 315 | 316 | // get matte color 317 | if flagMatte == "" { 318 | flagMatte = "000000" // black background 319 | } 320 | mc, err := colorful.Hex("#" + flagMatte) // RGB color from Hex format 321 | if err != nil { 322 | throwError(2, fmt.Sprintf("matte color : %s is not a hex-color", flagMatte)) 323 | } 324 | 325 | // create new ANSImage from file 326 | pix, err = ansimage.NewScaledFromFile(file, sfy*ty, sfx*tx, mc, sm, dm) 327 | 328 | /*if matched, _ := regexp.MatchString(`^https?://`, file); matched { 329 | pix, err = ansimage.NewScaledFromURL(file, sfy*ty, sfx*tx, mc, sm, dm) 330 | } else { 331 | pix, err = ansimage.NewScaledFromFile(file, sfy*ty, sfx*tx, mc, sm, dm) 332 | }*/ 333 | if err != nil { 334 | throwError(1, err) 335 | } 336 | 337 | // draw ANSImage to terminal 338 | if isTerminal() { 339 | ansimage.ClearTerminal() 340 | } 341 | pix.SetMaxProcs(runtime.NumCPU()) // maximum number of parallel goroutines! 342 | pix.Draw() 343 | if isTerminal() { 344 | fmt.Println() 345 | } 346 | } 347 | 348 | 349 | -------------------------------------------------------------------------------- /src/ansimage/ansimage.go: -------------------------------------------------------------------------------- 1 | // ___ _____ ____ 2 | // / _ \/ _/ |/_/ /____ ______ _ 3 | // / ___// /_> =2") 82 | 83 | // ErrOutOfBounds occurs when ANSI-pixel coordinates are out of ANSImage bounds. 84 | ErrOutOfBounds = errors.New("ANSImage: out of bounds") 85 | 86 | // errUnknownScaleMode occurs when scale mode is invalid. 87 | errUnknownScaleMode = errors.New("ANSImage: unknown scale mode") 88 | 89 | // errUnknownDitheringMode occurs when dithering mode is invalid. 90 | errUnknownDitheringMode = errors.New("ANSImage: unknown dithering mode") 91 | ) 92 | 93 | // ScaleMode type is used for image scale mode constants. 94 | type ScaleMode uint8 95 | 96 | // DitheringMode type is used for image scale dithering mode constants. 97 | type DitheringMode uint8 98 | 99 | // ANSIpixel represents a pixel of an ANSImage. 100 | type ANSIpixel struct { 101 | Brightness uint8 102 | R, G, B uint8 103 | upper bool 104 | source *ANSImage 105 | } 106 | 107 | // ANSImage represents an image encoded in ANSI escape codes. 108 | type ANSImage struct { 109 | h, w int 110 | maxprocs int 111 | bgR uint8 112 | bgG uint8 113 | bgB uint8 114 | dithering DitheringMode 115 | pixmap [][]*ANSIpixel 116 | } 117 | 118 | // Render returns the ANSI-compatible string form of ANSI-pixel. 119 | func (ap *ANSIpixel) Render() string { 120 | // WITHOUT DITHERING 121 | if ap.source.dithering == NoDithering { 122 | if ap.upper { 123 | return fmt.Sprintf( 124 | "\033[48;2;%d;%d;%dm", 125 | ap.R, ap.G, ap.B, 126 | ) 127 | } 128 | return fmt.Sprintf( 129 | "\033[38;2;%d;%d;%dm%s", 130 | ap.R, ap.G, ap.B, 131 | lowerHalfBlock, 132 | ) 133 | } 134 | 135 | // WITH DITHERING 136 | block := " " 137 | if ap.source.dithering == DitheringWithBlocks { 138 | switch bri := ap.Brightness; { 139 | case bri > 204: 140 | block = fullBlock 141 | case bri > 152: 142 | block = darkShadeBlock 143 | case bri > 100: 144 | block = mediumShadeBlock 145 | case bri > 48: 146 | block = lightShadeBlock 147 | } 148 | } else if ap.source.dithering == DitheringWithChars { 149 | switch bri := ap.Brightness; { 150 | case bri > 230: 151 | block = "#" 152 | case bri > 207: 153 | block = "&" 154 | case bri > 184: 155 | block = "$" 156 | case bri > 161: 157 | block = "X" 158 | case bri > 138: 159 | block = "x" 160 | case bri > 115: 161 | block = "=" 162 | case bri > 92: 163 | block = "+" 164 | case bri > 69: 165 | block = ";" 166 | case bri > 46: 167 | block = ":" 168 | case bri > 23: 169 | block = "." 170 | } 171 | } else { 172 | panic(errUnknownDitheringMode) 173 | } 174 | 175 | return fmt.Sprintf( 176 | "\033[48;2;%d;%d;%dm\033[38;2;%d;%d;%dm%s", 177 | ap.source.bgR, ap.source.bgG, ap.source.bgB, 178 | ap.R, ap.G, ap.B, 179 | block, 180 | ) 181 | } 182 | 183 | // Height gets total rows of ANSImage. 184 | func (ai *ANSImage) Height() int { 185 | return ai.h 186 | } 187 | 188 | // Width gets total columns of ANSImage. 189 | func (ai *ANSImage) Width() int { 190 | return ai.w 191 | } 192 | 193 | // DitheringMode gets the dithering mode of ANSImage. 194 | func (ai *ANSImage) DitheringMode() DitheringMode { 195 | return ai.dithering 196 | } 197 | 198 | // SetMaxProcs sets the maximum number of parallel goroutines to render the ANSImage 199 | // (user should manually sets `runtime.GOMAXPROCS(max)` before to this change takes effect). 200 | func (ai *ANSImage) SetMaxProcs(max int) { 201 | ai.maxprocs = max 202 | } 203 | 204 | // GetMaxProcs gets the maximum number of parallels goroutines to render the ANSImage. 205 | func (ai *ANSImage) GetMaxProcs() int { 206 | return ai.maxprocs 207 | } 208 | 209 | // SetAt sets ANSI-pixel color (RBG) and brightness in coordinates (y,x). 210 | func (ai *ANSImage) SetAt(y, x int, r, g, b, brightness uint8) error { 211 | if y >= 0 && y < ai.h && x >= 0 && x < ai.w { 212 | ai.pixmap[y][x].R = r 213 | ai.pixmap[y][x].G = g 214 | ai.pixmap[y][x].B = b 215 | ai.pixmap[y][x].Brightness = brightness 216 | ai.pixmap[y][x].upper = ((ai.dithering == NoDithering) && (y%2 == 0)) 217 | return nil 218 | } 219 | return ErrOutOfBounds 220 | } 221 | 222 | // GetAt gets ANSI-pixel in coordinates (y,x). 223 | func (ai *ANSImage) GetAt(y, x int) (*ANSIpixel, error) { 224 | if y >= 0 && y < ai.h && x >= 0 && x < ai.w { 225 | return &ANSIpixel{ 226 | R: ai.pixmap[y][x].R, 227 | G: ai.pixmap[y][x].G, 228 | B: ai.pixmap[y][x].B, 229 | Brightness: ai.pixmap[y][x].Brightness, 230 | upper: ai.pixmap[y][x].upper, 231 | source: ai.pixmap[y][x].source, 232 | }, 233 | nil 234 | } 235 | return nil, ErrOutOfBounds 236 | } 237 | 238 | // Render returns the ANSI-compatible string form of ANSImage. 239 | // (Nice info for ANSI True Colour - https://gist.github.com/XVilka/8346728) 240 | func (ai *ANSImage) Render() string { 241 | type renderData struct { 242 | row int 243 | render string 244 | } 245 | 246 | // WITHOUT DITHERING 247 | if ai.dithering == NoDithering { 248 | rows := make([]string, ai.h/2) 249 | for y := 0; y < ai.h; y += ai.maxprocs { 250 | ch := make(chan renderData, ai.maxprocs) 251 | for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { 252 | go func(r, y int) { 253 | var str string 254 | for x := 0; x < ai.w; x++ { 255 | str += ai.pixmap[y][x].Render() // upper pixel 256 | str += ai.pixmap[y+1][x].Render() // lower pixel 257 | } 258 | str += "\033[0m\n" // reset ansi style 259 | ch <- renderData{row: r, render: str} 260 | }(r, 2*r) 261 | // DEBUG: 262 | // fmt.Printf("y:%d | n:%d | r:%d | 2*r:%d\n", y, n, r, 2*r) 263 | // time.Sleep(time.Millisecond * 100) 264 | } 265 | for n, r := 0, y+1; (n <= ai.maxprocs) && (2*r+1 < ai.h); n, r = n+1, y+n+1 { 266 | data := <-ch 267 | rows[data.row] = data.render 268 | // DEBUG: 269 | // fmt.Printf("data.row:%d\n", data.row) 270 | // time.Sleep(time.Millisecond * 100) 271 | } 272 | } 273 | return strings.Join(rows, "") 274 | } 275 | 276 | // WITH DITHERING 277 | rows := make([]string, ai.h) 278 | for y := 0; y < ai.h; y += ai.maxprocs { 279 | ch := make(chan renderData, ai.maxprocs) 280 | for n, r := 0, y; (n <= ai.maxprocs) && (r+1 < ai.h); n, r = n+1, y+n+1 { 281 | go func(y int) { 282 | var str string 283 | for x := 0; x < ai.w; x++ { 284 | str += ai.pixmap[y][x].Render() 285 | } 286 | str += "\033[0m\n" // reset ansi style 287 | ch <- renderData{row: y, render: str} 288 | }(r) 289 | } 290 | for n, r := 0, y; (n <= ai.maxprocs) && (r+1 < ai.h); n, r = n+1, y+n+1 { 291 | data := <-ch 292 | rows[data.row] = data.render 293 | } 294 | } 295 | return strings.Join(rows, "") 296 | } 297 | 298 | // Draw writes the ANSImage to standard output (terminal). 299 | func (ai *ANSImage) Draw() { 300 | fmt.Print(ai.Render()) 301 | } 302 | 303 | // New creates a new empty ANSImage ready to draw on it. 304 | func New(h, w int, bg color.Color, dm DitheringMode) (*ANSImage, error) { 305 | if (dm == NoDithering) && (h%2 != 0) { 306 | return nil, ErrHeightNonMoT 307 | } 308 | 309 | if h < 2 || w < 2 { 310 | return nil, ErrInvalidBoundsMoT 311 | } 312 | 313 | r, g, b, _ := bg.RGBA() 314 | ansimage := &ANSImage{ 315 | h: h, w: w, 316 | maxprocs: 1, 317 | bgR: uint8(r), 318 | bgG: uint8(g), 319 | bgB: uint8(b), 320 | dithering: dm, 321 | pixmap: nil, 322 | } 323 | 324 | ansimage.pixmap = func() [][]*ANSIpixel { 325 | v := make([][]*ANSIpixel, h) 326 | for y := 0; y < h; y++ { 327 | v[y] = make([]*ANSIpixel, w) 328 | for x := 0; x < w; x++ { 329 | v[y][x] = &ANSIpixel{ 330 | R: 0, 331 | G: 0, 332 | B: 0, 333 | Brightness: 0, 334 | source: ansimage, 335 | upper: ((dm == NoDithering) && (y%2 == 0)), 336 | } 337 | } 338 | } 339 | return v 340 | }() 341 | 342 | return ansimage, nil 343 | } 344 | 345 | // NewFromReader creates a new ANSImage from an io.Reader. 346 | // Background color is used to fill when image has transparency or dithering mode is enabled. 347 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 348 | func NewFromReader(reader io.Reader, bg color.Color, dm DitheringMode) (*ANSImage, error) { 349 | image, _, err := image.Decode(reader) 350 | if err != nil { 351 | return nil, err 352 | } 353 | 354 | return createANSImage(image, bg, dm) 355 | } 356 | 357 | // NewScaledFromReader creates a new scaled ANSImage from an io.Reader. 358 | // Background color is used to fill when image has transparency or dithering mode is enabled. 359 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 360 | func NewScaledFromReader(reader io.Reader, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) { 361 | image, _, err := image.Decode(reader) 362 | if err != nil { 363 | return nil, err 364 | } 365 | 366 | switch sm { 367 | case ScaleModeResize: 368 | image = imaging.Resize(image, x, y, imaging.Lanczos) 369 | case ScaleModeFill: 370 | image = imaging.Fill(image, x, y, imaging.Center, imaging.Lanczos) 371 | case ScaleModeFit: 372 | image = imaging.Fit(image, x, y, imaging.Lanczos) 373 | default: 374 | panic(errUnknownScaleMode) 375 | } 376 | 377 | return createANSImage(image, bg, dm) 378 | } 379 | 380 | // NewFromFile creates a new ANSImage from a file. 381 | // Background color is used to fill when image has transparency or dithering mode is enabled. 382 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 383 | func NewFromFile(name string, bg color.Color, dm DitheringMode) (*ANSImage, error) { 384 | reader, err := os.Open(name) 385 | if err != nil { 386 | return nil, err 387 | } 388 | defer reader.Close() 389 | return NewFromReader(reader, bg, dm) 390 | } 391 | 392 | // NewFromURL creates a new ANSImage from an image URL. 393 | // Background color is used to fill when image has transparency or dithering mode is enabled. 394 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 395 | func NewFromURL(url string, bg color.Color, dm DitheringMode) (*ANSImage, error) { 396 | res, err := http.Get(url) 397 | if err != nil { 398 | return nil, err 399 | } 400 | if res.StatusCode != http.StatusOK { 401 | return nil, ErrImageDownloadFailed 402 | } 403 | defer res.Body.Close() 404 | return NewFromReader(res.Body, bg, dm) 405 | } 406 | 407 | // NewScaledFromFile creates a new scaled ANSImage from a file. 408 | // Background color is used to fill when image has transparency or dithering mode is enabled. 409 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 410 | func NewScaledFromFile(name string, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) { 411 | reader, err := os.Open(name) 412 | if err != nil { 413 | return nil, err 414 | } 415 | defer reader.Close() 416 | return NewScaledFromReader(reader, y, x, bg, sm, dm) 417 | } 418 | 419 | // NewScaledFromURL creates a new scaled ANSImage from an image URL. 420 | // Background color is used to fill when image has transparency or dithering mode is enabled. 421 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 422 | func NewScaledFromURL(url string, y, x int, bg color.Color, sm ScaleMode, dm DitheringMode) (*ANSImage, error) { 423 | res, err := http.Get(url) 424 | if err != nil { 425 | return nil, err 426 | } 427 | if res.StatusCode != http.StatusOK { 428 | return nil, ErrImageDownloadFailed 429 | } 430 | defer res.Body.Close() 431 | return NewScaledFromReader(res.Body, y, x, bg, sm, dm) 432 | } 433 | 434 | // ClearTerminal clears current terminal buffer using ANSI escape code. 435 | // (Nice info for ANSI escape codes - http://unix.stackexchange.com/questions/124762/how-does-clear-command-work) 436 | func ClearTerminal() { 437 | fmt.Print("\033[H\033[2J") 438 | } 439 | 440 | // createANSImage loads data from an image and returns an ANSImage. 441 | // Background color is used to fill when image has transparency or dithering mode is enabled. 442 | // Dithering mode is used to specify the way that ANSImage render ANSI-pixels (char/block elements). 443 | func createANSImage(img image.Image, bg color.Color, dm DitheringMode) (*ANSImage, error) { 444 | var rgbaOut *image.RGBA 445 | bounds := img.Bounds() 446 | 447 | // do compositing only if background color has no transparency (thank you @disq for the idea!) 448 | // (info - http://stackoverflow.com/questions/36595687/transparent-pixel-color-go-lang-image) 449 | if _, _, _, a := bg.RGBA(); a >= 0xffff { 450 | rgbaOut = image.NewRGBA(bounds) 451 | draw.Draw(rgbaOut, bounds, image.NewUniform(bg), image.ZP, draw.Src) 452 | draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Over) 453 | } else { 454 | if v, ok := img.(*image.RGBA); ok { 455 | rgbaOut = v 456 | } else { 457 | rgbaOut = image.NewRGBA(bounds) 458 | draw.Draw(rgbaOut, bounds, img, image.ZP, draw.Src) 459 | } 460 | } 461 | 462 | yMin, xMin := bounds.Min.Y, bounds.Min.X 463 | yMax, xMax := bounds.Max.Y, bounds.Max.X 464 | 465 | if dm == NoDithering { 466 | // always sets an even number of ANSIPixel rows... 467 | yMax = yMax - yMax%2 // one for upper pixel and another for lower pixel --> without dithering 468 | } else { 469 | yMax = yMax / BlockSizeY // always sets 1 ANSIPixel block... 470 | xMax = xMax / BlockSizeX // per 8x4 real pixels --> with dithering 471 | } 472 | 473 | ansimage, err := New(yMax, xMax, bg, dm) 474 | if err != nil { 475 | return nil, err 476 | } 477 | 478 | if dm == NoDithering { 479 | for y := yMin; y < yMax; y++ { 480 | for x := xMin; x < xMax; x++ { 481 | v := rgbaOut.RGBAAt(x, y) 482 | if err := ansimage.SetAt(y, x, v.R, v.G, v.B, 0); err != nil { 483 | return nil, err 484 | } 485 | } 486 | } 487 | } else { 488 | pixelCount := BlockSizeY * BlockSizeX 489 | 490 | for y := yMin; y < yMax; y++ { 491 | for x := xMin; x < xMax; x++ { 492 | 493 | var sumR, sumG, sumB, sumBri float64 494 | for dy := 0; dy < BlockSizeY; dy++ { 495 | py := BlockSizeY*y + dy 496 | 497 | for dx := 0; dx < BlockSizeX; dx++ { 498 | px := BlockSizeX*x + dx 499 | 500 | pixel := rgbaOut.At(px, py) 501 | _color, _ := colorful.MakeColor(pixel) 502 | _, _, v := _color.Hsv() 503 | color, _ := colorful.MakeColor(pixel) 504 | sumR += color.R 505 | sumG += color.G 506 | sumB += color.B 507 | sumBri += v 508 | } 509 | } 510 | 511 | r := uint8(sumR/float64(pixelCount)*255.0 + 0.5) 512 | g := uint8(sumG/float64(pixelCount)*255.0 + 0.5) 513 | b := uint8(sumB/float64(pixelCount)*255.0 + 0.5) 514 | brightness := uint8(sumBri/float64(pixelCount)*255.0 + 0.5) 515 | 516 | if err := ansimage.SetAt(y, x, r, g, b, brightness); err != nil { 517 | return nil, err 518 | } 519 | } 520 | } 521 | } 522 | 523 | return ansimage, nil 524 | } 525 | --------------------------------------------------------------------------------