-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathxpm.go
371 lines (325 loc) · 11.1 KB
/
xpm.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
package xpm
import (
"fmt"
"image"
"image/color"
"io"
"os"
"strings"
"github.com/xyproto/palgen"
)
const (
// VersionString contains the current package name and version
VersionString = "xpm 1.3.0"
azAZ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// AllowedLetters is the 93 available ASCII letters
// ref: https://en.wikipedia.org/wiki/X_PixMap
// They are in the same order as GIMP, but with the question mark character as
// well. Double question marks may result in trigraphs in C, but this is
// avoided by checking specifically for this.
// ref: https://en.wikipedia.org/wiki/Digraphs_and_trigraphs#C
AllowedLetters = " .+@#$%&*=-;>,')!~{]^/(_:<[}|1234567890" + azAZ + "`?"
)
// Encoder contains encoding configuration that is used by the Encode method
type Encoder struct {
// The internal image name
ImageName string
// With comments?
Comments bool
// The alpha threshold
AlphaThreshold float64
// These are used when encoding the color ID as ASCII
AllowedLetters []rune
// MaxColors is the maximum allowed number of colors, or -1 for no limit. The default is 256.
MaxColors int
}
// NewEncoder creates a new Encoder configuration struct
func NewEncoder(imageName string) *Encoder {
var validIdentifier []rune
for _, letter := range imageName {
if strings.ContainsRune(azAZ+"_", letter) {
validIdentifier = append(validIdentifier, letter)
}
}
if len(validIdentifier) > 0 {
imageName = string(validIdentifier)
} else {
imageName = "img"
}
return &Encoder{imageName, true, 0.5, []rune(AllowedLetters), 256}
}
// hexify converts a slice of bytes to a slice of hex strings on the form 0x00
func hexify(data []byte) (r []string) {
for _, b := range data {
r = append(r, fmt.Sprintf("0x%02x", b))
}
return r
}
// c2hex converts from a color.Color to a XPM friendly color
// (either on the form #000000 or as a color, like "black")
// XPM only supports 100% or 0% alpha, represented as the None color
func c2hex(c color.Color, threshold float64) string {
byteColor := color.NRGBAModel.Convert(c).(color.NRGBA)
r, g, b, a := byteColor.R, byteColor.G, byteColor.B, byteColor.A
if a < uint8(256.0*threshold) {
return "None"
}
// "black" and "red" are shorter than the hex codes
if r == 0 {
if g == 0 {
if b == 0 {
return "black"
} else if b == 0xff {
return "blue"
}
} else if g == 0xff && b == 0 {
return "green"
}
} else if r == 0xff {
if g == 0xff && b == 0xff {
return "white"
} else if g == 0 && b == 0 {
return "red"
}
}
// return hex color code on the form #000000
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// inc will advance to the next string. Uses a-z. From "a" to "b", "zz" to "aaa" etc
func inc(s string, allowedLetters []rune) string {
firstLetter := allowedLetters[0]
lastLetter := allowedLetters[len(allowedLetters)-1]
if s == "" {
return string(firstLetter)
}
var lastRuneOfString rune
for i, r := range s {
if i == len(s)-1 {
lastRuneOfString = r
}
}
if len(s) == 1 {
if lastRuneOfString != lastLetter { // one digit, not the last letter
// lastRuneOfString is the only rune in this string
pos := strings.IndexRune(string(allowedLetters), lastRuneOfString)
pos++
if pos == len(allowedLetters) {
pos = 0
}
return string(allowedLetters[pos]) // return the next letter
}
// one digit, and it is the last letter
return string(firstLetter) + string(firstLetter)
}
if lastRuneOfString == lastLetter { // two or more digits, the last digit is z
return inc(s[:len(s)-1], allowedLetters) + string(firstLetter) // increase next to last digit with one + "a"
}
// two or more digits, the last digit is not z
return s[:len(s)-1] + inc(string(lastRuneOfString), allowedLetters) // first digit + last digit increases with one
}
func validColorID(s string) bool {
// avoid double question marks and comment markers
// double question marks in strings may have a special meaning in C
// also avoid strings starting or ending with *, / or ? since they may be combined when using the color IDs in the pixel data
return !(strings.Contains(s, "??") || strings.Contains(s, "/*") || strings.Contains(s, "//") || strings.Contains(s, "*/") || strings.HasPrefix(s, "*") || strings.HasSuffix(s, "*") || strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") || strings.HasPrefix(s, "?") || strings.HasSuffix(s, "?"))
}
// num2charcode converts a number to ascii letters, like 0 to "a", and 1 to "b".
// Can output multiple letters for higher numbers.
func num2charcode(num int, allowedLetters []rune) string {
// This is not the efficient way, but it's only called once per image conversion
d := string(allowedLetters[0])
for i := 0; i < num; i++ {
d = inc(d, allowedLetters)
// check if the color ID may cause problems
for !validColorID(d) {
// try the next one
d = inc(d, allowedLetters)
}
}
return d
}
// Encode will encode the given image as XBM, using a custom image name from
// the Encoder struct. The given palette will be used.
func (enc *Encoder) encodePaletted(w io.Writer, m image.PalettedImage) error {
width := m.Bounds().Dx()
height := m.Bounds().Dy()
pal := m.ColorModel().(color.Palette)
// Store all colors in a map, from hexstring to color, unordered
paletteMap := make(map[string]color.Color)
for _, c := range pal {
paletteMap[c2hex(c, enc.AlphaThreshold)] = c
}
var paletteSlice []string // hexstrings, ordered
// First append the "None" color, for transparency, so that it is first
for hexColor := range paletteMap {
if hexColor == "None" {
paletteSlice = append(paletteSlice, hexColor)
// Then remove None from the paletteMap and break out
delete(paletteMap, "None")
break
}
}
// Then append the rest of the colors
for hexColor := range paletteMap {
paletteSlice = append(paletteSlice, hexColor)
}
// Find the character code of the highest index
highestCharCode := num2charcode(len(paletteSlice)-1, enc.AllowedLetters)
charsPerPixel := len(highestCharCode)
colors := len(paletteSlice)
// Imlib does not like this
if colors > 32766 {
fmt.Fprintf(os.Stderr, "WARNING: Too many colors for some XPM interpreters: %d\n", colors)
}
// Write the header, now that we know the right values
fmt.Fprint(w, "/* XPM */\n")
fmt.Fprintf(w, "static char *%s[] = {\n", enc.ImageName)
if enc.Comments {
fmt.Fprint(w, "/* Values */\n")
}
fmt.Fprintf(w, "\"%d %d %d %d\",\n", width, height, colors, charsPerPixel)
// Write the colors of paletteSlice, and generate a lookup table from hexColor to charcode
if enc.Comments {
fmt.Fprint(w, "/* Colors */\n")
}
lookup := make(map[string]string) // hexcolor -> paletteindexchars, unordered
charcode := strings.Repeat(string(enc.AllowedLetters[0]), charsPerPixel)
for _, hexColor := range paletteSlice {
trimmed := strings.TrimSpace(charcode)
if len(trimmed) < len(charcode) {
diffLength := len(charcode) - len(trimmed)
charcode = strings.TrimSpace(charcode) + strings.Repeat(" ", diffLength)
}
fmt.Fprintf(w, "\"%s c %s\",\n", charcode, hexColor)
lookup[hexColor] = charcode
charcode = inc(charcode, enc.AllowedLetters)
// check if the color ID may cause problems
for !validColorID(charcode) {
// try the next one
charcode = inc(charcode, enc.AllowedLetters)
}
}
// Now write the pixels, as character codes
if enc.Comments {
fmt.Fprint(w, "/* Pixels */\n")
}
lastY := m.Bounds().Max.Y - 1
for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ {
fmt.Fprintf(w, "\"")
for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ {
c := m.At(x, y)
charcode := lookup[c2hex(c, enc.AlphaThreshold)]
// Now write the id code for the hex color to the file
fmt.Fprint(w, charcode)
}
if y < lastY {
fmt.Fprintf(w, "\",\n")
} else {
// Don't output a final comma
fmt.Fprintf(w, "\"\n")
}
}
fmt.Fprintf(w, "};\n")
return nil
}
// Encode will encode the given image as XBM, using a custom image name from
// the Encoder struct.
func (enc *Encoder) Encode(w io.Writer, m image.Image) error {
if pi, ok := m.(image.PalettedImage); ok {
return enc.encodePaletted(w, pi)
}
width := m.Bounds().Dx()
height := m.Bounds().Dy()
paletteMap := make(map[string]color.Color) // hexstring -> color, unordered
for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ {
for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ {
c := m.At(x, y)
// TODO: Create both paletteMap and lookupMap here
paletteMap[c2hex(c, enc.AlphaThreshold)] = c
}
}
var paletteSlice []string // hexstrings, ordered
// First append the "None" color, for transparency, so that it is first
for hexColor := range paletteMap {
if hexColor == "None" {
paletteSlice = append(paletteSlice, hexColor)
// Then remove None from the paletteMap and break out
delete(paletteMap, "None")
break
}
}
// Then append the rest of the colors
for hexColor := range paletteMap {
paletteSlice = append(paletteSlice, hexColor)
}
// Find the character code of the highest index
highestCharCode := num2charcode(len(paletteSlice)-1, enc.AllowedLetters)
charsPerPixel := len(highestCharCode)
colors := len(paletteSlice)
// Imlib does not like this
//if colors > 32766 {
// fmt.Fprintf(os.Stderr, "WARNING: Too many colors for some XPM interpreters %d\n", colors)
//}
if colors > enc.MaxColors {
// Too many colors, reducing to a maximum of 256 colors
palettedImage, err := palgen.Convert(m)
if err != nil {
return err
}
return enc.Encode(w, palettedImage)
}
// Write the header, now that we know the right values
fmt.Fprint(w, "/* XPM */\n")
fmt.Fprintf(w, "static char *%s[] = {\n", enc.ImageName)
if enc.Comments {
fmt.Fprint(w, "/* Values */\n")
}
fmt.Fprintf(w, "\"%d %d %d %d\",\n", width, height, colors, charsPerPixel)
// Write the colors of paletteSlice, and generate a lookup table from hexColor to charcode
if enc.Comments {
fmt.Fprint(w, "/* Colors */\n")
}
lookup := make(map[string]string) // hexcolor -> paletteindexchars, unordered
charcode := strings.Repeat(string(enc.AllowedLetters[0]), charsPerPixel)
for _, hexColor := range paletteSlice {
trimmed := strings.TrimSpace(charcode)
if len(trimmed) < len(charcode) {
diffLength := len(charcode) - len(trimmed)
charcode = strings.TrimSpace(charcode) + strings.Repeat(" ", diffLength)
}
fmt.Fprintf(w, "\"%s c %s\",\n", charcode, hexColor)
lookup[hexColor] = charcode
charcode = inc(charcode, enc.AllowedLetters)
// check if the color ID may cause problems
for !validColorID(charcode) {
// try the next one
charcode = inc(charcode, enc.AllowedLetters)
}
}
// Now write the pixels, as character codes
if enc.Comments {
fmt.Fprint(w, "/* Pixels */\n")
}
lastY := m.Bounds().Max.Y - 1
for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ {
fmt.Fprintf(w, "\"")
for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ {
c := m.At(x, y)
charcode := lookup[c2hex(c, enc.AlphaThreshold)]
// Now write the id code for the hex color to the file
fmt.Fprint(w, charcode)
}
if y < lastY {
fmt.Fprintf(w, "\",\n")
} else {
// Don't output a final comma
fmt.Fprintf(w, "\"\n")
}
}
fmt.Fprintf(w, "};\n")
return nil
}
// Encode will encode the image as XBM, using "img" as the image name
func Encode(w io.Writer, m image.Image) error {
return NewEncoder("img").Encode(w, m)
}