forked from joejulian/gio-cmd
cmd/svg2gio: add utility for converting SVG files to Gio
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+582
@@ -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,
|
||||
)
|
||||
}
|
||||
`
|
||||
Reference in New Issue
Block a user