From 4f84a1d63ffbac53488ffde8ef701a4fc53ecf2d Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Thu, 19 Dec 2024 10:34:53 -0700 Subject: [PATCH] De-duplicate, fix, and improve color parsing There was a very similar color parsing implementation in `color.go` and `chartdraw/drawing/color.go`. After benchmarking it was found that `color.go` was faster, but also incorrectly handled the alpha channel. This commit updates the implementation in `chartdraw/drawing/color.go` to use this simpler and more flexible regex pattern. Allowing both rgb and rgba formats to be parsed by `ColorFromRGBA` in a more performant way. With this change the implementation in `color.go` was removed to instead defer to the improved `chartdraw/drawing/color.go` implementation. PraseColor has been elevated to the `charts` package so this functionality can be easily found. --- chartdraw/drawing/color.go | 128 ++++++++++++-------------------- chartdraw/drawing/color_test.go | 2 +- color.go | 43 +---------- color_test.go | 22 ++++-- echarts.go | 8 +- theme.go | 50 ++++++------- 6 files changed, 100 insertions(+), 153 deletions(-) diff --git a/chartdraw/drawing/color.go b/chartdraw/drawing/color.go index 0ac5b1a..7c2c32d 100644 --- a/chartdraw/drawing/color.go +++ b/chartdraw/drawing/color.go @@ -44,91 +44,62 @@ var ( ColorAqua = Color{R: 0, G: 255, B: 255, A: 255} ) -func parseHex(hex string) uint8 { - v, _ := strconv.ParseInt(hex, 16, 16) - return uint8(v) -} - // ParseColor parses a color from a string. func ParseColor(rawColor string) Color { if strings.HasPrefix(rawColor, "#") { return ColorFromHex(rawColor) - } else if strings.HasPrefix(rawColor, "rgba") { - return ColorFromRGBA(rawColor) } else if strings.HasPrefix(rawColor, "rgb") { - return ColorFromRGB(rawColor) + return ColorFromRGBA(rawColor) } return ColorFromKnown(rawColor) } -var rgbaexpr = regexp.MustCompile(`rgba\((?P.+),(?P.+),(?P.+),(?P.+)\)`) +var rgbReg = regexp.MustCompile(`\(([^)]+)\)`) -// ColorFromRGBA returns a color from an `rgba()` css function. -func ColorFromRGBA(rgba string) (output Color) { - values := rgbaexpr.FindStringSubmatch(rgba) - for i, name := range rgbaexpr.SubexpNames() { - if i == 0 { - continue - } - if i >= len(values) { - break - } - switch name { - case "R": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.R = uint8(parsed) - case "G": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.G = uint8(parsed) - case "B": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.B = uint8(parsed) - case "A": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseFloat(value, 32) - if parsed > 1 { - parsed = 1 - } else if parsed < 0 { - parsed = 0 - } - output.A = uint8(parsed * 255) - } +// ColorFromRGBA returns a color from a `rgb(i,i,i)` or `rgba(i,i,i,f)` css function. +func ColorFromRGBA(color string) Color { + var c Color + + // Attempt to parse rgb(...) or rgba(...) + result := rgbReg.FindAllStringSubmatch(color, 1) + if len(result) == 0 || len(result[0]) != 2 { + return c + } + arr := strings.Split(result[0][1], ",") + if len(arr) < 3 { // at a minimum we expect r,g,b to be specified + return c } - return -} -var rgbexpr = regexp.MustCompile(`rgb\((?P.+),(?P.+),(?P.+)\)`) + rVal, _ := strconv.ParseInt(strings.TrimSpace(arr[0]), 10, 16) + c.R = uint8(rVal) + gVal, _ := strconv.ParseInt(strings.TrimSpace(arr[1]), 10, 16) + c.G = uint8(gVal) + bVal, _ := strconv.ParseInt(strings.TrimSpace(arr[2]), 10, 16) + c.B = uint8(bVal) + if len(arr) > 3 { // if alpha channel is specified + aVal, _ := strconv.ParseFloat(strings.TrimSpace(arr[3]), 64) + if aVal < 0 { + aVal = 0 + } else if aVal <= 1 { + // correctly specified decimal, convert it to an integer scale + aVal *= 255 + } // else, incorrectly specified value over 1, accept the value directly + c.A = uint8(aVal) + } else { + c.A = 255 // default alpha channel to 255 + } -// ColorFromRGB returns a color from an `rgb()` css function. + return c +} + +// Deprecated: ColorFromRGB is deprecated, use ColorFromRGBA to get colors from RGB or RGBA format strings. func ColorFromRGB(rgb string) (output Color) { - output.A = 255 - values := rgbexpr.FindStringSubmatch(rgb) - for i, name := range rgbaexpr.SubexpNames() { - if i == 0 { - continue - } - if i >= len(values) { - break - } - switch name { - case "R": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.R = uint8(parsed) - case "G": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.G = uint8(parsed) - case "B": - value := strings.TrimSpace(values[i]) - parsed, _ := strconv.ParseInt(value, 10, 16) - output.B = uint8(parsed) - } - } - return + return ColorFromRGBA(rgb) +} + +func parseHex(hex string) uint8 { + v, _ := strconv.ParseInt(hex, 16, 16) + return uint8(v) } // ColorFromHex returns a color from a css hex code. @@ -136,7 +107,7 @@ func ColorFromRGB(rgb string) (output Color) { // NOTE: it will trim a leading '#' character if present. func ColorFromHex(hex string) Color { hex = strings.TrimPrefix(hex, "#") - var c Color + c := Color{A: 255} if len(hex) == 3 { c.R = parseHex(string(hex[0])) * 0x11 c.G = parseHex(string(hex[1])) * 0x11 @@ -146,7 +117,6 @@ func ColorFromHex(hex string) Color { c.G = parseHex(hex[2:4]) c.B = parseHex(hex[4:6]) } - c.A = 255 return c } @@ -193,12 +163,12 @@ func ColorFromKnown(known string) Color { // ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values. func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color { fa := float64(a) / 255.0 - var c Color - c.R = uint8(float64(r) / fa) - c.G = uint8(float64(g) / fa) - c.B = uint8(float64(b) / fa) - c.A = uint8(a | (a >> 8)) - return c + return Color{ + R: uint8(float64(r) / fa), + G: uint8(float64(g) / fa), + B: uint8(float64(b) / fa), + A: uint8(a | (a >> 8)), + } } // ColorChannelFromFloat returns a normalized byte from a given float value. diff --git a/chartdraw/drawing/color_test.go b/chartdraw/drawing/color_test.go index 234098f..cd2ce6a 100644 --- a/chartdraw/drawing/color_test.go +++ b/chartdraw/drawing/color_test.go @@ -73,7 +73,7 @@ func Test_ColorFromRGBA(t *testing.T) { parsed = ColorFromRGBA(value) assert.Equal(t, ColorSilver, parsed) - value = "rgba(192,192,192,1.5)" + value = "rgba(192,192,192,255)" parsed = ColorFromRGBA(value) assert.Equal(t, ColorSilver, parsed) } diff --git a/color.go b/color.go index 1604190..a4c6d80 100644 --- a/color.go +++ b/color.go @@ -2,9 +2,6 @@ package charts import ( "math" - "regexp" - "strconv" - "strings" "github.com/go-analyze/charts/chartdraw/drawing" ) @@ -16,40 +13,8 @@ func isLightColor(c Color) bool { return math.Sqrt(r+g+b) > 127.5 } -var rgbReg = regexp.MustCompile(`\((\S+)\)`) - -// TODO - de-duplicate with chartdraw/drawing/color.go:ParseColor -func parseColor(color string) Color { - c := Color{} - if color == "" { - return c - } - if strings.HasPrefix(color, "#") { - return drawing.ColorFromHex(color[1:]) - } - result := rgbReg.FindAllStringSubmatch(color, 1) - if len(result) == 0 || len(result[0]) != 2 { - return c - } - arr := strings.Split(result[0][1], ",") - if len(arr) < 3 { - return c - } - // set the default value to 255 - c.A = 255 - for index, v := range arr { - value, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 16) - ui8 := uint8(value) - switch index { - case 0: - c.R = ui8 - case 1: - c.G = ui8 - case 2: - c.B = ui8 - default: - c.A = ui8 - } - } - return c +// ParseColor parses a color from a string. The color can be specified in hex with a `#` prefix (for example '#313233'), +// in rgb(i,i,i) or rgba(i,i,i,f) format, or as a common name (for example 'red'). +func ParseColor(color string) Color { + return drawing.ParseColor(color) } diff --git a/color_test.go b/color_test.go index 9f5f7da..572aa3a 100644 --- a/color_test.go +++ b/color_test.go @@ -22,18 +22,30 @@ func TestIsLightColor(t *testing.T) { func TestParseColor(t *testing.T) { t.Parallel() - c := parseColor("") + c := ParseColor("") assert.True(t, c.IsZero()) - c = parseColor("#333") + c = ParseColor("#333") assert.Equal(t, drawing.Color{R: 51, G: 51, B: 51, A: 255}, c) - c = parseColor("#313233") + c = ParseColor("#313233") assert.Equal(t, drawing.Color{R: 49, G: 50, B: 51, A: 255}, c) - c = parseColor("rgb(31,32,33)") + c = ParseColor("rgb(31,32,33)") assert.Equal(t, drawing.Color{R: 31, G: 32, B: 33, A: 255}, c) - c = parseColor("rgba(50,51,52,250)") + c = ParseColor("rgba(50,51,52,.981)") assert.Equal(t, drawing.Color{R: 50, G: 51, B: 52, A: 250}, c) + + c = ParseColor("rgba(50,51,52,250)") + assert.Equal(t, drawing.Color{R: 50, G: 51, B: 52, A: 250}, c) +} + +func BenchmarkParseColor(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = ParseColor("#333") + _ = ParseColor("#313233") + _ = ParseColor("rgb(31,32,33)") + _ = ParseColor("rgba(50,51,52,250)") + } } diff --git a/echarts.go b/echarts.go index 4779d15..ced1292 100644 --- a/echarts.go +++ b/echarts.go @@ -298,7 +298,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { Min: item.Min, Label: SeriesLabel{ FontStyle: FontStyle{ - FontColor: parseColor(item.Label.Color), + FontColor: ParseColor(item.Label.Color), }, Show: item.Label.Show, Distance: item.Label.Distance, @@ -319,7 +319,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { YAxisIndex: item.YAxisIndex, Label: SeriesLabel{ FontStyle: FontStyle{ - FontColor: parseColor(item.Label.Color), + FontColor: ParseColor(item.Label.Color), }, Show: item.Label.Show, Distance: item.Label.Distance, @@ -342,7 +342,7 @@ func (et *EChartsTextStyle) ToStyle() chartdraw.Style { s := chartdraw.Style{ FontStyle: chartdraw.FontStyle{ FontSize: et.FontSize, - FontColor: parseColor(et.Color), + FontColor: ParseColor(et.Color), }, } if et.FontFamily != "" { @@ -455,7 +455,7 @@ func (eo *EChartsOption) ToOption() ChartOption { Min: item.Min, Max: item.Max, Formatter: item.AxisLabel.Formatter, - AxisColor: parseColor(item.AxisLine.LineStyle.Color), + AxisColor: ParseColor(item.AxisLine.LineStyle.Color), Data: item.Data, } } diff --git a/theme.go b/theme.go index 08549df..0e641a9 100644 --- a/theme.go +++ b/theme.go @@ -76,15 +76,15 @@ var defaultDarkFontColor = drawing.Color{R: 238, G: 238, B: 238, A: 255} func init() { darkGray := Color{R: 40, G: 40, B: 40, A: 255} echartSeriesColors := []Color{ - parseColor("#5470c6"), - parseColor("#91cc75"), - parseColor("#fac858"), - parseColor("#ee6666"), - parseColor("#73c0de"), - parseColor("#3ba272"), - parseColor("#fc8452"), - parseColor("#9a60b4"), - parseColor("#ea7ccc"), + drawing.ColorFromHex("#5470c6"), + drawing.ColorFromHex("#91cc75"), + drawing.ColorFromHex("#fac858"), + drawing.ColorFromHex("#ee6666"), + drawing.ColorFromHex("#73c0de"), + drawing.ColorFromHex("#3ba272"), + drawing.ColorFromHex("#fc8452"), + drawing.ColorFromHex("#9a60b4"), + drawing.ColorFromHex("#ea7ccc"), } InstallTheme( ThemeLight, @@ -168,14 +168,14 @@ func init() { BackgroundColor: drawing.ColorWhite, TextColor: Color{R: 70, G: 70, B: 70, A: 255}, SeriesColors: []Color{ - parseColor("#5b8ff9"), - parseColor("#5ad8a6"), - parseColor("#5d7092"), - parseColor("#f6bd16"), - parseColor("#6f5ef9"), - parseColor("#6dc8ec"), - parseColor("#945fb9"), - parseColor("#ff9845"), + drawing.ColorFromHex("#5b8ff9"), + drawing.ColorFromHex("#5ad8a6"), + drawing.ColorFromHex("#5d7092"), + drawing.ColorFromHex("#f6bd16"), + drawing.ColorFromHex("#6f5ef9"), + drawing.ColorFromHex("#6dc8ec"), + drawing.ColorFromHex("#945fb9"), + drawing.ColorFromHex("#ff9845"), }, }, ) @@ -188,14 +188,14 @@ func init() { BackgroundColor: Color{R: 31, G: 29, B: 29, A: 255}, TextColor: Color{R: 216, G: 217, B: 218, A: 255}, SeriesColors: []Color{ - parseColor("#7EB26D"), - parseColor("#EAB839"), - parseColor("#6ED0E0"), - parseColor("#EF843C"), - parseColor("#E24D42"), - parseColor("#1F78C1"), - parseColor("#705DA0"), - parseColor("#508642"), + drawing.ColorFromHex("#7EB26D"), + drawing.ColorFromHex("#EAB839"), + drawing.ColorFromHex("#6ED0E0"), + drawing.ColorFromHex("#EF843C"), + drawing.ColorFromHex("#E24D42"), + drawing.ColorFromHex("#1F78C1"), + drawing.ColorFromHex("#705DA0"), + drawing.ColorFromHex("#508642"), }, }, )