genicons.go raw
1 // Copyright 2016 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package main
6
7 import (
8 "bytes"
9 "encoding/xml"
10 "flag"
11 "fmt"
12 "go/format"
13 "image/color"
14 "io"
15 "io/ioutil"
16 "log"
17 "os"
18 "path/filepath"
19 "sort"
20 "strconv"
21 "strings"
22
23 "golang.org/x/exp/shiny/iconvg"
24 "golang.org/x/image/math/f32"
25 )
26
27 var outDir = flag.String("o", "", "output directory")
28 var pkgName = flag.String("pkg", "icons", "package name")
29
30 var (
31 out = new(bytes.Buffer)
32 failures = []string{}
33 varNames = []string{}
34
35 totalFiles int
36 totalIVGBytes int
37 totalSVGBytes int
38 )
39
40 func upperCase(s string) string {
41 if c := s[0]; 'a' <= c && c <= 'z' {
42 return string(c-0x20) + s[1:]
43 }
44 return s
45 }
46
47 func main() {
48 flag.Parse()
49 args := flag.Args()
50 if len(args) < 1 {
51 _, _ = fmt.Fprintf(os.Stderr, "please provide a directory to convert\n")
52 os.Exit(2)
53 }
54 iconsDir := args[0]
55 out.WriteString("//go:generate go run ./genicons/. -pkg p9icons . \n")
56 out.WriteString("// generated by go run gen.go; DO NOT EDIT\n\npackage ")
57 out.WriteString(*pkgName)
58 out.WriteString("\n\n")
59 if e := genDir(iconsDir); E.Chk(e) {
60 F.Ln(e)
61 }
62 _, _ = fmt.Fprintf(
63 out,
64 "// In total, %d SVG bytes in %d files converted to %d IconVG bytes.\n",
65 totalSVGBytes, totalFiles, totalIVGBytes,
66 )
67 if len(failures) != 0 {
68 out.WriteString("\n/*\nFAILURES:\n\n")
69 for _, failure := range failures {
70 out.WriteString(failure)
71 out.WriteByte('\n')
72 }
73 out.WriteString("\n*/")
74 }
75 if *outDir != "" {
76 if e := os.MkdirAll(*outDir, 0775); e != nil && !os.IsExist(e) {
77 F.Ln(e)
78 }
79 }
80 raw := out.Bytes()
81 formatted, e := format.Source(raw)
82 if e != nil {
83 log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", e, raw)
84 }
85 // formatted := raw
86 if e := ioutil.WriteFile(filepath.Join(*outDir, "data.go"), formatted, 0644); E.Chk(e) {
87 log.Fatalf("WriteFile failed: %s\n", e)
88 }
89 {
90 b := new(bytes.Buffer)
91 b.WriteString("// generated by go run genicons.go; DO NOT EDIT\n\npackage ")
92 b.WriteString(*pkgName)
93 b.WriteString("\n\n")
94 b.WriteString("var list = []struct{ name string; data []byte } {\n")
95 for _, v := range varNames {
96 _, _ = fmt.Fprintf(b, "{%q, %s},\n", v, v)
97 }
98 b.WriteString("}\n\n")
99 raw := b.Bytes()
100 formatted, e := format.Source(raw)
101 if e != nil {
102 log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", e, raw)
103 }
104 if e := ioutil.WriteFile(filepath.Join(*outDir, "data_test.go"), formatted, 0644); E.Chk(e) {
105 log.Fatalf("WriteFile failed: %s\n", e)
106 }
107 }
108 }
109
110 func genDir(dirName string) (e error) {
111 fqSVGDirName := filepath.FromSlash(dirName)
112 f, e := os.Open(fqSVGDirName)
113 if e != nil {
114 return e
115 }
116 defer func() {
117 if e = f.Close(); E.Chk(e) {
118 }
119 }()
120
121 var infos []os.FileInfo
122 infos, e = f.Readdir(-1)
123 if e != nil {
124 F.Ln(e)
125 }
126 baseNames, fileNames, sizes := []string{}, map[string]string{}, map[string]int{}
127 for _, info := range infos {
128 name := info.Name()
129
130 nameParts := strings.Split(name, "_")
131 if len(nameParts) != 3 || nameParts[0] != "ic" {
132 continue
133 }
134 baseName := nameParts[1]
135 var size int
136 if n, e := fmt.Sscanf(nameParts[2], "%dpx.svg", &size); e != nil || n != 1 {
137 continue
138 }
139 if prevSize, ok := sizes[baseName]; ok {
140 if size > prevSize {
141 fileNames[baseName] = name
142 sizes[baseName] = size
143 }
144 } else {
145 fileNames[baseName] = name
146 sizes[baseName] = size
147 baseNames = append(baseNames, baseName)
148 }
149 }
150
151 sort.Strings(baseNames)
152 for _, baseName := range baseNames {
153 fileName := fileNames[baseName]
154 path := filepath.Join(dirName, fileName)
155 f, e := ioutil.ReadFile(path)
156 if e != nil {
157 failures = append(failures, fmt.Sprintf("%s: %v", path, e))
158 continue
159 }
160 if e = genFile(f, baseName, float32(sizes[baseName])); E.Chk(e) {
161 failures = append(failures, fmt.Sprintf("%s: %v", path, e))
162 continue
163 }
164 }
165 return nil
166 }
167
168 type SVG struct {
169 Width string `xml:"width,attr"`
170 Height string `xml:"height,attr"`
171 Fill string `xml:"fill,attr"`
172 ViewBox string `xml:"viewBox,attr"`
173 Paths []*Path `xml:"path"`
174 // Some of the SVG files contain <circle> elements, not just <path>
175 // elements. IconVG doesn't have circles per se. Instead, we convert such
176 // circles to paired arcTo commands, tacked on to the first path.
177 //
178 // In general, this isn't correct if the circles and the path overlap, but
179 // that doesn't happen in the specific case of the Material Design icons.
180 Circles []Circle `xml:"circle"`
181 }
182
183 type Path struct {
184 D string `xml:"d,attr"`
185 Fill string `xml:"fill,attr"`
186 FillOpacity *float32 `xml:"fill-opacity,attr"`
187 Opacity *float32 `xml:"opacity,attr"`
188
189 creg uint8
190 }
191
192 type Circle struct {
193 Cx float32 `xml:"cx,attr"`
194 Cy float32 `xml:"cy,attr"`
195 R float32 `xml:"r,attr"`
196 }
197
198 func genFile(svgData []byte, baseName string, outSize float32) (e error) {
199 var varName string
200 for _, s := range strings.Split(baseName, "_") {
201 varName += upperCase(s)
202 }
203 _, _ = fmt.Fprintf(out, "var %s = []byte{", varName)
204 defer func() {
205 _, _ = fmt.Fprintf(out, "\n}\n\n")
206 }()
207 varNames = append(varNames, varName)
208
209 g := &SVG{}
210 if e = xml.Unmarshal(svgData, g); E.Chk(e) {
211 return e
212 }
213
214 var vbx, vby, vbx2, vby2 float32
215 for i, v := range strings.Split(g.ViewBox, " ") {
216 var f float64
217 f, e = strconv.ParseFloat(v, 32)
218 if e != nil {
219 return fmt.Errorf(
220 "genFile: failed to parse ViewBox (%q): %v",
221 g.ViewBox, e,
222 )
223 }
224 switch i {
225 case 0:
226 vbx = float32(f)
227 case 1:
228 vby = float32(f)
229 case 2:
230 vbx2 = float32(f)
231 case 3:
232 vby2 = float32(f)
233 }
234 }
235 dx, dy := outSize, outSize
236 var size float32
237 if aspect := (vbx2 - vbx) / (vby2 - vby); aspect >= 1 {
238 dy /= aspect
239 size = vbx2 - vbx
240 } else {
241 dx /= aspect
242 size = vby2 - vby
243 }
244 palette := iconvg.DefaultPalette
245 pmap := make(map[color.RGBA]uint8)
246 for _, p := range g.Paths {
247 if p.Fill == "" {
248 p.Fill = g.Fill
249 }
250 var c color.RGBA
251 c, e = parseColor(p.Fill)
252 if e != nil {
253 return e
254 }
255 var ok bool
256 if p.creg, ok = pmap[c]; !ok {
257 if len(pmap) == 64 {
258 panic("too many colors")
259 }
260 p.creg = uint8(len(pmap))
261 palette[p.creg] = c
262 pmap[c] = p.creg
263 }
264 }
265 var enc iconvg.Encoder
266 enc.Reset(
267 iconvg.Metadata{
268 ViewBox: iconvg.Rectangle{
269 Min: f32.Vec2{-dx * .5, -dy * .5},
270 Max: f32.Vec2{+dx * .5, +dy * .5},
271 },
272 Palette: palette,
273 },
274 )
275
276 offset := f32.Vec2{
277 vbx * outSize / size,
278 vby * outSize / size,
279 }
280
281 // adjs maps from opacity to a cReg adj value.
282 adjs := map[float32]uint8{}
283
284 for _, p := range g.Paths {
285 if e = genPath(&enc, p, adjs, outSize, size, offset, g.Circles); E.Chk(e) {
286 return e
287 }
288 g.Circles = nil
289 }
290
291 if len(g.Circles) != 0 {
292 if e = genPath(&enc, &Path{}, adjs, outSize, size, offset, g.Circles); E.Chk(e) {
293 return e
294 }
295 g.Circles = nil
296 }
297
298 ivgData, e := enc.Bytes()
299 if e != nil {
300 return fmt.Errorf("iconvg encoding failed: %v", e)
301 }
302 for i, x := range ivgData {
303 if i&0x0f == 0x00 {
304 out.WriteByte('\n')
305 }
306 _, _ = fmt.Fprintf(out, "%#02x, ", x)
307 }
308
309 totalFiles++
310 totalSVGBytes += len(svgData)
311 totalIVGBytes += len(ivgData)
312 return nil
313 }
314
315 func parseColor(col string) (color.RGBA, error) {
316 if col == "none" {
317 return color.RGBA{}, nil
318 }
319 if len(col) == 0 {
320 return color.RGBA{A: 0xff}, nil
321 }
322 if len(col) == 0 || col[0] != '#' {
323 return color.RGBA{}, fmt.Errorf("invalid color: %q", col)
324 }
325 col = col[1:]
326 if len(col) != 6 {
327 return color.RGBA{}, fmt.Errorf("invalid color length: %q", col)
328 }
329 elems := make([]byte, len(col)/2)
330 for i := range elems {
331 u, e := strconv.ParseUint(col[i*2:i*2+2], 16, 8)
332 if e != nil {
333 return color.RGBA{}, e
334 }
335 elems[i] = byte(u)
336 }
337 return color.RGBA{R: elems[0], G: elems[1], B: elems[2], A: 255}, nil
338 }
339
340 func genPath(
341 enc *iconvg.Encoder,
342 p *Path,
343 adjs map[float32]uint8,
344 outSize, size float32,
345 offset f32.Vec2,
346 circles []Circle,
347 ) (e error) {
348 adj := uint8(0)
349 opacity := float32(1)
350 if p.Opacity != nil {
351 opacity = *p.Opacity
352 } else if p.FillOpacity != nil {
353 opacity = *p.FillOpacity
354 }
355 if opacity != 1 {
356 var ok bool
357 if adj, ok = adjs[opacity]; !ok {
358 adj = uint8(len(adjs) + 1)
359 adjs[opacity] = adj
360 // Set CREG[0-adj] to be a blend of transparent (0x7f) and the
361 // first custom palette color (0x80).
362 enc.SetCReg(adj, false, iconvg.BlendColor(uint8(opacity*0xff), 0x7f, 0x80+p.creg))
363 }
364 } else {
365 enc.SetCReg(adj, false, iconvg.PaletteIndexColor(p.creg))
366 }
367
368 needStartPath := true
369 if p.D != "" {
370 needStartPath = false
371 if e := genPathData(enc, adj, p.D, outSize, size, offset); E.Chk(e) {
372 return e
373 }
374 }
375
376 for _, c := range circles {
377 // Normalize.
378 cx := c.Cx * outSize / size
379 cx -= outSize/2 + offset[0]
380 cy := c.Cy * outSize / size
381 cy -= outSize/2 + offset[1]
382 r := c.R * outSize / size
383
384 if needStartPath {
385 needStartPath = false
386 enc.StartPath(adj, cx-r, cy)
387 } else {
388 enc.ClosePathAbsMoveTo(cx-r, cy)
389 }
390
391 // Convert a circle to two relative arcTo ops, each of 180 degrees.
392 // We can't use one 360 degree arcTo as the start and end point
393 // would be coincident and the computation is degenerate.
394 enc.RelArcTo(r, r, 0, false, true, +2*r, 0)
395 enc.RelArcTo(r, r, 0, false, true, -2*r, 0)
396 }
397
398 enc.ClosePathEndPath()
399 return nil
400 }
401
402 func genPathData(enc *iconvg.Encoder, adj uint8, pathData string, outSize, size float32, offset f32.Vec2) (e error) {
403 if strings.HasSuffix(pathData, "z") {
404 pathData = pathData[:len(pathData)-1]
405 }
406 r := strings.NewReader(pathData)
407
408 var args [7]float32
409 op, relative, started := byte(0), false, false
410 var count int
411 for {
412 b, e := r.ReadByte()
413 if e == io.EOF {
414 break
415 }
416 if e != nil {
417 return e
418 }
419 count++
420
421 switch {
422 case b == ' ' || b == '\n' || b == '\t':
423 continue
424 case 'A' <= b && b <= 'Z':
425 op, relative = b, false
426 case 'a' <= b && b <= 'z':
427 op, relative = b, true
428 default:
429 if e := r.UnreadByte(); E.Chk(e) {
430 }
431 }
432
433 n := 0
434 switch op {
435 case 'A', 'a':
436 n = 7
437 case 'L', 'l', 'T', 't':
438 n = 2
439 case 'Q', 'q', 'S', 's':
440 n = 4
441 case 'C', 'c':
442 n = 6
443 case 'H', 'h', 'V', 'v':
444 n = 1
445 case 'M', 'm':
446 n = 2
447 case 'Z', 'z':
448 default:
449 return fmt.Errorf("unknown opcode %c\n", b)
450 }
451
452 scan(&args, r, n)
453 normalize(&args, n, op, outSize, size, offset, relative)
454
455 switch op {
456 case 'A':
457 enc.AbsArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
458 case 'a':
459 enc.RelArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
460 case 'L':
461 enc.AbsLineTo(args[0], args[1])
462 case 'l':
463 enc.RelLineTo(args[0], args[1])
464 case 'T':
465 enc.AbsSmoothQuadTo(args[0], args[1])
466 case 't':
467 enc.RelSmoothQuadTo(args[0], args[1])
468 case 'Q':
469 enc.AbsQuadTo(args[0], args[1], args[2], args[3])
470 case 'q':
471 enc.RelQuadTo(args[0], args[1], args[2], args[3])
472 case 'S':
473 enc.AbsSmoothCubeTo(args[0], args[1], args[2], args[3])
474 case 's':
475 enc.RelSmoothCubeTo(args[0], args[1], args[2], args[3])
476 case 'C':
477 enc.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
478 case 'c':
479 enc.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
480 case 'H':
481 enc.AbsHLineTo(args[0])
482 case 'h':
483 enc.RelHLineTo(args[0])
484 case 'V':
485 enc.AbsVLineTo(args[0])
486 case 'v':
487 enc.RelVLineTo(args[0])
488 case 'M':
489 if !started {
490 started = true
491 enc.StartPath(adj, args[0], args[1])
492 } else {
493 enc.ClosePathAbsMoveTo(args[0], args[1])
494 }
495 case 'm':
496 enc.ClosePathRelMoveTo(args[0], args[1])
497 }
498 }
499 return nil
500 }
501
502 func scan(args *[7]float32, r *strings.Reader, n int) {
503 for i := 0; i < n; i++ {
504 for {
505 if b, _ := r.ReadByte(); b != ' ' && b != ',' && b != '\n' && b != '\t' {
506 if e := r.UnreadByte(); E.Chk(e) {
507 }
508 break
509 }
510 }
511 _, _ = fmt.Fscanf(r, "%f", &args[i])
512 }
513 }
514
515 func normalize(args *[7]float32, n int, op byte, outSize, size float32, offset f32.Vec2, relative bool) {
516 for i := 0; i < n; i++ {
517 if (op == 'A' || op == 'a') && (i == 3 || i == 4) {
518 continue
519 }
520 args[i] *= outSize / size
521 if relative {
522 continue
523 }
524 if (op == 'A' || op == 'a') && i < 5 {
525 // For arcs, skip everything other than x, y.
526 continue
527 }
528 args[i] -= outSize / 2
529 switch {
530 case op == 'A' && i == 5: // Arc x.
531 args[i] -= offset[0]
532 case op == 'A' && i == 6: // Arc y.
533 args[i] -= offset[1]
534 case n != 1:
535 args[i] -= offset[i&0x01]
536 case op == 'H':
537 args[i] -= offset[0]
538 case op == 'V':
539 args[i] -= offset[1]
540 }
541 }
542 }
543