-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathasciize.go
189 lines (159 loc) · 4.35 KB
/
asciize.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Package asciize provides methods and types to transform images to ASCII art.
package asciize
import (
"fmt"
"image"
"image/color"
"strings"
"errors"
"github.com/andybons/gogif"
"github.com/aybabtme/rgbterm"
"github.com/nfnt/resize"
)
// OutputFormat represents a supported output format. i.e.: text, html
type OutputFormat string
const (
// FormatText represents plain text output format
FormatText OutputFormat = "text"
// FormatHTML represents HTML representation of the ASCII art
FormatHTML OutputFormat = "html"
defaultWidth = 100
defaultCharset = " .~:+=o*x^%#@$MW"
)
// Asciizer allows to transform any image into its
// ASCII art representation with a set of configurable options.
type Asciizer struct {
width uint
outFmt OutputFormat
colored bool
charset []byte
invertCharset bool
}
// NewAsciizer initialize a new asciizer with the given options.
func NewAsciizer(opts ...Option) *Asciizer {
a := &Asciizer{
width: defaultWidth,
outFmt: FormatText,
charset: []byte(defaultCharset),
}
for _, opt := range opts {
opt(a)
}
return a
}
// Asciize receives an image and transforms it to an ASCII art
// representation.
func (a *Asciizer) Asciize(m image.Image) (ascii string, err error) {
if m == nil {
return "", errors.New("no image provided")
}
mSize := m.Bounds().Size()
w := a.width
if w == 0 {
w = uint(mSize.X)
}
scale := float64(w) / float64(mSize.X)
h := uint(float64(mSize.Y) * scale / 2)
rsM := resize.Resize(w, h, m, resize.NearestNeighbor)
grayM := imageToGray(rsM, a.charset)
for y := 0; y <= grayM.Bounds().Max.Y; y++ {
for x := 0; x <= grayM.Bounds().Max.X; x++ {
r, _, _, _ := grayM.At(x, y).RGBA()
// TODO: find a better way of weighting the index
i := (int(r) / len(a.charset)) % len(a.charset)
if a.invertCharset {
i = len(a.charset) - 1 - i
}
character := fmt.Sprintf("%c", a.charset[i])
if a.colored {
r, g, b, _ := rsM.At(x, y).RGBA()
character = decorateCharacter(character, a.outFmt, uint8(r), uint8(g), uint8(b), a.colored)
}
ascii += character
}
switch a.outFmt {
case FormatText:
ascii += "\n"
case FormatHTML:
ascii += "<br \\>"
}
}
return
}
// Option is a type defining functions that can change
// Asciizer behavior.
type Option func(*Asciizer)
// Format indicates what the format to output will be, can be
// "text" or "html".
func Format(f OutputFormat) Option {
return func(a *Asciizer) {
a.outFmt = f
}
}
// Width sets the output target width.
func Width(w uint) Option {
return func(a *Asciizer) {
a.width = w
}
}
// Charset defines what charset is being used to transform images.
func Charset(charset []byte) Option {
return func(a *Asciizer) {
a.charset = charset
}
}
// Colored indicates the output will be colored in the
// specified format (ANSI for text or HTML).
func Colored(c bool) Option {
return func(a *Asciizer) {
a.colored = c
}
}
// InvertCharset allows to enable or disable inverted charset.
// This can make the result clearer in some images.
func InvertCharset(i bool) Option {
return func(a *Asciizer) {
a.invertCharset = i
}
}
// DefaultCharset returns the default charset " .~:+=o*x^%#@$MW".
func DefaultCharset() []byte {
return []byte(defaultCharset)
}
// TODO: get rid of external dependencies to quantize and color
func imageToGray(m image.Image, cs []byte) image.Image {
if _, ok := m.(*image.Gray); ok {
return m // early return if image already in gray scale
}
// TODO: implement custom quantizer
pm := image.NewPaletted(m.Bounds(), nil)
quantizer := gogif.MedianCutQuantizer{NumColor: len(cs)}
quantizer.Quantize(pm, pm.Bounds(), m, image.ZP)
grayM := image.NewGray(m.Bounds())
for y := 0; y < m.Bounds().Max.Y; y++ {
for x := 0; x < pm.Bounds().Max.X; x++ {
oldPixel := pm.At(x, y)
pixel := color.GrayModel.Convert(oldPixel)
grayM.Set(x, y, pixel)
}
}
return grayM
}
func decorateCharacter(c string, f OutputFormat, r, g, b uint8, colored bool) string {
// TODO: implement custom ANSI coloring
switch f {
case FormatText:
if colored {
return rgbterm.FgString(c, r, g, b)
}
return c
case FormatHTML:
c = strings.Replace(c, " ", " ", -1)
color := ""
if colored {
color = fmt.Sprintf("color: #%2x%2x%2x;", r, g, b)
}
return fmt.Sprintf("<span style=\"font-family: 'Lucida Console', Monaco, monospace; %s\">%s</span>", color, c)
}
return c
}