svg.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package svg
  2. import (
  3. "bytes"
  4. "encoding/hex"
  5. "encoding/xml"
  6. "errors"
  7. "fmt"
  8. "image"
  9. "image/color"
  10. "io"
  11. "io/ioutil"
  12. "strconv"
  13. "github.com/srwiley/oksvg"
  14. "github.com/srwiley/rasterx"
  15. "fyne.io/fyne/v2"
  16. col "fyne.io/fyne/v2/internal/color"
  17. )
  18. // Colorize creates a new SVG from a given one by replacing all fill colors by the given color.
  19. func Colorize(src []byte, clr color.Color) []byte {
  20. rdr := bytes.NewReader(src)
  21. s, err := svgFromXML(rdr)
  22. if err != nil {
  23. fyne.LogError("could not load SVG, falling back to static content:", err)
  24. return src
  25. }
  26. if err := s.replaceFillColor(clr); err != nil {
  27. fyne.LogError("could not replace fill color, falling back to static content:", err)
  28. return src
  29. }
  30. colorized, err := xml.Marshal(s)
  31. if err != nil {
  32. fyne.LogError("could not marshal svg, falling back to static content:", err)
  33. return src
  34. }
  35. return colorized
  36. }
  37. // ToImage reads an SVG from an io.Reader and renders it into an image.NRGBA using the requested width and height.
  38. // The optional `validateSize` callback can be used to cancel the rendering depending on the SVG’s original size.
  39. // In this case `nil` is returned.
  40. func ToImage(file io.Reader, width, height int, validateSize func(origW, origH int) bool) (*image.NRGBA, error) {
  41. icon, err := oksvg.ReadIconStream(file)
  42. if err != nil {
  43. return nil, fmt.Errorf("SVG Load error: %w", err)
  44. }
  45. origW, origH := int(icon.ViewBox.W), int(icon.ViewBox.H)
  46. if validateSize != nil && !validateSize(origW, origH) {
  47. return nil, nil
  48. }
  49. aspect := float32(origW) / float32(origH)
  50. viewAspect := float32(width) / float32(height)
  51. imgW, imgH := width, height
  52. if viewAspect > aspect {
  53. imgW = int(float32(height) * aspect)
  54. } else if viewAspect < aspect {
  55. imgH = int(float32(width) / aspect)
  56. }
  57. icon.SetTarget(0, 0, float64(imgW), float64(imgH))
  58. img := image.NewNRGBA(image.Rect(0, 0, imgW, imgH))
  59. scanner := rasterx.NewScannerGV(origW, origH, img, img.Bounds())
  60. raster := rasterx.NewDasher(width, height, scanner)
  61. err = drawSVGSafely(icon, raster)
  62. if err != nil {
  63. err = fmt.Errorf("SVG render error: %w", err)
  64. return nil, err
  65. }
  66. return img, nil
  67. }
  68. // svg holds the unmarshaled XML from a Scalable Vector Graphic
  69. type svg struct {
  70. XMLName xml.Name `xml:"svg"`
  71. XMLNS string `xml:"xmlns,attr"`
  72. Width string `xml:"width,attr"`
  73. Height string `xml:"height,attr"`
  74. ViewBox string `xml:"viewBox,attr,omitempty"`
  75. Paths []*pathObj `xml:"path"`
  76. Rects []*rectObj `xml:"rect"`
  77. Circles []*circleObj `xml:"circle"`
  78. Ellipses []*ellipseObj `xml:"ellipse"`
  79. Polygons []*polygonObj `xml:"polygon"`
  80. Groups []*objGroup `xml:"g"`
  81. }
  82. type pathObj struct {
  83. XMLName xml.Name `xml:"path"`
  84. Fill string `xml:"fill,attr,omitempty"`
  85. FillOpacity string `xml:"fill-opacity,attr,omitempty"`
  86. Stroke string `xml:"stroke,attr,omitempty"`
  87. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  88. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  89. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  90. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  91. D string `xml:"d,attr"`
  92. }
  93. type rectObj struct {
  94. XMLName xml.Name `xml:"rect"`
  95. Fill string `xml:"fill,attr,omitempty"`
  96. FillOpacity string `xml:"fill-opacity,attr,omitempty"`
  97. Stroke string `xml:"stroke,attr,omitempty"`
  98. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  99. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  100. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  101. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  102. X string `xml:"x,attr,omitempty"`
  103. Y string `xml:"y,attr,omitempty"`
  104. Width string `xml:"width,attr,omitempty"`
  105. Height string `xml:"height,attr,omitempty"`
  106. }
  107. type circleObj struct {
  108. XMLName xml.Name `xml:"circle"`
  109. Fill string `xml:"fill,attr,omitempty"`
  110. FillOpacity string `xml:"fill-opacity,attr,omitempty"`
  111. Stroke string `xml:"stroke,attr,omitempty"`
  112. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  113. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  114. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  115. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  116. CX string `xml:"cx,attr,omitempty"`
  117. CY string `xml:"cy,attr,omitempty"`
  118. R string `xml:"r,attr,omitempty"`
  119. }
  120. type ellipseObj struct {
  121. XMLName xml.Name `xml:"ellipse"`
  122. Fill string `xml:"fill,attr,omitempty"`
  123. FillOpacity string `xml:"fill-opacity,attr,omitempty"`
  124. Stroke string `xml:"stroke,attr,omitempty"`
  125. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  126. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  127. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  128. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  129. CX string `xml:"cx,attr,omitempty"`
  130. CY string `xml:"cy,attr,omitempty"`
  131. RX string `xml:"rx,attr,omitempty"`
  132. RY string `xml:"ry,attr,omitempty"`
  133. }
  134. type polygonObj struct {
  135. XMLName xml.Name `xml:"polygon"`
  136. Fill string `xml:"fill,attr,omitempty"`
  137. FillOpacity string `xml:"fill-opacity,attr,omitempty"`
  138. Stroke string `xml:"stroke,attr,omitempty"`
  139. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  140. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  141. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  142. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  143. Points string `xml:"points,attr"`
  144. }
  145. type objGroup struct {
  146. XMLName xml.Name `xml:"g"`
  147. ID string `xml:"id,attr,omitempty"`
  148. Fill string `xml:"fill,attr,omitempty"`
  149. Stroke string `xml:"stroke,attr,omitempty"`
  150. StrokeWidth string `xml:"stroke-width,attr,omitempty"`
  151. StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
  152. StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
  153. StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
  154. Paths []*pathObj `xml:"path"`
  155. Circles []*circleObj `xml:"circle"`
  156. Ellipses []*ellipseObj `xml:"ellipse"`
  157. Rects []*rectObj `xml:"rect"`
  158. Polygons []*polygonObj `xml:"polygon"`
  159. }
  160. func replacePathsFill(paths []*pathObj, hexColor string, opacity string) {
  161. for _, path := range paths {
  162. if path.Fill != "none" {
  163. path.Fill = hexColor
  164. path.FillOpacity = opacity
  165. }
  166. }
  167. }
  168. func replaceRectsFill(rects []*rectObj, hexColor string, opacity string) {
  169. for _, rect := range rects {
  170. if rect.Fill != "none" {
  171. rect.Fill = hexColor
  172. rect.FillOpacity = opacity
  173. }
  174. }
  175. }
  176. func replaceCirclesFill(circles []*circleObj, hexColor string, opacity string) {
  177. for _, circle := range circles {
  178. if circle.Fill != "none" {
  179. circle.Fill = hexColor
  180. circle.FillOpacity = opacity
  181. }
  182. }
  183. }
  184. func replaceEllipsesFill(ellipses []*ellipseObj, hexColor string, opacity string) {
  185. for _, ellipse := range ellipses {
  186. if ellipse.Fill != "none" {
  187. ellipse.Fill = hexColor
  188. ellipse.FillOpacity = opacity
  189. }
  190. }
  191. }
  192. func replacePolygonsFill(polys []*polygonObj, hexColor string, opacity string) {
  193. for _, poly := range polys {
  194. if poly.Fill != "none" {
  195. poly.Fill = hexColor
  196. poly.FillOpacity = opacity
  197. }
  198. }
  199. }
  200. func replaceGroupObjectFill(groups []*objGroup, hexColor string, opacity string) {
  201. for _, grp := range groups {
  202. replaceCirclesFill(grp.Circles, hexColor, opacity)
  203. replaceEllipsesFill(grp.Ellipses, hexColor, opacity)
  204. replacePathsFill(grp.Paths, hexColor, opacity)
  205. replaceRectsFill(grp.Rects, hexColor, opacity)
  206. replacePolygonsFill(grp.Polygons, hexColor, opacity)
  207. }
  208. }
  209. // replaceFillColor alters an svg objects fill color. Note that if an svg with multiple fill
  210. // colors is being operated upon, all fills will be converted to a single color. Mostly used
  211. // to recolor Icons to match the theme's IconColor.
  212. func (s *svg) replaceFillColor(color color.Color) error {
  213. hexColor, opacity := colorToHexAndOpacity(color)
  214. replacePathsFill(s.Paths, hexColor, opacity)
  215. replaceRectsFill(s.Rects, hexColor, opacity)
  216. replaceCirclesFill(s.Circles, hexColor, opacity)
  217. replaceEllipsesFill(s.Ellipses, hexColor, opacity)
  218. replacePolygonsFill(s.Polygons, hexColor, opacity)
  219. replaceGroupObjectFill(s.Groups, hexColor, opacity)
  220. return nil
  221. }
  222. func svgFromXML(reader io.Reader) (*svg, error) {
  223. var s svg
  224. bSlice, err := ioutil.ReadAll(reader)
  225. if err != nil {
  226. return nil, err
  227. }
  228. if err := xml.Unmarshal(bSlice, &s); err != nil {
  229. return nil, err
  230. }
  231. return &s, nil
  232. }
  233. func colorToHexAndOpacity(color color.Color) (hexStr, aStr string) {
  234. r, g, b, a := col.ToNRGBA(color)
  235. cBytes := []byte{byte(r), byte(g), byte(b)}
  236. hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
  237. return
  238. }
  239. func drawSVGSafely(icon *oksvg.SvgIcon, raster *rasterx.Dasher) error {
  240. var err error
  241. defer func() {
  242. if r := recover(); r != nil {
  243. err = errors.New("crash when rendering svg")
  244. }
  245. }()
  246. icon.Draw(raster, 1)
  247. return err
  248. }