icon_cursor.go

  1// Copyright 2017 The oksvg Authors. All rights reserved.
  2// created: 2/12/2017 by S.R.Wiley
  3//
  4// utils.go implements translation of an SVG2.0 path into a rasterx Path.
  5
  6package oksvg
  7
  8import (
  9	"encoding/xml"
 10	"errors"
 11	"fmt"
 12	"image/color"
 13	"log"
 14	"math"
 15	"strings"
 16
 17	"github.com/srwiley/rasterx"
 18)
 19
 20// IconCursor is used while parsing SVG files.
 21type IconCursor struct {
 22	PathCursor
 23	icon                                                 *SvgIcon
 24	StyleStack                                           []PathStyle
 25	grad                                                 *rasterx.Gradient
 26	inTitleText, inDescText, inGrad, inDefs, inDefsStyle bool
 27	currentDef                                           []definition
 28}
 29
 30// ReadGradURL reads an SVG format gradient url
 31// Since the context of the gradient can affect the colors
 32// the current fill or line color is passed in and used in
 33// the case of a nil stopClor value
 34func (c *IconCursor) ReadGradURL(v string, defaultColor interface{}) (grad rasterx.Gradient, ok bool) {
 35	if strings.HasPrefix(v, "url(") && strings.HasSuffix(v, ")") {
 36		urlStr := strings.TrimSpace(v[4 : len(v)-1])
 37		if strings.HasPrefix(urlStr, "#") {
 38			var g *rasterx.Gradient
 39			g, ok = c.icon.Grads[urlStr[1:]]
 40			if ok {
 41				grad = localizeGradIfStopClrNil(g, defaultColor)
 42			}
 43		}
 44	}
 45	return
 46}
 47
 48// ReadGradAttr reads an SVG gradient attribute
 49func (c *IconCursor) ReadGradAttr(attr xml.Attr) (err error) {
 50	switch attr.Name.Local {
 51	case "gradientTransform":
 52		c.grad.Matrix, err = c.parseTransform(attr.Value)
 53	case "gradientUnits":
 54		switch strings.TrimSpace(attr.Value) {
 55		case "userSpaceOnUse":
 56			c.grad.Units = rasterx.UserSpaceOnUse
 57		case "objectBoundingBox":
 58			c.grad.Units = rasterx.ObjectBoundingBox
 59		}
 60	case "spreadMethod":
 61		switch strings.TrimSpace(attr.Value) {
 62		case "pad":
 63			c.grad.Spread = rasterx.PadSpread
 64		case "reflect":
 65			c.grad.Spread = rasterx.ReflectSpread
 66		case "repeat":
 67			c.grad.Spread = rasterx.RepeatSpread
 68		}
 69	}
 70	return
 71}
 72
 73// PushStyle parses the style element, and push it on the style stack. Only color and opacity are supported
 74// for fill. Note that this parses both the contents of a style attribute plus
 75// direct fill and opacity attributes.
 76func (c *IconCursor) PushStyle(attrs []xml.Attr) error {
 77	var pairs []string
 78	className := ""
 79	for _, attr := range attrs {
 80		switch strings.ToLower(attr.Name.Local) {
 81		case "style":
 82			pairs = append(pairs, strings.Split(attr.Value, ";")...)
 83		case "class":
 84			className = attr.Value
 85		default:
 86			pairs = append(pairs, attr.Name.Local+":"+attr.Value)
 87		}
 88	}
 89	// Make a copy of the top style
 90	curStyle := c.StyleStack[len(c.StyleStack)-1]
 91	for _, pair := range pairs {
 92		kv := strings.Split(pair, ":")
 93		if len(kv) >= 2 {
 94			k := strings.ToLower(kv[0])
 95			k = strings.TrimSpace(k)
 96			v := strings.TrimSpace(kv[1])
 97			err := c.readStyleAttr(&curStyle, k, v)
 98			if err != nil {
 99				return err
100			}
101		}
102	}
103	c.adaptClasses(&curStyle, className)
104	c.StyleStack = append(c.StyleStack, curStyle) // Push style onto stack
105	return nil
106}
107
108func (c *IconCursor) readTransformAttr(m1 rasterx.Matrix2D, k string) (rasterx.Matrix2D, error) {
109	ln := len(c.points)
110	switch k {
111	case "rotate":
112		if ln == 1 {
113			m1 = m1.Rotate(c.points[0] * math.Pi / 180)
114		} else if ln == 3 {
115			m1 = m1.Translate(c.points[1], c.points[2]).
116				Rotate(c.points[0]*math.Pi/180).
117				Translate(-c.points[1], -c.points[2])
118		} else {
119			return m1, errParamMismatch
120		}
121	case "translate":
122		if ln == 1 {
123			m1 = m1.Translate(c.points[0], 0)
124		} else if ln == 2 {
125			m1 = m1.Translate(c.points[0], c.points[1])
126		} else {
127			return m1, errParamMismatch
128		}
129	case "skewx":
130		if ln == 1 {
131			m1 = m1.SkewX(c.points[0] * math.Pi / 180)
132		} else {
133			return m1, errParamMismatch
134		}
135	case "skewy":
136		if ln == 1 {
137			m1 = m1.SkewY(c.points[0] * math.Pi / 180)
138		} else {
139			return m1, errParamMismatch
140		}
141	case "scale":
142		if ln == 1 {
143			m1 = m1.Scale(c.points[0], 0)
144		} else if ln == 2 {
145			m1 = m1.Scale(c.points[0], c.points[1])
146		} else {
147			return m1, errParamMismatch
148		}
149	case "matrix":
150		if ln == 6 {
151			m1 = m1.Mult(rasterx.Matrix2D{
152				A: c.points[0],
153				B: c.points[1],
154				C: c.points[2],
155				D: c.points[3],
156				E: c.points[4],
157				F: c.points[5]})
158		} else {
159			return m1, errParamMismatch
160		}
161	default:
162		return m1, errParamMismatch
163	}
164	return m1, nil
165}
166
167func (c *IconCursor) parseTransform(v string) (rasterx.Matrix2D, error) {
168	ts := strings.Split(v, ")")
169	m1 := c.StyleStack[len(c.StyleStack)-1].mAdder.M
170	for _, t := range ts {
171		t = strings.TrimSpace(t)
172		if len(t) == 0 {
173			continue
174		}
175		d := strings.Split(t, "(")
176		if len(d) != 2 || len(d[1]) < 1 {
177			return m1, errParamMismatch // badly formed transformation
178		}
179		err := c.GetPoints(d[1])
180		if err != nil {
181			return m1, err
182		}
183		m1, err = c.readTransformAttr(m1, strings.ToLower(strings.TrimSpace(d[0])))
184		if err != nil {
185			return m1, err
186		}
187	}
188	return m1, nil
189}
190
191func (c *IconCursor) readStyleAttr(curStyle *PathStyle, k, v string) error {
192	switch k {
193	case "fill":
194		gradient, ok := c.ReadGradURL(v, curStyle.fillerColor)
195		if ok {
196			curStyle.fillerColor = gradient
197			break
198		}
199		var err error
200		curStyle.fillerColor, err = ParseSVGColor(v)
201		return err
202	case "stroke":
203		gradient, ok := c.ReadGradURL(v, curStyle.linerColor)
204		if ok {
205			curStyle.linerColor = gradient
206			break
207		}
208		col, errc := ParseSVGColor(v)
209		if errc != nil {
210			return errc
211		}
212		if col != nil {
213			curStyle.linerColor = col.(color.NRGBA)
214		} else {
215			curStyle.linerColor = nil
216		}
217	case "stroke-linegap":
218		switch v {
219		case "flat":
220			curStyle.LineGap = rasterx.FlatGap
221		case "round":
222			curStyle.LineGap = rasterx.RoundGap
223		case "cubic":
224			curStyle.LineGap = rasterx.CubicGap
225		case "quadratic":
226			curStyle.LineGap = rasterx.QuadraticGap
227		}
228	case "stroke-leadlinecap":
229		switch v {
230		case "butt":
231			curStyle.LeadLineCap = rasterx.ButtCap
232		case "round":
233			curStyle.LeadLineCap = rasterx.RoundCap
234		case "square":
235			curStyle.LeadLineCap = rasterx.SquareCap
236		case "cubic":
237			curStyle.LeadLineCap = rasterx.CubicCap
238		case "quadratic":
239			curStyle.LeadLineCap = rasterx.QuadraticCap
240		}
241	case "stroke-linecap":
242		switch v {
243		case "butt":
244			curStyle.LineCap = rasterx.ButtCap
245		case "round":
246			curStyle.LineCap = rasterx.RoundCap
247		case "square":
248			curStyle.LineCap = rasterx.SquareCap
249		case "cubic":
250			curStyle.LineCap = rasterx.CubicCap
251		case "quadratic":
252			curStyle.LineCap = rasterx.QuadraticCap
253		}
254	case "stroke-linejoin":
255		switch v {
256		case "miter":
257			curStyle.LineJoin = rasterx.Miter
258		case "miter-clip":
259			curStyle.LineJoin = rasterx.MiterClip
260		case "arc-clip":
261			curStyle.LineJoin = rasterx.ArcClip
262		case "round":
263			curStyle.LineJoin = rasterx.Round
264		case "arc":
265			curStyle.LineJoin = rasterx.Arc
266		case "bevel":
267			curStyle.LineJoin = rasterx.Bevel
268		}
269	case "stroke-miterlimit":
270		mLimit, err := parseFloat(v, 64)
271		if err != nil {
272			return err
273		}
274		curStyle.MiterLimit = mLimit
275	case "stroke-width":
276		width, err := parseFloat(v, 64)
277		if err != nil {
278			return err
279		}
280		curStyle.LineWidth = width
281	case "stroke-dashoffset":
282		dashOffset, err := parseFloat(v, 64)
283		if err != nil {
284			return err
285		}
286		curStyle.DashOffset = dashOffset
287	case "stroke-dasharray":
288		if v != "none" {
289			dashes := splitOnCommaOrSpace(v)
290			dList := make([]float64, len(dashes))
291			for i, dstr := range dashes {
292				d, err := parseFloat(strings.TrimSpace(dstr), 64)
293				if err != nil {
294					return err
295				}
296				dList[i] = d
297			}
298			curStyle.Dash = dList
299			break
300		}
301	case "opacity", "stroke-opacity", "fill-opacity":
302		op, err := parseFloat(v, 64)
303		if err != nil {
304			return err
305		}
306		if k != "stroke-opacity" {
307			curStyle.FillOpacity *= op
308		}
309		if k != "fill-opacity" {
310			curStyle.LineOpacity *= op
311		}
312	case "transform":
313		m, err := c.parseTransform(v)
314		if err != nil {
315			return err
316		}
317		curStyle.mAdder.M = m
318	}
319	return nil
320}
321
322func (c *IconCursor) readStartElement(se xml.StartElement) (err error) {
323	var skipDef bool
324	if se.Name.Local == "radialGradient" || se.Name.Local == "linearGradient" || c.inGrad {
325		skipDef = true
326	}
327	if c.inDefs && !skipDef {
328		ID := ""
329		for _, attr := range se.Attr {
330			if attr.Name.Local == "id" {
331				ID = attr.Value
332			}
333		}
334		if ID != "" && len(c.currentDef) > 0 {
335			c.icon.Defs[c.currentDef[0].ID] = c.currentDef
336			c.currentDef = make([]definition, 0)
337		}
338		c.currentDef = append(c.currentDef, definition{
339			ID:    ID,
340			Tag:   se.Name.Local,
341			Attrs: se.Attr,
342		})
343		return nil
344	}
345	df, ok := drawFuncs[se.Name.Local]
346	if !ok {
347		errStr := "Cannot process svg element " + se.Name.Local
348		if c.returnError(errStr) {
349			return errors.New(errStr)
350		}
351		return nil
352	}
353	err = df(c, se.Attr)
354	if err != nil {
355		e := fmt.Sprintf("error during processing svg element %s: %s", se.Name.Local, err.Error())
356		if c.returnError(e) {
357			err = errors.New(e)
358		}
359		err = nil
360	}
361
362	if len(c.Path) > 0 {
363		//The cursor parsed a path from the xml element
364		pathCopy := make(rasterx.Path, len(c.Path))
365		copy(pathCopy, c.Path)
366		c.icon.SVGPaths = append(c.icon.SVGPaths,
367			SvgPath{c.StyleStack[len(c.StyleStack)-1], pathCopy})
368		c.Path = c.Path[:0]
369	}
370	return
371}
372
373func (c *IconCursor) adaptClasses(pathStyle *PathStyle, className string) {
374	if className == "" || len(c.icon.classes) == 0 {
375		return
376	}
377	for k, v := range c.icon.classes[className] {
378		c.readStyleAttr(pathStyle, k, v)
379	}
380}
381
382func (c *IconCursor) returnError(errMsg string) bool {
383	if c.ErrorMode == StrictErrorMode {
384		return true
385	}
386	if c.ErrorMode == WarnErrorMode {
387		log.Println(errMsg)
388	}
389
390	return false
391}