aboutsummaryrefslogtreecommitdiffstats
path: root/icat.go
blob: 56ab95abbc977a89418ac015f95d7786fe43505f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"os"

	_ "golang.org/x/image/bmp"
	"golang.org/x/image/draw"
	_ "golang.org/x/image/tiff"
	_ "golang.org/x/image/webp"
	"golang.org/x/term"
)

func load(filename string) (image.Image, error) {
	file := os.Stdin
	if filename != "-" {
		var err error
		file, err = os.Open(filename)
		if err != nil {
			return nil, err
		}
		defer file.Close()
	}

	img, _, err := image.Decode(file)
	if err != nil {
		return nil, fmt.Errorf("decode %q: %s", filename, err)
	}

	return img, nil
}

func printImg(img image.Image, cols, lines int) error {
	// Try not to stretch the image.
	if img.Bounds().Dy()*cols <= img.Bounds().Dx()*lines*5/2 {
		lines = img.Bounds().Dy() * cols / (img.Bounds().Dx() * 5 / 2)
	} else {
		cols = img.Bounds().Dx() * lines * 5 / 2 / img.Bounds().Dy()
	}

	dst := image.NewRGBA(image.Rect(0, 0, cols, 2*lines))
	draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)

	w := bufio.NewWriter(os.Stdout)
	defer w.Flush()
	for y := 0; y < 2*lines; y += 2 {
		for x := 0; x < cols; x++ {
			hiR, hiG, hiB, _ := dst.At(x, y).RGBA()
			loR, loG, loB, _ := dst.At(x, y+1).RGBA()
			fmt.Fprintf(w, "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dmâ–€",
				hiR>>8, hiG>>8, hiB>>8,
				loR>>8, loG>>8, loB>>8)
		}
		fmt.Fprintln(w, "\033[49m")
	}
	fmt.Fprint(w, "\033[39m")
	return nil
}

func icat(args []string) error {
	if len(args) == 0 {
		return errors.New("missing positional argument")
	}
	if len(args) > 1 {
		return errors.New("too many positional arguments")
	}
	file := args[0]

	cols, lines, err := term.GetSize(1)
	if err != nil {
		return fmt.Errorf("terminal size: %s", err)
	}
	lines-- // Leave a line for the status bar.

	img, err := load(file)
	if err != nil {
		return err
	}

	if err := printImg(img, cols, lines); err != nil {
		return err
	}
	return nil
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: icat FILE\n")
	}
	flag.Parse()

	if err := icat(flag.Args()); err != nil {
		fmt.Fprintf(os.Stderr, "Failed: %s\n", err)
		os.Exit(1)
	}
}