-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpixelview.go
157 lines (138 loc) · 4.62 KB
/
pixelview.go
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Package pixelview is a simple package which converts images to text formatted for tview.
// It uses coloured unicode half-block characters (▀) to represent pixels.
package pixelview
import (
"os"
"io"
"fmt"
"image"
"image/color"
"strings"
"github.com/pkg/errors"
)
// FromFile is a convenience function that converts a file on disk to a formatted string.
// See FromImage() for more details.
func FromFile(filename string) (encoded string, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
return FromReader(io.Reader(f))
}
// FromReader is a convenience function that converts an io.Reader to a formatted string.
// See FromImage() for more details.
func FromReader(reader io.Reader) (encoded string, err error) {
img, _, err := image.Decode(reader)
if err != nil {
return
}
return FromImage(img)
}
// FromImage is the primary function of this package,
// It takes an image.Image and converts it to a string formatted for tview.
// The unicode half-block character (▀) with a fg & bg colour set will represent
// pixels in the returned string.
// Because each character represents two pixels, it is not possible to convert an
// image if its height is uneven. Attempts to do so will return an error.
func FromImage(img image.Image) (encoded string, err error) {
if (img.Bounds().Max.Y - img.Bounds().Min.Y) % 2 != 0 {
err = errors.New("pixelview: Can't process image with uneven height")
return
}
switch v := img.(type) {
default:
return fromImageGeneric(img)
case *image.Paletted:
return fromPaletted(v)
case *image.NRGBA:
return fromNRGBA(v)
}
}
// fromImageGeneric is the fallback function for processing images.
// It will be used for more exotic image formats than png or gif.
func fromImageGeneric(img image.Image) (encoded string, err error) {
var sb strings.Builder
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y += 2 {
var prevfg, prevbg color.Color
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
fg := img.At(x, y)
bg := img.At(x, y + 1)
encode(fg, bg, &prevfg, &prevbg, &sb)
}
sb.WriteRune('\n')
}
encoded = sb.String()
return
}
// fromPaletted saves a few μs when working with paletted images.
// These are what PNG8 images are decoded as.
func fromPaletted(img *image.Paletted) (encoded string, err error) {
var sb strings.Builder
for y := img.Rect.Min.Y; y < img.Rect.Max.Y; y += 2 {
var prevfg, prevbg color.Color
for x := img.Rect.Min.X; x < img.Rect.Max.X; x++ {
i := (y - img.Rect.Min.Y) * img.Stride + (x - img.Rect.Min.X)
fg := img.Palette[img.Pix[i]]
bg := img.Palette[img.Pix[i + img.Stride]]
encode(fg, bg, &prevfg, &prevbg, &sb)
}
sb.WriteRune('\n')
}
encoded = sb.String()
return
}
// fromNRGBA saves a handful of μs when working with NRGBA images.
// These are what PNG24 images are decoded as.
func fromNRGBA(img *image.NRGBA) (encoded string, err error) {
var sb strings.Builder
for y := img.Rect.Min.Y; y < img.Rect.Max.Y; y += 2 {
var prevfg, prevbg color.Color
for x := img.Rect.Min.X; x < img.Rect.Max.X; x++ {
i := (y - img.Rect.Min.Y) * img.Stride + (x - img.Rect.Min.X) * 4
fg := color.NRGBA{img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3]}
i += img.Stride
bg := color.NRGBA{img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3]}
encode(fg, bg, &prevfg, &prevbg, &sb)
}
sb.WriteRune('\n')
}
encoded = sb.String()
return
}
// encode converts a fg & bg colour into a formatted pair of 'pixels',
// using the prevfg & prevbg colours to perform something akin to run-length encoding
func encode(fg, bg color.Color, prevfg, prevbg *color.Color, sb *strings.Builder) {
if fg == *prevfg && bg == *prevbg {
sb.WriteRune('▀')
return
}
if fg == *prevfg {
sb.WriteString(fmt.Sprintf(
"[:%s]▀",
hexColour(bg),
))
*prevbg = bg
return
}
if bg == *prevbg {
sb.WriteString(fmt.Sprintf(
"[%s:]▀",
hexColour(fg),
))
*prevfg = fg
return
}
sb.WriteString(fmt.Sprintf(
"[%s:%s]▀",
hexColour(fg),
hexColour(bg),
))
*prevfg = fg
*prevbg = bg
return
}
func hexColour(c color.Color) string {
r, g, b, _ := c.RGBA()
return fmt.Sprintf("#%.2x%.2x%.2x", r >> 8, g >> 8, b >> 8)
}