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