Files
supersonic/ui/util/svg.go
2025-09-17 22:30:49 +02:00

322 lines
10 KiB
Go

package util
// Contents of this file come from the fyne internal SVG package
// Once https://github.com/fyne-io/fyne/pull/5345 is available in main,
// this file can be retired, and the ColorizeSVG func can be replaced with
// `canvas.ColorizeSVG`
import (
"bytes"
"encoding/hex"
"encoding/xml"
"fmt"
"image/color"
"io"
"strconv"
)
// ColorizeSVG creates a new SVG from a given one by replacing all fill colors by the given color.
func ColorizeSVG(src []byte, clr color.Color) ([]byte, error) {
rdr := bytes.NewReader(src)
s, err := svgFromXML(rdr)
if err != nil {
return src, fmt.Errorf("could not load SVG, falling back to static content: %v", err)
}
if err := s.replaceFillColor(clr); err != nil {
return src, fmt.Errorf("could not replace fill color, falling back to static content: %v", err)
}
colorized, err := xml.Marshal(s)
if err != nil {
return src, fmt.Errorf("could not marshal svg, falling back to static content: %v", err)
}
return colorized, nil
}
// 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,omitempty"`
Height string `xml:"height,attr,omitempty"`
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"`
Transform string `xml:"transform,attr,omitempty"`
}
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"`
Transform string `xml:"transform,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"`
Transform string `xml:"transform,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"`
Transform string `xml:"transform,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"`
Transform string `xml:"transform,attr,omitempty"`
}
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"`
Transform string `xml:"transform,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 := toNRGBA(color)
cBytes := []byte{byte(r), byte(g), byte(b)}
hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
return hexStr, aStr
}
// toNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA().
func toNRGBA(c color.Color) (r, g, b, a int) {
// We use UnmultiplyAlpha with RGBA, RGBA64, and unrecognized implementations of Color.
// It works for all Colors whose RGBA() method is implemented according to spec, but is only necessary for those.
// Only RGBA and RGBA64 have components which are already premultiplied.
switch col := c.(type) {
// NRGBA and NRGBA64 are not premultiplied
case color.NRGBA:
r = int(col.R)
g = int(col.G)
b = int(col.B)
a = int(col.A)
case *color.NRGBA:
r = int(col.R)
g = int(col.G)
b = int(col.B)
a = int(col.A)
case color.NRGBA64:
r = int(col.R) >> 8
g = int(col.G) >> 8
b = int(col.B) >> 8
a = int(col.A) >> 8
case *color.NRGBA64:
r = int(col.R) >> 8
g = int(col.G) >> 8
b = int(col.B) >> 8
a = int(col.A) >> 8
// Gray and Gray16 have no alpha component
case *color.Gray:
r = int(col.Y)
g = int(col.Y)
b = int(col.Y)
a = 0xff
case color.Gray:
r = int(col.Y)
g = int(col.Y)
b = int(col.Y)
a = 0xff
case *color.Gray16:
r = int(col.Y) >> 8
g = int(col.Y) >> 8
b = int(col.Y) >> 8
a = 0xff
case color.Gray16:
r = int(col.Y) >> 8
g = int(col.Y) >> 8
b = int(col.Y) >> 8
a = 0xff
// Alpha and Alpha16 contain only an alpha component.
case color.Alpha:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A)
case *color.Alpha:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A)
case color.Alpha16:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A) >> 8
case *color.Alpha16:
r = 0xff
g = 0xff
b = 0xff
a = int(col.A) >> 8
default: // RGBA, RGBA64, and unknown implementations of Color
r, g, b, a = unmultiplyAlpha(c)
}
return r, g, b, a
}
// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication.
// It is only used by ToRGBA.
func unmultiplyAlpha(c color.Color) (r, g, b, a int) {
red, green, blue, alpha := c.RGBA()
if alpha != 0 && alpha != 0xffff {
red = (red * 0xffff) / alpha
green = (green * 0xffff) / alpha
blue = (blue * 0xffff) / alpha
}
// Convert from range 0-65535 to range 0-255
r = int(red >> 8)
g = int(green >> 8)
b = int(blue >> 8)
a = int(alpha >> 8)
return r, g, b, a
}