| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build ignore |
| |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/xml" |
| "errors" |
| "flag" |
| "fmt" |
| "go/format" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "path" |
| "path/filepath" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/exp/shiny/iconvg" |
| "golang.org/x/image/math/f32" |
| ) |
| |
| var mdicons = flag.String("mdicons", "", "The directory on the local file system where "+ |
| "https://github.com/google/material-design-icons was checked out", |
| ) |
| |
| // outSize is the width and height (in ideal vector space) of the generated |
| // IconVG graphic, regardless of the size of the input SVG. |
| const outSize = 48 |
| |
| // errSkip deliberately skips generating an icon. |
| // |
| // When manually debugging one particular icon, it can be useful to add |
| // something like: |
| // if baseName != "check_box" { return errSkip } |
| // at the top of func genFile. |
| var errSkip = errors.New("skipping SVG to IconVG conversion") |
| |
| var ( |
| out = new(bytes.Buffer) |
| failures = []string{} |
| varNames = []string{} |
| |
| totalFiles int |
| totalIVGBytes int |
| totalPNG24Bytes int |
| totalPNG48Bytes int |
| totalSVGBytes int |
| ) |
| |
| var acronyms = map[string]string{ |
| "3d": "3D", |
| "ac": "AC", |
| "adb": "ADB", |
| "airplanemode": "AirplaneMode", |
| "atm": "ATM", |
| "av": "AV", |
| "ccw": "CCW", |
| "cw": "CW", |
| "din": "DIN", |
| "dns": "DNS", |
| "dvr": "DVR", |
| "eta": "ETA", |
| "ev": "EV", |
| "gif": "GIF", |
| "gps": "GPS", |
| "hd": "HD", |
| "hdmi": "HDMI", |
| "hdr": "HDR", |
| "http": "HTTP", |
| "https": "HTTPS", |
| "iphone": "IPhone", |
| "iso": "ISO", |
| "jpeg": "JPEG", |
| "markunread": "MarkUnread", |
| "mms": "MMS", |
| "nfc": "NFC", |
| "ondemand": "OnDemand", |
| "pdf": "PDF", |
| "phonelink": "PhoneLink", |
| "png": "PNG", |
| "rss": "RSS", |
| "rv": "RV", |
| "sd": "SD", |
| "sim": "SIM", |
| "sip": "SIP", |
| "sms": "SMS", |
| "streetview": "StreetView", |
| "svideo": "SVideo", |
| "textdirection": "TextDirection", |
| "textsms": "TextSMS", |
| "timelapse": "TimeLapse", |
| "toc": "TOC", |
| "tv": "TV", |
| "usb": "USB", |
| "vpn": "VPN", |
| "wb": "WB", |
| "wc": "WC", |
| "whatshot": "WhatsHot", |
| "wifi": "WiFi", |
| } |
| |
| func upperCase(s string) string { |
| if a, ok := acronyms[s]; ok { |
| return a |
| } |
| if c := s[0]; 'a' <= c && c <= 'z' { |
| return string(c-0x20) + s[1:] |
| } |
| return s |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| out.WriteString("// generated by go run gen.go; DO NOT EDIT\n\npackage icons\n\n") |
| |
| f, err := os.Open(*mdicons) |
| if err != nil { |
| log.Fatalf("%v\n\nDid you override the -mdicons flag in icons.go?\n\n", err) |
| } |
| defer f.Close() |
| infos, err := f.Readdir(-1) |
| if err != nil { |
| log.Fatal(err) |
| } |
| names := []string{} |
| for _, info := range infos { |
| if !info.IsDir() { |
| continue |
| } |
| name := info.Name() |
| if name[0] == '.' { |
| continue |
| } |
| names = append(names, name) |
| } |
| sort.Strings(names) |
| for _, name := range names { |
| genDir(name) |
| } |
| |
| fmt.Fprintf(out, |
| "// In total, %d SVG bytes in %d files (%d PNG bytes at 24px * 24px,\n"+ |
| "// %d PNG bytes at 48px * 48px) converted to %d IconVG bytes.\n", |
| totalSVGBytes, totalFiles, totalPNG24Bytes, totalPNG48Bytes, totalIVGBytes) |
| |
| if len(failures) != 0 { |
| out.WriteString("\n/*\nFAILURES:\n\n") |
| for _, failure := range failures { |
| out.WriteString(failure) |
| out.WriteByte('\n') |
| } |
| out.WriteString("\n*/") |
| } |
| |
| raw := out.Bytes() |
| formatted, err := format.Source(raw) |
| if err != nil { |
| log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", err, raw) |
| } |
| if err := ioutil.WriteFile("data.go", formatted, 0644); err != nil { |
| log.Fatalf("WriteFile failed: %s\n", err) |
| } |
| |
| // Generate data_test.go. The code immediately above generates data.go. |
| { |
| b := new(bytes.Buffer) |
| b.WriteString("// generated by go run gen.go; DO NOT EDIT\n\npackage icons\n\n") |
| b.WriteString("var list = []struct{ name string; data []byte } {\n") |
| for _, v := range varNames { |
| fmt.Fprintf(b, "{%q, %s},\n", v, v) |
| } |
| b.WriteString("}\n\n") |
| raw := b.Bytes() |
| formatted, err := format.Source(raw) |
| if err != nil { |
| log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", err, raw) |
| } |
| if err := ioutil.WriteFile("data_test.go", formatted, 0644); err != nil { |
| log.Fatalf("WriteFile failed: %s\n", err) |
| } |
| } |
| } |
| |
| func genDir(dirName string) { |
| fqPNGDirName := filepath.FromSlash(path.Join(*mdicons, dirName, "1x_web")) |
| fqSVGDirName := filepath.FromSlash(path.Join(*mdicons, dirName, "svg/production")) |
| f, err := os.Open(fqSVGDirName) |
| if err != nil { |
| return |
| } |
| defer f.Close() |
| |
| infos, err := f.Readdir(-1) |
| if err != nil { |
| log.Fatal(err) |
| } |
| baseNames, fileNames, sizes := []string{}, map[string]string{}, map[string]int{} |
| for _, info := range infos { |
| name := info.Name() |
| |
| if !strings.HasPrefix(name, "ic_") || skippedFiles[[2]string{dirName, name}] { |
| continue |
| } |
| size := 0 |
| switch { |
| case strings.HasSuffix(name, "_12px.svg"): |
| size = 12 |
| case strings.HasSuffix(name, "_18px.svg"): |
| size = 18 |
| case strings.HasSuffix(name, "_24px.svg"): |
| size = 24 |
| case strings.HasSuffix(name, "_36px.svg"): |
| size = 36 |
| case strings.HasSuffix(name, "_48px.svg"): |
| size = 48 |
| default: |
| continue |
| } |
| |
| baseName := name[3 : len(name)-9] |
| if prevSize, ok := sizes[baseName]; ok { |
| if size > prevSize { |
| fileNames[baseName] = name |
| sizes[baseName] = size |
| } |
| } else { |
| fileNames[baseName] = name |
| sizes[baseName] = size |
| baseNames = append(baseNames, baseName) |
| } |
| } |
| |
| sort.Strings(baseNames) |
| for _, baseName := range baseNames { |
| fileName := fileNames[baseName] |
| err := genFile(fqSVGDirName, dirName, baseName, fileName, float32(sizes[baseName])) |
| if err == errSkip { |
| continue |
| } |
| if err != nil { |
| failures = append(failures, fmt.Sprintf("%v/svg/production/%v: %v", dirName, fileName, err)) |
| continue |
| } |
| totalPNG24Bytes += pngSize(fqPNGDirName, dirName, baseName, 24) |
| totalPNG48Bytes += pngSize(fqPNGDirName, dirName, baseName, 48) |
| } |
| } |
| |
| func pngSize(fqPNGDirName, dirName, baseName string, targetSize int) int { |
| for _, size := range [...]int{48, 24, 18} { |
| if size > targetSize { |
| continue |
| } |
| fInfo, err := os.Stat(filepath.Join(fqPNGDirName, |
| fmt.Sprintf("ic_%s_black_%ddp.png", baseName, size))) |
| if err != nil { |
| continue |
| } |
| return int(fInfo.Size()) |
| } |
| failures = append(failures, |
| fmt.Sprintf("no PNG found for %s/1x_web/ic_%s_black_{48,24,18}dp.png", dirName, baseName)) |
| return 0 |
| } |
| |
| type SVG struct { |
| Width float32 `xml:"where,attr"` |
| Height float32 `xml:"height,attr"` |
| ViewBox string `xml:"viewBox,attr"` |
| Paths []Path `xml:"path"` |
| // Some of the SVG files contain <circle> elements, not just <path> |
| // elements. IconVG doesn't have circles per se. Instead, we convert such |
| // circles to be paired arcTo commands, tacked on to the first path. |
| // |
| // In general, this isn't correct if the circles and the path overlap, but |
| // that doesn't happen in the specific case of the Material Design icons. |
| Circles []Circle `xml:"circle"` |
| } |
| |
| type Path struct { |
| D string `xml:"d,attr"` |
| Fill string `xml:"fill,attr"` |
| FillOpacity *float32 `xml:"fill-opacity,attr"` |
| Opacity *float32 `xml:"opacity,attr"` |
| } |
| |
| type Circle struct { |
| Cx float32 `xml:"cx,attr"` |
| Cy float32 `xml:"cy,attr"` |
| R float32 `xml:"r,attr"` |
| } |
| |
| var skippedPaths = map[string]string{ |
| // hardware/svg/production/ic_scanner_48px.svg contains a filled white |
| // rectangle that is overwritten by the subsequent path. |
| // |
| // See https://github.com/google/material-design-icons/issues/490 |
| // |
| // Matches <path fill="#fff" d="M16 34h22v4H16z"/> |
| "M16 34h22v4H16z": "#fff", |
| |
| // device/svg/production/ic_airplanemode_active_48px.svg and |
| // maps/svg/production/ic_flight_48px.svg contain a degenerate path that |
| // contains only one moveTo op. |
| // |
| // See https://github.com/google/material-design-icons/issues/491 |
| // |
| // Matches <path d="M20.36 18"/> |
| "M20.36 18": "", |
| } |
| |
| var skippedFiles = map[[2]string]bool{ |
| // ic_play_circle_filled_white_48px.svg is just the same as |
| // ic_play_circle_filled_48px.svg with an explicit fill="#fff". |
| {"av", "ic_play_circle_filled_white_48px.svg"}: true, |
| } |
| |
| func genFile(fqSVGDirName, dirName, baseName, fileName string, size float32) error { |
| fqFileName := filepath.Join(fqSVGDirName, fileName) |
| svgData, err := ioutil.ReadFile(fqFileName) |
| if err != nil { |
| return err |
| } |
| |
| varName := upperCase(dirName) |
| for _, s := range strings.Split(baseName, "_") { |
| varName += upperCase(s) |
| } |
| fmt.Fprintf(out, "var %s = []byte{", varName) |
| defer fmt.Fprintf(out, "\n}\n\n") |
| varNames = append(varNames, varName) |
| |
| var enc iconvg.Encoder |
| enc.Reset(iconvg.Metadata{ |
| ViewBox: iconvg.Rectangle{ |
| Min: f32.Vec2{-24, -24}, |
| Max: f32.Vec2{+24, +24}, |
| }, |
| Palette: iconvg.DefaultPalette, |
| }) |
| |
| g := &SVG{} |
| if err := xml.Unmarshal(svgData, g); err != nil { |
| return err |
| } |
| |
| var vbx, vby float32 |
| for i, v := range strings.Split(g.ViewBox, " ") { |
| f, err := strconv.ParseFloat(v, 32) |
| if err != nil { |
| return err |
| } |
| switch i { |
| case 0: |
| vbx = float32(f) |
| case 1: |
| vby = float32(f) |
| } |
| } |
| offset := f32.Vec2{ |
| vbx * outSize / size, |
| vby * outSize / size, |
| } |
| |
| // adjs maps from opacity to a cReg adj value. |
| adjs := map[float32]uint8{} |
| |
| for _, p := range g.Paths { |
| if fill, ok := skippedPaths[p.D]; ok && fill == p.Fill { |
| continue |
| } |
| if err := genPath(&enc, &p, adjs, size, offset, g.Circles); err != nil { |
| return err |
| } |
| g.Circles = nil |
| } |
| |
| if len(g.Circles) != 0 { |
| if err := genPath(&enc, &Path{}, adjs, size, offset, g.Circles); err != nil { |
| return err |
| } |
| g.Circles = nil |
| } |
| |
| ivgData, err := enc.Bytes() |
| if err != nil { |
| return err |
| } |
| for i, x := range ivgData { |
| if i&0x0f == 0x00 { |
| out.WriteByte('\n') |
| } |
| fmt.Fprintf(out, "%#02x, ", x) |
| } |
| |
| totalFiles++ |
| totalSVGBytes += len(svgData) |
| totalIVGBytes += len(ivgData) |
| return nil |
| } |
| |
| func genPath(enc *iconvg.Encoder, p *Path, adjs map[float32]uint8, size float32, offset f32.Vec2, circles []Circle) error { |
| adj := uint8(0) |
| opacity := float32(1) |
| if p.Opacity != nil { |
| opacity = *p.Opacity |
| } else if p.FillOpacity != nil { |
| opacity = *p.FillOpacity |
| } |
| if opacity != 1 { |
| var ok bool |
| if adj, ok = adjs[opacity]; !ok { |
| adj = uint8(len(adjs) + 1) |
| adjs[opacity] = adj |
| // Set CREG[0-adj] to be a blend of transparent (0x7f) and the |
| // first custom palette color (0x80). |
| enc.SetCReg(adj, false, iconvg.BlendColor(uint8(opacity*0xff), 0x7f, 0x80)) |
| } |
| } |
| |
| needStartPath := true |
| if p.D != "" { |
| needStartPath = false |
| if err := genPathData(enc, adj, p.D, size, offset); err != nil { |
| return err |
| } |
| } |
| |
| for _, c := range circles { |
| // Normalize. |
| cx := c.Cx * outSize / size |
| cx -= outSize/2 + offset[0] |
| cy := c.Cy * outSize / size |
| cy -= outSize/2 + offset[1] |
| r := c.R * outSize / size |
| |
| if needStartPath { |
| needStartPath = false |
| enc.StartPath(adj, cx-r, cy) |
| } else { |
| enc.ClosePathAbsMoveTo(cx-r, cy) |
| } |
| |
| // Convert a circle to two relative arcTo ops, each of 180 degrees. |
| // We can't use one 360 degree arcTo as the start and end point |
| // would be coincident and the computation is degenerate. |
| enc.RelArcTo(r, r, 0, false, true, +2*r, 0) |
| enc.RelArcTo(r, r, 0, false, true, -2*r, 0) |
| } |
| |
| enc.ClosePathEndPath() |
| return nil |
| } |
| |
| func genPathData(enc *iconvg.Encoder, adj uint8, pathData string, size float32, offset f32.Vec2) error { |
| if strings.HasSuffix(pathData, "z") { |
| pathData = pathData[:len(pathData)-1] |
| } |
| r := strings.NewReader(pathData) |
| |
| var args [6]float32 |
| op, relative, started := byte(0), false, false |
| for { |
| b, err := r.ReadByte() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return err |
| } |
| |
| switch { |
| case b == ' ': |
| continue |
| case 'A' <= b && b <= 'Z': |
| op, relative = b, false |
| case 'a' <= b && b <= 'z': |
| op, relative = b, true |
| default: |
| r.UnreadByte() |
| } |
| |
| n := 0 |
| switch op { |
| case 'L', 'l', 'T', 't': |
| n = 2 |
| case 'Q', 'q', 'S', 's': |
| n = 4 |
| case 'C', 'c': |
| n = 6 |
| case 'H', 'h', 'V', 'v': |
| n = 1 |
| case 'M', 'm': |
| n = 2 |
| case 'Z', 'z': |
| default: |
| return fmt.Errorf("unknown opcode %c\n", b) |
| } |
| |
| scan(&args, r, n) |
| normalize(&args, n, op, size, offset, relative) |
| |
| switch op { |
| case 'L': |
| enc.AbsLineTo(args[0], args[1]) |
| case 'l': |
| enc.RelLineTo(args[0], args[1]) |
| case 'T': |
| enc.AbsSmoothQuadTo(args[0], args[1]) |
| case 't': |
| enc.RelSmoothQuadTo(args[0], args[1]) |
| case 'Q': |
| enc.AbsQuadTo(args[0], args[1], args[2], args[3]) |
| case 'q': |
| enc.RelQuadTo(args[0], args[1], args[2], args[3]) |
| case 'S': |
| enc.AbsSmoothCubeTo(args[0], args[1], args[2], args[3]) |
| case 's': |
| enc.RelSmoothCubeTo(args[0], args[1], args[2], args[3]) |
| case 'C': |
| enc.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5]) |
| case 'c': |
| enc.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5]) |
| case 'H': |
| enc.AbsHLineTo(args[0]) |
| case 'h': |
| enc.RelHLineTo(args[0]) |
| case 'V': |
| enc.AbsVLineTo(args[0]) |
| case 'v': |
| enc.RelVLineTo(args[0]) |
| case 'M': |
| if !started { |
| started = true |
| enc.StartPath(adj, args[0], args[1]) |
| } else { |
| enc.ClosePathAbsMoveTo(args[0], args[1]) |
| } |
| case 'm': |
| enc.ClosePathRelMoveTo(args[0], args[1]) |
| } |
| } |
| return nil |
| } |
| |
| func scan(args *[6]float32, r *strings.Reader, n int) { |
| for i := 0; i < n; i++ { |
| for { |
| if b, _ := r.ReadByte(); b != ' ' { |
| r.UnreadByte() |
| break |
| } |
| } |
| fmt.Fscanf(r, "%f", &args[i]) |
| } |
| } |
| |
| func atof(s []byte) (float32, error) { |
| f, err := strconv.ParseFloat(string(s), 32) |
| if err != nil { |
| return 0, fmt.Errorf("could not parse %q as a float32: %v", s, err) |
| } |
| return float32(f), err |
| } |
| |
| func normalize(args *[6]float32, n int, op byte, size float32, offset f32.Vec2, relative bool) { |
| for i := 0; i < n; i++ { |
| args[i] *= outSize / size |
| if relative { |
| continue |
| } |
| args[i] -= outSize / 2 |
| switch { |
| case n != 1: |
| args[i] -= offset[i&0x01] |
| case op == 'H': |
| args[i] -= offset[0] |
| case op == 'V': |
| args[i] -= offset[1] |
| } |
| } |
| } |