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 "errors"
10 "image/color"
11 "strconv"
12 "strings"
13
14 "github.com/srwiley/rasterx"
15 "golang.org/x/image/colornames"
16)
17
18// unitSuffixes are suffixes sometimes applied to the width and height attributes
19// of the svg element.
20var unitSuffixes = []string{"cm", "mm", "px", "pt"}
21
22func parseColorValue(v string) (uint8, error) {
23 if v[len(v)-1] == '%' {
24 n, err := strconv.Atoi(strings.TrimSpace(v[:len(v)-1]))
25 if err != nil {
26 return 0, err
27 }
28 return uint8(n * 0xFF / 100), nil
29 }
30 n, err := strconv.Atoi(strings.TrimSpace(v))
31 if n > 255 {
32 n = 255
33 }
34 return uint8(n), err
35}
36
37// trimSuffixes removes unitSuffixes from any number that is not just numeric
38func trimSuffixes(a string) (b string) {
39 if a == "" || (a[len(a)-1] >= '0' && a[len(a)-1] <= '9') {
40 return a
41 }
42 b = a
43 for _, v := range unitSuffixes {
44 b = strings.TrimSuffix(b, v)
45 }
46 return
47}
48
49// parseFloat is a helper function that strips suffixes before passing to strconv.ParseFloat
50func parseFloat(s string, bitSize int) (float64, error) {
51 val := trimSuffixes(s)
52 return strconv.ParseFloat(val, bitSize)
53}
54
55// splitOnCommaOrSpace returns a list of strings after splitting the input on comma and space delimiters
56func splitOnCommaOrSpace(s string) []string {
57 return strings.FieldsFunc(s,
58 func(r rune) bool {
59 return r == ',' || r == ' '
60 })
61}
62
63func parseClasses(data string) (map[string]styleAttribute, error) {
64 res := map[string]styleAttribute{}
65 arr := strings.Split(data, "}")
66 for _, v := range arr {
67 v = strings.TrimSpace(v)
68 if v == "" {
69 continue
70 }
71 valueIndex := strings.Index(v, "{")
72 if valueIndex == -1 || valueIndex == len(v)-1 {
73 return res, errors.New(v + "}: invalid map format in class definitions")
74 }
75 classesStr := v[:valueIndex]
76 attrStr := v[valueIndex+1:]
77 attrMap, err := parseAttrs(attrStr)
78 if err != nil {
79 return res, err
80 }
81 classes := strings.Split(classesStr, ",")
82 for _, class := range classes {
83 class = strings.TrimSpace(class)
84 if len(class) > 0 && class[0] == '.' {
85 class = class[1:]
86 }
87 for attrKey, attrVal := range attrMap {
88 if res[class] == nil {
89 res[class] = make(styleAttribute, len(attrMap))
90 }
91 res[class][attrKey] = attrVal
92 }
93 }
94 }
95 return res, nil
96}
97
98func parseAttrs(attrStr string) (styleAttribute, error) {
99 arr := strings.Split(attrStr, ";")
100 res := make(styleAttribute, len(arr))
101 for _, kv := range arr {
102 kv = strings.TrimSpace(kv)
103 if kv == "" {
104 continue
105 }
106 tmp := strings.SplitN(kv, ":", 2)
107 if len(tmp) != 2 {
108 return res, errors.New(kv + ": invalid attribute format")
109 }
110 k := strings.TrimSpace(tmp[0])
111 v := strings.TrimSpace(tmp[1])
112 res[k] = v
113 }
114 return res, nil
115}
116
117func readFraction(v string) (f float64, err error) {
118 v = strings.TrimSpace(v)
119 d := 1.0
120 if strings.HasSuffix(v, "%") {
121 d = 100
122 v = strings.TrimSuffix(v, "%")
123 }
124 f, err = parseFloat(v, 64)
125 f /= d
126 // Is this is an unnecessary restriction? For now fractions can be all values not just in the range [0,1]
127 // if f > 1 {
128 // f = 1
129 // } else if f < 0 {
130 // f = 0
131 // }
132 return
133}
134
135// getColor is a helper function to get the background color
136// if ReadGradUrl needs it.
137func getColor(clr interface{}) color.Color {
138 switch c := clr.(type) {
139 case rasterx.Gradient: // This is a bit lazy but oh well
140 for _, s := range c.Stops {
141 if s.StopColor != nil {
142 return s.StopColor
143 }
144 }
145 case color.NRGBA:
146 return c
147 }
148 return colornames.Black
149}
150
151func localizeGradIfStopClrNil(g *rasterx.Gradient, defaultColor interface{}) (grad rasterx.Gradient) {
152 grad = *g
153 for _, s := range grad.Stops {
154 if s.StopColor == nil { // This means we need copy the gradient's Stop slice
155 // and fill in the default color
156
157 // Copy the stops
158 stops := make([]rasterx.GradStop, len(grad.Stops))
159 copy(stops, grad.Stops)
160 grad.Stops = stops
161 // Use the background color when a stop color is nil
162 clr := getColor(defaultColor)
163 for i, s := range stops {
164 if s.StopColor == nil {
165 grad.Stops[i].StopColor = clr
166 }
167 }
168 break // Only need to do this once
169 }
170 }
171 return
172}