| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- package svg
- import (
- "bytes"
- "encoding/hex"
- "encoding/xml"
- "errors"
- "fmt"
- "image"
- "image/color"
- "io"
- "path/filepath"
- "strconv"
- "strings"
- "github.com/srwiley/oksvg"
- "github.com/srwiley/rasterx"
- "fyne.io/fyne/v2"
- col "fyne.io/fyne/v2/internal/color"
- )
- // Colorize creates a new SVG from a given one by replacing all fill colors by the given color.
- func Colorize(src []byte, clr color.Color) []byte {
- rdr := bytes.NewReader(src)
- s, err := svgFromXML(rdr)
- if err != nil {
- fyne.LogError("could not load SVG, falling back to static content:", err)
- return src
- }
- if err := s.replaceFillColor(clr); err != nil {
- fyne.LogError("could not replace fill color, falling back to static content:", err)
- return src
- }
- colorized, err := xml.Marshal(s)
- if err != nil {
- fyne.LogError("could not marshal svg, falling back to static content:", err)
- return src
- }
- return colorized
- }
- type Decoder struct {
- icon *oksvg.SvgIcon
- }
- type Config struct {
- Width int
- Height int
- Aspect float32
- }
- func NewDecoder(stream io.Reader) (*Decoder, error) {
- icon, err := oksvg.ReadIconStream(stream)
- if err != nil {
- return nil, err
- }
- return &Decoder{
- icon: icon,
- }, nil
- }
- func (d *Decoder) Config() Config {
- return Config{
- int(d.icon.ViewBox.W),
- int(d.icon.ViewBox.H),
- float32(d.icon.ViewBox.W / d.icon.ViewBox.H),
- }
- }
- func (d *Decoder) Draw(width, height int) (*image.NRGBA, error) {
- config := d.Config()
- viewAspect := float32(width) / float32(height)
- imgW, imgH := width, height
- if viewAspect > config.Aspect {
- imgW = int(float32(height) * config.Aspect)
- } else if viewAspect < config.Aspect {
- imgH = int(float32(width) / config.Aspect)
- }
- d.icon.SetTarget(0, 0, float64(imgW), float64(imgH))
- img := image.NewNRGBA(image.Rect(0, 0, imgW, imgH))
- scanner := rasterx.NewScannerGV(config.Width, config.Height, img, img.Bounds())
- raster := rasterx.NewDasher(width, height, scanner)
- err := drawSVGSafely(d.icon, raster)
- if err != nil {
- err = fmt.Errorf("SVG render error: %w", err)
- return nil, err
- }
- return img, nil
- }
- func IsFileSVG(path string) bool {
- return strings.ToLower(filepath.Ext(path)) == ".svg"
- }
- // IsResourceSVG checks if the resource is an SVG or not.
- func IsResourceSVG(res fyne.Resource) bool {
- if strings.ToLower(filepath.Ext(res.Name())) == ".svg" {
- return true
- }
- if len(res.Content()) < 5 {
- return false
- }
- switch strings.ToLower(string(res.Content()[:5])) {
- case "<!doc", "<?xml", "<svg ":
- return true
- }
- return false
- }
- // svg holds the unmarshaled XML from a Scalable Vector Graphic
- type svg struct {
- XMLName xml.Name `xml:"svg"`
- XMLNS string `xml:"xmlns,attr"`
- Width string `xml:"width,attr"`
- Height string `xml:"height,attr"`
- ViewBox string `xml:"viewBox,attr,omitempty"`
- Paths []*pathObj `xml:"path"`
- Rects []*rectObj `xml:"rect"`
- Circles []*circleObj `xml:"circle"`
- Ellipses []*ellipseObj `xml:"ellipse"`
- Polygons []*polygonObj `xml:"polygon"`
- Groups []*objGroup `xml:"g"`
- }
- type pathObj struct {
- XMLName xml.Name `xml:"path"`
- Fill string `xml:"fill,attr,omitempty"`
- FillOpacity string `xml:"fill-opacity,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- D string `xml:"d,attr"`
- }
- type rectObj struct {
- XMLName xml.Name `xml:"rect"`
- Fill string `xml:"fill,attr,omitempty"`
- FillOpacity string `xml:"fill-opacity,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- X string `xml:"x,attr,omitempty"`
- Y string `xml:"y,attr,omitempty"`
- Width string `xml:"width,attr,omitempty"`
- Height string `xml:"height,attr,omitempty"`
- }
- type circleObj struct {
- XMLName xml.Name `xml:"circle"`
- Fill string `xml:"fill,attr,omitempty"`
- FillOpacity string `xml:"fill-opacity,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- CX string `xml:"cx,attr,omitempty"`
- CY string `xml:"cy,attr,omitempty"`
- R string `xml:"r,attr,omitempty"`
- }
- type ellipseObj struct {
- XMLName xml.Name `xml:"ellipse"`
- Fill string `xml:"fill,attr,omitempty"`
- FillOpacity string `xml:"fill-opacity,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- CX string `xml:"cx,attr,omitempty"`
- CY string `xml:"cy,attr,omitempty"`
- RX string `xml:"rx,attr,omitempty"`
- RY string `xml:"ry,attr,omitempty"`
- }
- type polygonObj struct {
- XMLName xml.Name `xml:"polygon"`
- Fill string `xml:"fill,attr,omitempty"`
- FillOpacity string `xml:"fill-opacity,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- Points string `xml:"points,attr"`
- }
- type objGroup struct {
- XMLName xml.Name `xml:"g"`
- ID string `xml:"id,attr,omitempty"`
- Fill string `xml:"fill,attr,omitempty"`
- Stroke string `xml:"stroke,attr,omitempty"`
- StrokeWidth string `xml:"stroke-width,attr,omitempty"`
- StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
- StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
- StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
- Paths []*pathObj `xml:"path"`
- Circles []*circleObj `xml:"circle"`
- Ellipses []*ellipseObj `xml:"ellipse"`
- Rects []*rectObj `xml:"rect"`
- Polygons []*polygonObj `xml:"polygon"`
- Groups []*objGroup `xml:"g"`
- }
- func replacePathsFill(paths []*pathObj, hexColor string, opacity string) {
- for _, path := range paths {
- if path.Fill != "none" {
- path.Fill = hexColor
- path.FillOpacity = opacity
- }
- }
- }
- func replaceRectsFill(rects []*rectObj, hexColor string, opacity string) {
- for _, rect := range rects {
- if rect.Fill != "none" {
- rect.Fill = hexColor
- rect.FillOpacity = opacity
- }
- }
- }
- func replaceCirclesFill(circles []*circleObj, hexColor string, opacity string) {
- for _, circle := range circles {
- if circle.Fill != "none" {
- circle.Fill = hexColor
- circle.FillOpacity = opacity
- }
- }
- }
- func replaceEllipsesFill(ellipses []*ellipseObj, hexColor string, opacity string) {
- for _, ellipse := range ellipses {
- if ellipse.Fill != "none" {
- ellipse.Fill = hexColor
- ellipse.FillOpacity = opacity
- }
- }
- }
- func replacePolygonsFill(polys []*polygonObj, hexColor string, opacity string) {
- for _, poly := range polys {
- if poly.Fill != "none" {
- poly.Fill = hexColor
- poly.FillOpacity = opacity
- }
- }
- }
- func replaceGroupObjectFill(groups []*objGroup, hexColor string, opacity string) {
- for _, grp := range groups {
- replaceCirclesFill(grp.Circles, hexColor, opacity)
- replaceEllipsesFill(grp.Ellipses, hexColor, opacity)
- replacePathsFill(grp.Paths, hexColor, opacity)
- replaceRectsFill(grp.Rects, hexColor, opacity)
- replacePolygonsFill(grp.Polygons, hexColor, opacity)
- replaceGroupObjectFill(grp.Groups, hexColor, opacity)
- }
- }
- // replaceFillColor alters an svg objects fill color. Note that if an svg with multiple fill
- // colors is being operated upon, all fills will be converted to a single color. Mostly used
- // to recolor Icons to match the theme's IconColor.
- func (s *svg) replaceFillColor(color color.Color) error {
- hexColor, opacity := colorToHexAndOpacity(color)
- replacePathsFill(s.Paths, hexColor, opacity)
- replaceRectsFill(s.Rects, hexColor, opacity)
- replaceCirclesFill(s.Circles, hexColor, opacity)
- replaceEllipsesFill(s.Ellipses, hexColor, opacity)
- replacePolygonsFill(s.Polygons, hexColor, opacity)
- replaceGroupObjectFill(s.Groups, hexColor, opacity)
- return nil
- }
- func svgFromXML(reader io.Reader) (*svg, error) {
- var s svg
- bSlice, err := io.ReadAll(reader)
- if err != nil {
- return nil, err
- }
- if err := xml.Unmarshal(bSlice, &s); err != nil {
- return nil, err
- }
- return &s, nil
- }
- func colorToHexAndOpacity(color color.Color) (hexStr, aStr string) {
- r, g, b, a := col.ToNRGBA(color)
- cBytes := []byte{byte(r), byte(g), byte(b)}
- hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
- return
- }
- func drawSVGSafely(icon *oksvg.SvgIcon, raster *rasterx.Dasher) error {
- var err error
- defer func() {
- if r := recover(); r != nil {
- err = errors.New("crash when rendering svg")
- }
- }()
- icon.Draw(raster, 1)
- return err
- }
|