@@ 0,0 1,582 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Command svg2gio converts SVG files to Gio functions. Only a limited subset of
+// SVG files are supported.
+package main
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "go/format"
+
+ "gioui.org/f32"
+)
+
+var (
+ pkg = flag.String("pkg", "", "Go package")
+ output = flag.String("o", "svg.go", "Output Go file")
+)
+
+func main() {
+ flag.Parse()
+ if *pkg == "" {
+ fmt.Fprintf(os.Stderr, "specify a package name (-pkg)\n")
+ os.Exit(1)
+ }
+ args := flag.Args()
+ if err := convertAll(args); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(2)
+ }
+}
+
+type Points []float32
+
+func (p *Points) UnmarshalText(text []byte) error {
+ for {
+ text = bytes.TrimLeft(text, "\t\n")
+ if len(text) == 0 {
+ break
+ }
+ var num []byte
+ end := bytes.IndexAny(text, " ,")
+ if end != -1 {
+ num = text[:end]
+ text = text[end+1:]
+ } else {
+ num = text
+ text = nil
+ }
+ f, err := strconv.ParseFloat(string(num), 32)
+ if err != nil {
+ return err
+ }
+ *p = append(*p, float32(f))
+ }
+ return nil
+}
+
+type Transform f32.Affine2D
+
+func (t *Transform) UnmarshalText(text []byte) error {
+ switch {
+ case bytes.HasPrefix(text, []byte("matrix(")) && bytes.HasSuffix(text, []byte(")")):
+ trans := text[7 : len(text)-1]
+ var p Points
+ if err := p.UnmarshalText(trans); err != nil {
+ return err
+ }
+ if len(p) != 6 {
+ return fmt.Errorf("malformed transform matrix: %q", text)
+ }
+ *t = Transform(f32.NewAffine2D(p[0], p[2], p[4], p[1], p[3], p[5]))
+ return nil
+ default:
+ return fmt.Errorf("unsupported transform: %q", text)
+ }
+}
+
+type Fill struct {
+ Transform Transform `xml:"transform,attr"`
+ Fill Color `xml:"fill,attr"`
+ Stroke Color `xml:"stroke,attr"`
+ StrokeLinejoin string `xml:"stroke-linejoin,attr"`
+ StrokeLinecap string `xml:"stroke-linecap,attr"`
+ StrokeWidth float32 `xml:"stroke-width,attr"`
+}
+
+type Color struct {
+ Set bool
+ Value int
+}
+
+func (c *Color) UnmarshalText(text []byte) error {
+ if string(text) == "none" {
+ *c = Color{}
+ return nil
+ }
+ if !bytes.HasPrefix(text, []byte("#")) {
+ return fmt.Errorf("invalid color: %q", text)
+ }
+ text = text[1:]
+ i, err := strconv.ParseInt(string(text), 16, 32)
+ // Implied alpha.
+ if len(text) == 6 {
+ i |= 0xff000000
+ }
+ *c = Color{
+ Set: true,
+ Value: int(i),
+ }
+ return err
+}
+
+func convertAll(files []string) error {
+ w := new(bytes.Buffer)
+ fmt.Fprintf(w, "// Code generated by gioui.org/cmd/svg2gio; DO NOT EDIT.\n\n")
+ fmt.Fprintf(w, "package %s\n\n", *pkg)
+ fmt.Fprintf(w, "import \"image/color\"\n")
+ fmt.Fprintf(w, "import \"math\"\n")
+ fmt.Fprintf(w, "import \"gioui.org/op\"\n")
+ fmt.Fprintf(w, "import \"gioui.org/op/clip\"\n")
+ fmt.Fprintf(w, "import \"gioui.org/op/paint\"\n")
+ fmt.Fprintf(w, "import \"gioui.org/f32\"\n\n")
+ fmt.Fprintf(w, "var ops op.Ops\n\n")
+ fmt.Fprintf(w, funcs)
+ for _, filename := range files {
+ if err := convert(w, filename); err != nil {
+ return err
+ }
+ }
+ src, err := format.Source(w.Bytes())
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(*output, src, 0o660)
+}
+
+func convert(w io.Writer, filename string) error {
+ base := filepath.Base(filename)
+ ext := filepath.Ext(base)
+ name := "Image_" + base[:len(base)-len(ext)]
+
+ fmt.Fprintf(w, "var %s struct {\n", name)
+ fmt.Fprintf(w, "ViewBox struct { Min, Max f32.Point }\n")
+ fmt.Fprintf(w, "Call op.CallOp\n\n")
+ fmt.Fprintf(w, "}\n")
+ fmt.Fprintf(w, "func init() {\n")
+ defer fmt.Fprintf(w, "}\n")
+ f, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ d := xml.NewDecoder(f)
+ if err := parse(w, d, name); err != nil {
+ line, col := d.InputPos()
+ return fmt.Errorf("%s:%d:%d: %w", filename, line, col, err)
+ }
+ return nil
+}
+
+func parse(w io.Writer, d *xml.Decoder, name string) error {
+ for {
+ tok, err := d.Token()
+ if err != nil {
+ if err == io.EOF {
+ return errors.New("unexpected end of file")
+ }
+ return err
+ }
+ switch tok := tok.(type) {
+ case xml.StartElement:
+ if n := tok.Name.Local; n != "svg" {
+ return fmt.Errorf("invalid SVG root: <%s>", n)
+ }
+ if n := tok.Name.Space; n != "http://www.w3.org/2000/svg" {
+ return fmt.Errorf("unsupported SVG namespace: %s", n)
+ }
+ fmt.Fprintf(w, "m := op.Record(&ops)\n")
+ defer fmt.Fprintf(w, "%s.Call = m.Stop()\n", name)
+ for _, a := range tok.Attr {
+ if a.Name.Local == "viewBox" {
+ var p Points
+ if err := p.UnmarshalText([]byte(a.Value)); err != nil {
+ return fmt.Errorf("invalid viewBox attribute: %s", a.Value)
+ }
+ if len(p) != 4 {
+ return fmt.Errorf("invalid viewBox attribute: %s", a.Value)
+ }
+ fmt.Fprintf(w, "%s.ViewBox.Min = %s\n", name, point(f32.Pt(p[0], p[1])))
+ fmt.Fprintf(w, "%s.ViewBox.Max = %s\n", name, point(f32.Pt(p[2], p[3])))
+ }
+ }
+ return parseSVG(w, d)
+ }
+ }
+}
+
+func point(p f32.Point) string {
+ return fmt.Sprintf("f32.Pt(%g, %g)", p.X, p.Y)
+}
+
+type Poly struct {
+ XMLName xml.Name
+ Points Points `xml:"points,attr"`
+ Fill
+}
+
+func (p *Poly) Path(w io.Writer) error {
+ if len(p.Points) <= 1 {
+ return nil
+ }
+ pen := f32.Pt(p.Points[0], p.Points[1])
+ fmt.Fprintf(w, "p.MoveTo(%s)\n", point(pen))
+ last := pen
+ for i := 2; i < len(p.Points); i += 2 {
+ last = f32.Pt(p.Points[i], p.Points[i+1])
+ fmt.Fprintf(w, "p.LineTo(%s)\n", point(last))
+ }
+ if p.XMLName.Local == "polygon" && last != pen {
+ fmt.Fprintf(w, "p.LineTo(%s)\n", point(pen))
+ }
+ return nil
+}
+
+type Path struct {
+ D string `xml:"d,attr"`
+ Fill
+}
+
+func (p *Path) Path(w io.Writer) error {
+ return printPathCommands(w, p.D)
+}
+
+type Line struct {
+ X1 float32 `xml:"x1,attr"`
+ Y1 float32 `xml:"y1,attr"`
+ X2 float32 `xml:"x2,attr"`
+ Y2 float32 `xml:"y2,attr"`
+ Fill
+}
+
+func (l *Line) Path(w io.Writer) error {
+ fmt.Fprintf(w, "p.MoveTo(%s)\n", point(f32.Pt(l.X1, l.Y1)))
+ fmt.Fprintf(w, "p.LineTo(%s)\n", point(f32.Pt(l.X2, l.Y2)))
+ return nil
+}
+
+type Ellipse struct {
+ Cx float32 `xml:"cx,attr"`
+ Cy float32 `xml:"cy,attr"`
+ Rx float32 `xml:"rx,attr"`
+ Ry float32 `xml:"ry,attr"`
+ Fill
+}
+
+func (e *Ellipse) Path(w io.Writer) error {
+ c := f32.Pt(e.Cx, e.Cy)
+ r := f32.Pt(e.Rx, e.Ry)
+ fmt.Fprintf(w, "ellipse(&p, %s, %s)\n", point(c), point(r))
+ return nil
+}
+
+type Rect struct {
+ X float32 `xml:"x,attr"`
+ Y float32 `xml:"y,attr"`
+ Width float32 `xml:"width,attr"`
+ Height float32 `xml:"height,attr"`
+ Fill
+}
+
+func (r *Rect) Path(w io.Writer) error {
+ o := f32.Pt(r.X, r.Y)
+ sz := f32.Pt(r.Width, r.Height)
+ fmt.Fprintf(w, "rect(&p, %s, %s)\n", point(o), point(sz))
+ return nil
+}
+
+type Circle struct {
+ Cx float32 `xml:"cx,attr"`
+ Cy float32 `xml:"cy,attr"`
+ R float32 `xml:"r,attr"`
+ Fill
+}
+
+func (c *Circle) Path(w io.Writer) error {
+ center := f32.Pt(c.Cx, c.Cy)
+ r := f32.Pt(c.R, c.R)
+ fmt.Fprintf(w, "ellipse(&p, %s, %s)\n", point(center), point(r))
+ return nil
+}
+
+func parseSVG(w io.Writer, d *xml.Decoder) error {
+ for {
+ tok, err := d.Token()
+ if err != nil {
+ if err == io.EOF {
+ return errors.New("unexpected end of <svg> element")
+ }
+ return err
+ }
+ var start xml.StartElement
+ switch tok := tok.(type) {
+ case xml.EndElement:
+ return nil
+ case xml.StartElement:
+ start = tok
+ default:
+ continue
+ }
+ var elem interface {
+ Path(w io.Writer) error
+ }
+ var fill *Fill
+ switch n := start.Name.Local; n {
+ case "g":
+ // Flatten groups.
+ if err := parseSVG(w, d); err != nil {
+ return err
+ }
+ continue
+ case "title":
+ d.Skip()
+ continue
+ case "polygon", "polyline":
+ p := new(Poly)
+ elem = p
+ fill = &p.Fill
+ case "path":
+ p := new(Path)
+ elem = p
+ fill = &p.Fill
+ case "line":
+ l := new(Line)
+ elem = l
+ fill = &l.Fill
+ case "ellipse":
+ e := new(Ellipse)
+ elem = e
+ fill = &e.Fill
+ case "rect":
+ r := new(Rect)
+ elem = r
+ fill = &r.Fill
+ case "circle":
+ c := new(Circle)
+ elem = c
+ fill = &c.Fill
+ default:
+ return fmt.Errorf("unsupported tag: <%s>", n)
+ }
+ if err := d.DecodeElement(elem, &start); err != nil {
+ return err
+ }
+ if !fill.Fill.Set && !fill.Stroke.Set {
+ continue
+ }
+ fmt.Fprintf(w, "{\n")
+ trans := f32.Affine2D(fill.Transform)
+ if trans != (f32.Affine2D{}) {
+ sx, hx, ox, sy, hy, oy := trans.Elems()
+ fmt.Fprintf(w, "t := op.Affine(f32.NewAffine2D(%g, %g, %g, %g, %g, %g)).Push(&ops)\n", sx, hx, ox, sy, hy, oy)
+ }
+ fmt.Fprintf(w, "var p clip.Path\n")
+ fmt.Fprintf(w, "p.Begin(&ops)\n")
+ if err := elem.Path(w); err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "spec := p.End()\n")
+ if fill.Fill.Set {
+ fmt.Fprintf(w, "paint.FillShape(&ops, argb(%#.8x), clip.Outline{Path: spec}.Op())\n", fill.Fill.Value)
+ }
+ if fill.Stroke.Set {
+ fmt.Fprintf(w, "paint.FillShape(&ops, argb(%#.8x), clip.Stroke{Width: %g, Path: spec}.Op())\n", fill.Stroke.Value, fill.StrokeWidth)
+ }
+ if trans != (f32.Affine2D{}) {
+ fmt.Fprintf(w, "t.Pop()\n")
+ }
+ fmt.Fprintf(w, "}\n")
+ }
+}
+
+func printPathCommands(w io.Writer, cmds string) error {
+ moveTo := func(p f32.Point) {
+ fmt.Fprintf(w, "p.MoveTo(%s)\n", point(p))
+ }
+ lineTo := func(p f32.Point) {
+ fmt.Fprintf(w, "p.LineTo(%s)\n", point(p))
+ }
+ cubeTo := func(p0, p1, p2 f32.Point) {
+ fmt.Fprintf(w, "p.CubeTo(%s, %s, %s)\n", point(p0), point(p1), point(p2))
+ }
+ cmds = strings.TrimSpace(cmds)
+ var pen f32.Point
+ initPoint := pen
+ ctrl2 := pen
+ for {
+ cmds = strings.TrimLeft(cmds, " ,\t\n")
+ if len(cmds) == 0 {
+ break
+ }
+ orig := cmds
+ op := rune(cmds[0])
+ cmds = cmds[1:]
+ switch op {
+ case 'M', 'm', 'V', 'v', 'L', 'l', 'H', 'h', 'C', 'c', 'S', 's':
+ case 'Z', 'z':
+ if pen != initPoint {
+ lineTo(initPoint)
+ pen = initPoint
+ }
+ ctrl2 = initPoint
+ continue
+ default:
+ return fmt.Errorf("unknown <path> command %s in %q", string(op), orig)
+ }
+ var coords []float64
+ for {
+ cmds = strings.TrimLeft(cmds, " ,\t\n")
+ if len(cmds) == 0 {
+ break
+ }
+ n, x, ok := parseFloat(cmds)
+ if !ok {
+ break
+ }
+ cmds = cmds[n:]
+ coords = append(coords, x)
+ }
+ rel := unicode.IsLower(op)
+ newPen := pen
+ switch unicode.ToLower(op) {
+ case 'h':
+ for _, x := range coords {
+ p := f32.Pt(float32(x), pen.Y)
+ if rel {
+ p.X += pen.X
+ }
+ lineTo(p)
+ newPen = p
+ }
+ pen = newPen
+ ctrl2 = newPen
+ continue
+ case 'v':
+ for _, y := range coords {
+ p := f32.Pt(pen.X, float32(y))
+ if rel {
+ p.Y += pen.Y
+ }
+ lineTo(p)
+ newPen = p
+ }
+ pen = newPen
+ ctrl2 = newPen
+ continue
+ }
+ if len(coords)%2 != 0 {
+ return fmt.Errorf("odd number of coordinates in <path> data: %q", orig)
+ }
+ var off f32.Point
+ if rel {
+ // Relative command.
+ off = pen
+ } else {
+ off = f32.Pt(0, 0)
+ }
+ var points []f32.Point
+ for i := 0; i < len(coords); i += 2 {
+ p := f32.Pt(float32(coords[i]), float32(coords[i+1]))
+ p = p.Add(off)
+ points = append(points, p)
+ }
+ newCtrl2 := ctrl2
+ switch op := unicode.ToLower(op); op {
+ case 'm', 'l':
+ sop := moveTo
+ if op == 'l' {
+ sop = lineTo
+ }
+ for _, p := range points {
+ sop(p)
+ newPen = p
+ }
+ if op == 'm' {
+ initPoint = newPen
+ }
+ case 'c':
+ for i := 0; i < len(points); i += 3 {
+ p1, p2, p3 := points[i], points[i+1], points[i+2]
+ cubeTo(p1, p2, p3)
+ newPen = p3
+ newCtrl2 = p2
+ }
+ case 's':
+ for i := 0; i < len(points); i += 2 {
+ p2, p3 := points[i], points[i+1]
+ // Compute p1 by reflecting p2 on to the line that contains pen and p2.
+ p1 := pen.Mul(2).Sub(ctrl2)
+ cubeTo(p1, p2, p3)
+ newPen = p3
+ newCtrl2 = p2
+ }
+ }
+ pen = newPen
+ ctrl2 = newCtrl2
+ }
+ return nil
+}
+
+func parseFloat(s string) (int, float64, bool) {
+ n := 0
+ if len(s) > 0 && s[0] == '-' {
+ n++
+ }
+ for ; n < len(s); n++ {
+ if !(unicode.IsDigit(rune(s[n])) || s[n] == '.') {
+ break
+ }
+ }
+ f, err := strconv.ParseFloat(s[:n], 64)
+ return n, f, err == nil
+}
+
+const funcs = `
+func argb(c uint32) color.NRGBA {
+ return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)}
+}
+
+func rect(p *clip.Path, origin, size f32.Point) {
+ p.MoveTo(origin)
+ p.LineTo(origin.Add(f32.Pt(size.X, 0)))
+ p.LineTo(origin.Add(size))
+ p.LineTo(origin.Add(f32.Pt(0, size.Y)))
+ p.Close()
+}
+
+func ellipse(p *clip.Path, center, radius f32.Point) {
+ r := radius.X
+ // We'll model the ellipse as a circle scaled in the Y
+ // direction.
+ scale := radius.Y / r
+
+ // https://pomax.github.io/bezierinfo/#circles_cubic.
+ const q = 4 * (math.Sqrt2 - 1) / 3
+
+ curve := r * q
+ top := f32.Point{X: center.X, Y: center.Y - r*scale}
+
+ p.MoveTo(top)
+ p.CubeTo(
+ f32.Point{X: center.X + curve, Y: center.Y - r*scale},
+ f32.Point{X: center.X + r, Y: center.Y - curve*scale},
+ f32.Point{X: center.X + r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X + r, Y: center.Y + curve*scale},
+ f32.Point{X: center.X + curve, Y: center.Y + r*scale},
+ f32.Point{X: center.X, Y: center.Y + r*scale},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - curve, Y: center.Y + r*scale},
+ f32.Point{X: center.X - r, Y: center.Y + curve*scale},
+ f32.Point{X: center.X - r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - r, Y: center.Y - curve*scale},
+ f32.Point{X: center.X - curve, Y: center.Y - r*scale},
+ top,
+ )
+}
+`