public.go 7.7 KB

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