public.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	"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}