blob: c738c6093ec365805d17770e5cbeee59987337d4 [file] [log] [blame]
// 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]
}
}
}