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 "bytes"
10 "encoding/xml"
11 "fmt"
12 "image/color"
13 "io"
14 "io/ioutil"
15 "math"
16 "os"
17 "strconv"
18 "strings"
19
20 "github.com/srwiley/rasterx"
21 "golang.org/x/image/colornames"
22 "golang.org/x/net/html/charset"
23)
24
25// ReadIconStream reads the Icon from the given io.Reader.
26// This only supports a sub-set of SVG, but
27// is enough to draw many icons. If errMode is provided,
28// the first value determines if the icon ignores, errors out, or logs a warning
29// if it does not handle an element found in the icon file. Ignore warnings is
30// the default if no ErrorMode value is provided.
31func ReadIconStream(stream io.Reader, errMode ...ErrorMode) (*SvgIcon, error) {
32 icon := &SvgIcon{Defs: make(map[string][]definition), Grads: make(map[string]*rasterx.Gradient), Transform: rasterx.Identity}
33 cursor := &IconCursor{StyleStack: []PathStyle{DefaultStyle}, icon: icon}
34 if len(errMode) > 0 {
35 cursor.ErrorMode = errMode[0]
36 }
37 classInfo := ""
38 decoder := xml.NewDecoder(stream)
39 decoder.CharsetReader = charset.NewReaderLabel
40 for {
41 t, err := decoder.Token()
42 if err != nil {
43 if err == io.EOF {
44 break
45 }
46 return icon, err
47 }
48 // Inspect the type of the XML token
49 switch se := t.(type) {
50 case xml.StartElement:
51 // Reads all recognized style attributes from the start element
52 // and places it on top of the styleStack
53 err = cursor.PushStyle(se.Attr)
54 if err != nil {
55 return icon, err
56 }
57 err = cursor.readStartElement(se)
58 if err != nil {
59 return icon, err
60 }
61 if se.Name.Local == "style" && cursor.inDefs {
62 cursor.inDefsStyle = true
63 }
64 case xml.EndElement:
65 // pop style
66 cursor.StyleStack = cursor.StyleStack[:len(cursor.StyleStack)-1]
67 switch se.Name.Local {
68 case "g":
69 if cursor.inDefs {
70 cursor.currentDef = append(cursor.currentDef, definition{
71 Tag: "endg",
72 })
73 }
74 case "title":
75 cursor.inTitleText = false
76 case "desc":
77 cursor.inDescText = false
78 case "defs":
79 if len(cursor.currentDef) > 0 {
80 cursor.icon.Defs[cursor.currentDef[0].ID] = cursor.currentDef
81 cursor.currentDef = make([]definition, 0)
82 }
83 cursor.inDefs = false
84 case "radialGradient", "linearGradient":
85 cursor.inGrad = false
86
87 case "style":
88 if cursor.inDefsStyle {
89 icon.classes, err = parseClasses(classInfo)
90 if err != nil {
91 return icon, err
92 }
93 cursor.inDefsStyle = false
94 }
95 }
96 case xml.CharData:
97 if cursor.inTitleText {
98 icon.Titles[len(icon.Titles)-1] += string(se)
99 }
100 if cursor.inDescText {
101 icon.Descriptions[len(icon.Descriptions)-1] += string(se)
102 }
103 if cursor.inDefsStyle {
104 classInfo = string(se)
105 }
106 }
107 }
108 return icon, nil
109}
110
111// ReadReplacingCurrentColor replaces currentColor value with specified value and loads SvgIcon as ReadIconStream do.
112// currentColor value should be valid hex, rgb or named color value.
113func ReadReplacingCurrentColor(stream io.Reader, currentColor string, errMode ...ErrorMode) (icon *SvgIcon, err error) {
114 var (
115 data []byte
116 )
117
118 if data, err = ioutil.ReadAll(stream); err != nil {
119 return nil, fmt.Errorf("%w: read data: %v", errParamMismatch, err)
120 }
121
122 if currentColor != "" && strings.Contains(string(data), "currentColor") {
123 data = []byte(strings.ReplaceAll(string(data), "currentColor", currentColor))
124 }
125
126 if icon, err = ReadIconStream(bytes.NewBuffer(data), errMode...); err != nil {
127 return nil, fmt.Errorf("%w: load: %v", errParamMismatch, err)
128 }
129
130 return icon, nil
131}
132
133// ReadIcon reads the Icon from the named file.
134// This only supports a sub-set of SVG, but is enough to draw many icons.
135// If errMode is provided, the first value determines if the icon ignores, errors out, or logs a warning
136// if it does not handle an element found in the icon file.
137// Ignore warnings is the default if no ErrorMode value is provided.
138func ReadIcon(iconFile string, errMode ...ErrorMode) (*SvgIcon, error) {
139 fin, errf := os.Open(iconFile)
140 if errf != nil {
141 return nil, errf
142 }
143 defer fin.Close()
144 return ReadIconStream(fin, errMode...)
145}
146
147// ParseSVGColorNum reads the SFG color string e.g. #FBD9BD
148func ParseSVGColorNum(colorStr string) (r, g, b uint8, err error) {
149 colorStr = strings.TrimPrefix(colorStr, "#")
150 var t uint64
151 if len(colorStr) != 6 {
152 if len(colorStr) != 3 {
153 err = fmt.Errorf("color string %s is not length 3 or 6 as required by SVG specification",
154 colorStr)
155 return
156 }
157 // SVG specs say duplicate characters in case of 3 digit hex number
158 colorStr = string([]byte{colorStr[0], colorStr[0],
159 colorStr[1], colorStr[1], colorStr[2], colorStr[2]})
160 }
161 for _, v := range []struct {
162 c *uint8
163 s string
164 }{
165 {&r, colorStr[0:2]},
166 {&g, colorStr[2:4]},
167 {&b, colorStr[4:6]}} {
168 t, err = strconv.ParseUint(v.s, 16, 8)
169 if err != nil {
170 return
171 }
172 *v.c = uint8(t)
173 }
174 return
175}
176
177// ParseSVGColor parses an SVG color string in all forms
178// including all SVG1.1 names, obtained from the image.colornames package
179func ParseSVGColor(colorStr string) (color.Color, error) {
180 // _, _, _, a := curColor.RGBA()
181 v := strings.ToLower(colorStr)
182 if strings.HasPrefix(v, "url") { // We are not handling urls
183 // and gradients and stuff at this point
184 return color.NRGBA{0, 0, 0, 255}, nil
185 }
186 switch v {
187 case "none", "":
188 // nil signals that the function (fill or stroke) is off;
189 // not the same as black
190 return nil, nil
191 default:
192 cn, ok := colornames.Map[v]
193 if ok {
194 r, g, b, a := cn.RGBA()
195 return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}, nil
196 }
197 }
198 cStr := strings.TrimPrefix(colorStr, "rgb(")
199 if cStr != colorStr {
200 cStr := strings.TrimSuffix(cStr, ")")
201 vals := strings.Split(cStr, ",")
202 if len(vals) != 3 {
203 return color.NRGBA{}, errParamMismatch
204 }
205 var cvals [3]uint8
206 var err error
207 for i := range cvals {
208 cvals[i], err = parseColorValue(vals[i])
209 if err != nil {
210 return nil, err
211 }
212 }
213 return color.NRGBA{cvals[0], cvals[1], cvals[2], 0xFF}, nil
214 }
215
216 cStr = strings.TrimPrefix(colorStr, "hsl(")
217 if cStr != colorStr {
218 cStr := strings.TrimSuffix(cStr, ")")
219 vals := strings.Split(cStr, ",")
220 if len(vals) != 3 {
221 return color.NRGBA{}, errParamMismatch
222 }
223
224 H, err := strconv.ParseInt(strings.TrimSpace(vals[0]), 10, 64)
225 if err != nil {
226 return color.NRGBA{}, fmt.Errorf("invalid hue in hsl: '%s' (%s)", vals[0], err)
227 }
228
229 S, err := strconv.ParseFloat(strings.TrimSpace(vals[1][:len(vals[1])-1]), 64)
230 if err != nil {
231 return color.NRGBA{}, fmt.Errorf("invalid saturation in hsl: '%s' (%s)", vals[1], err)
232 }
233 S = S / 100
234
235 L, err := strconv.ParseFloat(strings.TrimSpace(vals[2][:len(vals[2])-1]), 64)
236 if err != nil {
237 return color.NRGBA{}, fmt.Errorf("invalid lightness in hsl: '%s' (%s)", vals[2], err)
238 }
239 L = L / 100
240
241 C := (1 - math.Abs((2*L)-1)) * S
242 X := C * (1 - math.Abs(math.Mod((float64(H)/60), 2)-1))
243 m := L - C/2
244
245 var rp, gp, bp float64
246 if H < 60 {
247 rp, gp, bp = float64(C), float64(X), float64(0)
248 } else if H < 120 {
249 rp, gp, bp = float64(X), float64(C), float64(0)
250 } else if H < 180 {
251 rp, gp, bp = float64(0), float64(C), float64(X)
252 } else if H < 240 {
253 rp, gp, bp = float64(0), float64(X), float64(C)
254 } else if H < 300 {
255 rp, gp, bp = float64(X), float64(0), float64(C)
256 } else {
257 rp, gp, bp = float64(C), float64(0), float64(X)
258 }
259
260 r, g, b := math.Round((rp+m)*255), math.Round((gp+m)*255), math.Round((bp+m)*255)
261 if r > 255 {
262 r = 255
263 }
264 if g > 255 {
265 g = 255
266 }
267 if b > 255 {
268 b = 255
269 }
270
271 return color.NRGBA{
272 uint8(r),
273 uint8(g),
274 uint8(b),
275 0xFF,
276 }, nil
277 }
278
279 if colorStr[0] == '#' {
280 r, g, b, err := ParseSVGColorNum(colorStr)
281 if err != nil {
282 return nil, err
283 }
284 return color.NRGBA{r, g, b, 0xFF}, nil
285 }
286 return nil, errParamMismatch
287}