392 lines
9.3 KiB
Go
392 lines
9.3 KiB
Go
// Copyright 2017 The oksvg Authors. All rights reserved.
|
|
// created: 2/12/2017 by S.R.Wiley
|
|
//
|
|
// utils.go implements translation of an SVG2.0 path into a rasterx Path.
|
|
|
|
package oksvg
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"log"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/srwiley/rasterx"
|
|
)
|
|
|
|
// IconCursor is used while parsing SVG files.
|
|
type IconCursor struct {
|
|
PathCursor
|
|
icon *SvgIcon
|
|
StyleStack []PathStyle
|
|
grad *rasterx.Gradient
|
|
inTitleText, inDescText, inGrad, inDefs, inDefsStyle bool
|
|
currentDef []definition
|
|
}
|
|
|
|
// ReadGradURL reads an SVG format gradient url
|
|
// Since the context of the gradient can affect the colors
|
|
// the current fill or line color is passed in and used in
|
|
// the case of a nil stopClor value
|
|
func (c *IconCursor) ReadGradURL(v string, defaultColor interface{}) (grad rasterx.Gradient, ok bool) {
|
|
if strings.HasPrefix(v, "url(") && strings.HasSuffix(v, ")") {
|
|
urlStr := strings.TrimSpace(v[4 : len(v)-1])
|
|
if strings.HasPrefix(urlStr, "#") {
|
|
var g *rasterx.Gradient
|
|
g, ok = c.icon.Grads[urlStr[1:]]
|
|
if ok {
|
|
grad = localizeGradIfStopClrNil(g, defaultColor)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ReadGradAttr reads an SVG gradient attribute
|
|
func (c *IconCursor) ReadGradAttr(attr xml.Attr) (err error) {
|
|
switch attr.Name.Local {
|
|
case "gradientTransform":
|
|
c.grad.Matrix, err = c.parseTransform(attr.Value)
|
|
case "gradientUnits":
|
|
switch strings.TrimSpace(attr.Value) {
|
|
case "userSpaceOnUse":
|
|
c.grad.Units = rasterx.UserSpaceOnUse
|
|
case "objectBoundingBox":
|
|
c.grad.Units = rasterx.ObjectBoundingBox
|
|
}
|
|
case "spreadMethod":
|
|
switch strings.TrimSpace(attr.Value) {
|
|
case "pad":
|
|
c.grad.Spread = rasterx.PadSpread
|
|
case "reflect":
|
|
c.grad.Spread = rasterx.ReflectSpread
|
|
case "repeat":
|
|
c.grad.Spread = rasterx.RepeatSpread
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// PushStyle parses the style element, and push it on the style stack. Only color and opacity are supported
|
|
// for fill. Note that this parses both the contents of a style attribute plus
|
|
// direct fill and opacity attributes.
|
|
func (c *IconCursor) PushStyle(attrs []xml.Attr) error {
|
|
var pairs []string
|
|
className := ""
|
|
for _, attr := range attrs {
|
|
switch strings.ToLower(attr.Name.Local) {
|
|
case "style":
|
|
pairs = append(pairs, strings.Split(attr.Value, ";")...)
|
|
case "class":
|
|
className = attr.Value
|
|
default:
|
|
pairs = append(pairs, attr.Name.Local+":"+attr.Value)
|
|
}
|
|
}
|
|
// Make a copy of the top style
|
|
curStyle := c.StyleStack[len(c.StyleStack)-1]
|
|
for _, pair := range pairs {
|
|
kv := strings.Split(pair, ":")
|
|
if len(kv) >= 2 {
|
|
k := strings.ToLower(kv[0])
|
|
k = strings.TrimSpace(k)
|
|
v := strings.TrimSpace(kv[1])
|
|
err := c.readStyleAttr(&curStyle, k, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
c.adaptClasses(&curStyle, className)
|
|
c.StyleStack = append(c.StyleStack, curStyle) // Push style onto stack
|
|
return nil
|
|
}
|
|
|
|
func (c *IconCursor) readTransformAttr(m1 rasterx.Matrix2D, k string) (rasterx.Matrix2D, error) {
|
|
ln := len(c.points)
|
|
switch k {
|
|
case "rotate":
|
|
if ln == 1 {
|
|
m1 = m1.Rotate(c.points[0] * math.Pi / 180)
|
|
} else if ln == 3 {
|
|
m1 = m1.Translate(c.points[1], c.points[2]).
|
|
Rotate(c.points[0]*math.Pi/180).
|
|
Translate(-c.points[1], -c.points[2])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "translate":
|
|
if ln == 1 {
|
|
m1 = m1.Translate(c.points[0], 0)
|
|
} else if ln == 2 {
|
|
m1 = m1.Translate(c.points[0], c.points[1])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "skewx":
|
|
if ln == 1 {
|
|
m1 = m1.SkewX(c.points[0] * math.Pi / 180)
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "skewy":
|
|
if ln == 1 {
|
|
m1 = m1.SkewY(c.points[0] * math.Pi / 180)
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "scale":
|
|
if ln == 1 {
|
|
m1 = m1.Scale(c.points[0], 0)
|
|
} else if ln == 2 {
|
|
m1 = m1.Scale(c.points[0], c.points[1])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "matrix":
|
|
if ln == 6 {
|
|
m1 = m1.Mult(rasterx.Matrix2D{
|
|
A: c.points[0],
|
|
B: c.points[1],
|
|
C: c.points[2],
|
|
D: c.points[3],
|
|
E: c.points[4],
|
|
F: c.points[5]})
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
default:
|
|
return m1, errParamMismatch
|
|
}
|
|
return m1, nil
|
|
}
|
|
|
|
func (c *IconCursor) parseTransform(v string) (rasterx.Matrix2D, error) {
|
|
ts := strings.Split(v, ")")
|
|
m1 := c.StyleStack[len(c.StyleStack)-1].mAdder.M
|
|
for _, t := range ts {
|
|
t = strings.TrimSpace(t)
|
|
if len(t) == 0 {
|
|
continue
|
|
}
|
|
d := strings.Split(t, "(")
|
|
if len(d) != 2 || len(d[1]) < 1 {
|
|
return m1, errParamMismatch // badly formed transformation
|
|
}
|
|
err := c.GetPoints(d[1])
|
|
if err != nil {
|
|
return m1, err
|
|
}
|
|
m1, err = c.readTransformAttr(m1, strings.ToLower(strings.TrimSpace(d[0])))
|
|
if err != nil {
|
|
return m1, err
|
|
}
|
|
}
|
|
return m1, nil
|
|
}
|
|
|
|
func (c *IconCursor) readStyleAttr(curStyle *PathStyle, k, v string) error {
|
|
switch k {
|
|
case "fill":
|
|
gradient, ok := c.ReadGradURL(v, curStyle.fillerColor)
|
|
if ok {
|
|
curStyle.fillerColor = gradient
|
|
break
|
|
}
|
|
var err error
|
|
curStyle.fillerColor, err = ParseSVGColor(v)
|
|
return err
|
|
case "stroke":
|
|
gradient, ok := c.ReadGradURL(v, curStyle.linerColor)
|
|
if ok {
|
|
curStyle.linerColor = gradient
|
|
break
|
|
}
|
|
col, errc := ParseSVGColor(v)
|
|
if errc != nil {
|
|
return errc
|
|
}
|
|
if col != nil {
|
|
curStyle.linerColor = col.(color.NRGBA)
|
|
} else {
|
|
curStyle.linerColor = nil
|
|
}
|
|
case "stroke-linegap":
|
|
switch v {
|
|
case "flat":
|
|
curStyle.LineGap = rasterx.FlatGap
|
|
case "round":
|
|
curStyle.LineGap = rasterx.RoundGap
|
|
case "cubic":
|
|
curStyle.LineGap = rasterx.CubicGap
|
|
case "quadratic":
|
|
curStyle.LineGap = rasterx.QuadraticGap
|
|
}
|
|
case "stroke-leadlinecap":
|
|
switch v {
|
|
case "butt":
|
|
curStyle.LeadLineCap = rasterx.ButtCap
|
|
case "round":
|
|
curStyle.LeadLineCap = rasterx.RoundCap
|
|
case "square":
|
|
curStyle.LeadLineCap = rasterx.SquareCap
|
|
case "cubic":
|
|
curStyle.LeadLineCap = rasterx.CubicCap
|
|
case "quadratic":
|
|
curStyle.LeadLineCap = rasterx.QuadraticCap
|
|
}
|
|
case "stroke-linecap":
|
|
switch v {
|
|
case "butt":
|
|
curStyle.LineCap = rasterx.ButtCap
|
|
case "round":
|
|
curStyle.LineCap = rasterx.RoundCap
|
|
case "square":
|
|
curStyle.LineCap = rasterx.SquareCap
|
|
case "cubic":
|
|
curStyle.LineCap = rasterx.CubicCap
|
|
case "quadratic":
|
|
curStyle.LineCap = rasterx.QuadraticCap
|
|
}
|
|
case "stroke-linejoin":
|
|
switch v {
|
|
case "miter":
|
|
curStyle.LineJoin = rasterx.Miter
|
|
case "miter-clip":
|
|
curStyle.LineJoin = rasterx.MiterClip
|
|
case "arc-clip":
|
|
curStyle.LineJoin = rasterx.ArcClip
|
|
case "round":
|
|
curStyle.LineJoin = rasterx.Round
|
|
case "arc":
|
|
curStyle.LineJoin = rasterx.Arc
|
|
case "bevel":
|
|
curStyle.LineJoin = rasterx.Bevel
|
|
}
|
|
case "stroke-miterlimit":
|
|
mLimit, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.MiterLimit = mLimit
|
|
case "stroke-width":
|
|
width, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.LineWidth = width
|
|
case "stroke-dashoffset":
|
|
dashOffset, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.DashOffset = dashOffset
|
|
case "stroke-dasharray":
|
|
if v != "none" {
|
|
dashes := splitOnCommaOrSpace(v)
|
|
dList := make([]float64, len(dashes))
|
|
for i, dstr := range dashes {
|
|
d, err := parseFloat(strings.TrimSpace(dstr), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dList[i] = d
|
|
}
|
|
curStyle.Dash = dList
|
|
break
|
|
}
|
|
case "opacity", "stroke-opacity", "fill-opacity":
|
|
op, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if k != "stroke-opacity" {
|
|
curStyle.FillOpacity *= op
|
|
}
|
|
if k != "fill-opacity" {
|
|
curStyle.LineOpacity *= op
|
|
}
|
|
case "transform":
|
|
m, err := c.parseTransform(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.mAdder.M = m
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *IconCursor) readStartElement(se xml.StartElement) (err error) {
|
|
var skipDef bool
|
|
if se.Name.Local == "radialGradient" || se.Name.Local == "linearGradient" || c.inGrad {
|
|
skipDef = true
|
|
}
|
|
if c.inDefs && !skipDef {
|
|
ID := ""
|
|
for _, attr := range se.Attr {
|
|
if attr.Name.Local == "id" {
|
|
ID = attr.Value
|
|
}
|
|
}
|
|
if ID != "" && len(c.currentDef) > 0 {
|
|
c.icon.Defs[c.currentDef[0].ID] = c.currentDef
|
|
c.currentDef = make([]definition, 0)
|
|
}
|
|
c.currentDef = append(c.currentDef, definition{
|
|
ID: ID,
|
|
Tag: se.Name.Local,
|
|
Attrs: se.Attr,
|
|
})
|
|
return nil
|
|
}
|
|
df, ok := drawFuncs[se.Name.Local]
|
|
if !ok {
|
|
errStr := "Cannot process svg element " + se.Name.Local
|
|
if c.returnError(errStr) {
|
|
return errors.New(errStr)
|
|
}
|
|
return nil
|
|
}
|
|
err = df(c, se.Attr)
|
|
if err != nil {
|
|
e := fmt.Sprintf("error during processing svg element %s: %s", se.Name.Local, err.Error())
|
|
if c.returnError(e) {
|
|
err = errors.New(e)
|
|
}
|
|
err = nil
|
|
}
|
|
|
|
if len(c.Path) > 0 {
|
|
//The cursor parsed a path from the xml element
|
|
pathCopy := make(rasterx.Path, len(c.Path))
|
|
copy(pathCopy, c.Path)
|
|
c.icon.SVGPaths = append(c.icon.SVGPaths,
|
|
SvgPath{c.StyleStack[len(c.StyleStack)-1], pathCopy})
|
|
c.Path = c.Path[:0]
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *IconCursor) adaptClasses(pathStyle *PathStyle, className string) {
|
|
if className == "" || len(c.icon.classes) == 0 {
|
|
return
|
|
}
|
|
for k, v := range c.icon.classes[className] {
|
|
c.readStyleAttr(pathStyle, k, v)
|
|
}
|
|
}
|
|
|
|
func (c *IconCursor) returnError(errMsg string) bool {
|
|
if c.ErrorMode == StrictErrorMode {
|
|
return true
|
|
}
|
|
if c.ErrorMode == WarnErrorMode {
|
|
log.Println(errMsg)
|
|
}
|
|
|
|
return false
|
|
}
|