Skip to content

Parse Paths Separately From Full SVG #326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 79 additions & 34 deletions svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -810,56 +810,56 @@ func (svg *svgParser) getFontFace() *FontFace {
return fontFamily.Face(fontSize, svg.ctx.Style.Fill.Color)
}

func (svg *svgParser) drawShape(tag string, attrs map[string]string) {
func (svg *svgParser) toPath(tag string, attrs map[string]string) (x float64, y float64, path *Path) {
switch tag {
case "circle":
cx := svg.parseDimension(attrs["cx"], svg.width)
cy := svg.parseDimension(attrs["cy"], svg.height)
r := svg.parseDimension(attrs["r"], svg.diagonal)
svg.ctx.DrawPath(cx, cy, Circle(r))
x = svg.parseDimension(attrs["cx"], svg.width)
y = svg.parseDimension(attrs["cy"], svg.height)
path = Circle(
svg.parseDimension(attrs["r"], svg.diagonal),
)
case "ellipse":
cx := svg.parseDimension(attrs["cx"], svg.width)
cy := svg.parseDimension(attrs["cy"], svg.height)
rx := svg.parseDimension(attrs["rx"], svg.width)
ry := svg.parseDimension(attrs["ry"], svg.height)
svg.ctx.DrawPath(cx, cy, Ellipse(rx, ry))
x = svg.parseDimension(attrs["cx"], svg.width)
y = svg.parseDimension(attrs["cy"], svg.height)
path = Ellipse(
svg.parseDimension(attrs["rx"], svg.width),
svg.parseDimension(attrs["ry"], svg.height),
)
case "path":
p, err := ParseSVGPath(attrs["d"])
var err error
path, err = ParseSVGPath(attrs["d"])
if err != nil && svg.err == nil {
svg.err = parse.NewErrorLexer(svg.z, "bad path: %w", err)
}
svg.ctx.DrawPath(0, 0, p)
case "polygon", "polyline":
path = &Path{}
points := svg.parsePoints(attrs["points"])
p := &Path{}
for i := 0; i+1 < len(points); i += 2 {
if i == 0 {
p.MoveTo(points[0], points[1])
path.MoveTo(points[0], points[1])
} else {
p.LineTo(points[i], points[i+1])
path.LineTo(points[i], points[i+1])
}
}
if tag == "polygon" {
p.Close()
path.Close()
}
svg.ctx.DrawPath(0.0, 0.0, p)
case "line":
p := &Path{}
x1 := svg.parseDimension(attrs["x1"], svg.width)
y1 := svg.parseDimension(attrs["y1"], svg.height)
x2 := svg.parseDimension(attrs["x2"], svg.width)
y2 := svg.parseDimension(attrs["y2"], svg.height)

p.MoveTo(x1, y1)
p.LineTo(x2, y2)
svg.ctx.DrawPath(0.0, 0.0, p)
path = &Path{}
path.MoveTo(x1, y1)
path.LineTo(x2, y2)
case "rect":
x := svg.parseDimension(attrs["x"], svg.width)
y := svg.parseDimension(attrs["y"], svg.height)
x = svg.parseDimension(attrs["x"], svg.width)
y = svg.parseDimension(attrs["y"], svg.height)
width := svg.parseDimension(attrs["width"], svg.width)
height := svg.parseDimension(attrs["height"], svg.height)
path = &Path{}
if attrs["rx"] == "" && attrs["ry"] == "" {
svg.ctx.DrawPath(x, y, Rectangle(width, height))
path = Rectangle(width, height)
} else {
// TODO: handle both rx and ry
var r float64
Expand All @@ -868,19 +868,42 @@ func (svg *svgParser) drawShape(tag string, attrs map[string]string) {
} else {
r = svg.parseDimension(attrs["ry"], svg.height)
}
svg.ctx.DrawPath(x, y, RoundedRectangle(width, height, r))
path = RoundedRectangle(width, height, r)
}
case "text":
svg.state.textX = svg.parseDimension(attrs["x"], svg.width)
svg.state.textY = svg.parseDimension(attrs["y"], svg.height)
}

return
}

func (svg *svgParser) drawShape(tag string, attrs map[string]string) {
svg.ctx.DrawPath(svg.toPath(tag, attrs))
}

type SVGPath struct {
Tag string
Attrs map[string]string
X, Y float64
*Path
}

func ParseSVG(r io.Reader) (*Canvas, error) {
cvs, _, err := parseSVGFull(r)
return cvs, err
}

func ParseSVGWithPaths(r io.Reader) (*Canvas, []SVGPath, error) {
return parseSVGFull(r)
}

func parseSVGFull(r io.Reader) (*Canvas, []SVGPath, error) {
z := parse.NewInput(r)
defer z.Restore()

l := xml.NewLexer(z)
var paths []SVGPath
svg := svgParser{
z: z,
defs: map[string]svgDef{},
Expand All @@ -892,16 +915,16 @@ func ParseSVG(r io.Reader) (*Canvas, error) {
switch tt {
case xml.ErrorToken:
if l.Err() != io.EOF {
return svg.c, l.Err()
return svg.c, paths, l.Err()
} else if svg.err != nil {
return svg.c, svg.err
return svg.c, paths, svg.err
} else if svg.c == nil {
return svg.c, fmt.Errorf("expected SVG tag")
return svg.c, paths, fmt.Errorf("expected SVG tag")
}
if svg.c.W == 0.0 || svg.c.H == 0.0 {
svg.c.Fit(0.0)
}
return svg.c, nil
return svg.c, paths, nil
case xml.StartTagToken:
tag := string(data[1:])
tt, attrNames, attrs := svg.parseAttributes(l)
Expand All @@ -911,7 +934,7 @@ func ParseSVG(r io.Reader) (*Canvas, error) {
width, height, viewbox := svg.parseViewBox(attrs["width"], attrs["height"], attrs["viewBox"])
svg.init(width, height, viewbox)
} else if tag != "svg" && svg.c == nil {
return svg.c, fmt.Errorf("expected SVG tag")
return svg.c, paths, fmt.Errorf("expected SVG tag")
}

// handle special tags
Expand All @@ -921,7 +944,7 @@ func ParseSVG(r io.Reader) (*Canvas, error) {
svg.parseStyle(data)
tt, data = l.Next() // end token
} else {
return svg.c, fmt.Errorf("bad style tag")
return svg.c, paths, fmt.Errorf("bad style tag")
}
break
} else if tag == "defs" {
Expand All @@ -941,8 +964,30 @@ func ParseSVG(r io.Reader) (*Canvas, error) {
}
svg.setStyling(props)

// draw shapes such as circles, paths, etc.
svg.drawShape(tag, attrs)
pathX, pathY, path := svg.toPath(tag, attrs)
if path != nil {
// draw shapes such as circles, paths, etc.
svg.ctx.DrawPath(pathX, pathY, path)

// Copy tag attributes map, excluding `d` which is where
// path data is stored. Since the path is already returned as
// `*Path`, there's not much point to returning `d`, and for
// large/many paths it can be wasteful of memory to return it.
attrsNoD := map[string]string{}
for key, value := range attrs {
if key != "d" {
attrsNoD[key] = value
}
}

paths = append(paths, SVGPath{
Tag: tag,
Attrs: attrsNoD,
X: pathX,
Y: pathY,
Path: path,
})
}

// set linearGradient, markers, etc.
// these defs depend on the shape or size of the path
Expand Down
Loading