svg.go 9.6 KB

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