From 02068d6340fc3427cb2a22fd25b2e73e0a08a2a5 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 16 Aug 2022 19:28:42 +0200 Subject: [PATCH] cmd/svg2gio: add utility for converting SVG files to Gio Signed-off-by: Elias Naur --- svg2gio/main.go | 582 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 svg2gio/main.go diff --git a/svg2gio/main.go b/svg2gio/main.go new file mode 100644 index 0000000..81a0227 --- /dev/null +++ b/svg2gio/main.go @@ -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 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 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 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, + ) +} +` -- 2.45.2