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}