├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── colors.go ├── colors_test.go ├── go.mod ├── hex.go ├── rgb.go └── rgba.go /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: Test 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.17.x,1.20.x] 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Restore Cache 24 | uses: actions/cache@v3 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.os }}-v1-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-v1-go- 30 | 31 | - name: Test 32 | run: go test -race -covermode=atomic -coverprofile="profile.cov" ./... 33 | 34 | golangci: 35 | name: lint 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/setup-go@v3 39 | with: 40 | go-version: 1.20.x 41 | - uses: actions/checkout@v3 42 | - name: golangci-lint 43 | uses: golangci/golangci-lint-action@v3 44 | with: 45 | version: latest 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=GO111MODULE=on go 2 | 3 | lint: 4 | golangci-lint run --timeout=5m 5 | 6 | test: 7 | $(GOCMD) test -cover -race ./... 8 | 9 | bench: 10 | $(GOCMD) test -bench=. -benchmem ./... 11 | 12 | .PHONY: test lint linters-install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package colors 2 | ================ 3 | ![Project status](https://img.shields.io/badge/version-1.3.1-green.svg) 4 | [![GoDoc](https://godoc.org/github.com/go-playground/colors?status.svg)](https://pkg.go.dev/github.com/go-playground/colors) 5 | 6 | Go color manipulation, conversion and printing library/utility 7 | 8 | this library is currently in development, not all color types such as HSL, HSV and CMYK will be included in the first release; pull requests are welcome. 9 | 10 | Installation 11 | ============ 12 | 13 | Use go get. 14 | 15 | go get github.com/go-playground/colors 16 | 17 | Then import the validator package into your own code. 18 | 19 | import "github.com/go-playground/colors" 20 | 21 | Usage and documentation 22 | ======================= 23 | 24 | #Example 25 | ```go 26 | hex, err := colors.ParseHEX("#fff") 27 | rgb, err := colors.ParseRGB("rgb(0,0,0)") 28 | rgb, err := colors.RGB(0,0,0) 29 | rgba, err := colors.ParseRGBA("rgba(0,0,0,1)") 30 | rgba, err := colors.RGBA(0,0,0,1) 31 | 32 | // don't know which color, it was user selectable 33 | color, err := colors.Parse("#000") 34 | 35 | color.ToRGB() // rgb(0,0,0) 36 | color.ToRGBA() // rgba(0,0,0,1) 37 | color.ToHEX() // #000000 38 | color.IsLight() // false 39 | color.IsDark() // true 40 | 41 | ``` 42 | 43 | How to Contribute 44 | ================= 45 | 46 | Make a pull request... 47 | 48 | License 49 | ======= 50 | Distributed under MIT License, please see license file in code for more details. 51 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // ErrBadColor is the default bad color error 10 | ErrBadColor = errors.New("parsing of color failed, Bad Color") 11 | ) 12 | 13 | // Color is the base color interface from which all others ascribe to 14 | type Color interface { 15 | // ToHEX converts the Color interface to a concrete HEXColor 16 | ToHEX() *HEXColor 17 | 18 | // ToRGB converts the Color interface to a concrete RGBColor 19 | ToRGB() *RGBColor 20 | 21 | // ToRGBA converts the Color interface to a concrete RGBAColor 22 | ToRGBA() *RGBAColor 23 | 24 | // String returns the string representation of the Color 25 | String() string 26 | 27 | // IsLight returns whether the color is perceived to be a light color 28 | // http://stackoverflow.com/a/24213274/3158232 and http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx 29 | IsLight() bool 30 | 31 | // IsDark returns whether the color is perceived to be a dark color 32 | //for perceived luminance, not strict math 33 | IsDark() bool 34 | 35 | // RGBA implements std-lib color.Color interface. 36 | // It returns the red, green, blue and alpha values for the color. Each value ranges within [0, 0xffff] 37 | RGBA() (r, g, b, a uint32) 38 | 39 | // Equal reports whether the colors are the same 40 | Equal(Color) bool 41 | } 42 | 43 | // Parse parses an unknown color type to it's appropriate type, or returns a ErrBadColor 44 | func Parse(s string) (Color, error) { 45 | 46 | if len(s) < 4 { 47 | return nil, ErrBadColor 48 | } 49 | 50 | s = strings.ToLower(s) 51 | 52 | if s[:1] == "#" { 53 | return ParseHEX(s) 54 | } else if s[:4] == "rgba" { 55 | return ParseRGBA(s) 56 | } else if s[:3] == "rgb" { 57 | return ParseRGB(s) 58 | } 59 | 60 | return nil, ErrBadColor 61 | } 62 | -------------------------------------------------------------------------------- /colors_test.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "path" 7 | "reflect" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | // NOTES: 13 | // - Run "go test" to run tests 14 | // - Run "gocov test | gocov report" to report on test converage by file 15 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 16 | // 17 | 18 | func IsEqual(t *testing.T, val1, val2 interface{}) bool { 19 | v1 := reflect.ValueOf(val1) 20 | v2 := reflect.ValueOf(val2) 21 | 22 | if v1.Kind() == reflect.Ptr { 23 | v1 = v1.Elem() 24 | } 25 | 26 | if v2.Kind() == reflect.Ptr { 27 | v2 = v2.Elem() 28 | } 29 | 30 | if !v1.IsValid() && !v2.IsValid() { 31 | return true 32 | } 33 | 34 | switch v1.Kind() { 35 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 36 | if v1.IsNil() { 37 | v1 = reflect.ValueOf(nil) 38 | } 39 | } 40 | 41 | switch v2.Kind() { 42 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 43 | if v2.IsNil() { 44 | v2 = reflect.ValueOf(nil) 45 | } 46 | } 47 | 48 | v1Underlying := reflect.Zero(reflect.TypeOf(v1)).Interface() 49 | v2Underlying := reflect.Zero(reflect.TypeOf(v2)).Interface() 50 | 51 | if v1 == v1Underlying { 52 | if v2 == v2Underlying { 53 | goto CASE4 54 | } else { 55 | goto CASE3 56 | } 57 | } else { 58 | if v2 == v2Underlying { 59 | goto CASE2 60 | } else { 61 | goto CASE1 62 | } 63 | } 64 | 65 | CASE1: 66 | // fmt.Println("CASE 1") 67 | return reflect.DeepEqual(v1.Interface(), v2.Interface()) 68 | CASE2: 69 | // fmt.Println("CASE 2") 70 | return reflect.DeepEqual(v1.Interface(), v2) 71 | CASE3: 72 | // fmt.Println("CASE 3") 73 | return reflect.DeepEqual(v1, v2.Interface()) 74 | CASE4: 75 | // fmt.Println("CASE 4") 76 | return reflect.DeepEqual(v1, v2) 77 | } 78 | 79 | func Equal(t *testing.T, val1, val2 interface{}) { 80 | EqualSkip(t, 2, val1, val2) 81 | } 82 | 83 | func EqualSkip(t *testing.T, skip int, val1, val2 interface{}) { 84 | 85 | if !IsEqual(t, val1, val2) { 86 | 87 | _, file, line, _ := runtime.Caller(skip) 88 | fmt.Printf("%s:%d %v does not equal %v\n", path.Base(file), line, val1, val2) 89 | t.FailNow() 90 | } 91 | } 92 | 93 | func NotEqual(t *testing.T, val1, val2 interface{}) { 94 | NotEqualSkip(t, 2, val1, val2) 95 | } 96 | 97 | func NotEqualSkip(t *testing.T, skip int, val1, val2 interface{}) { 98 | 99 | if IsEqual(t, val1, val2) { 100 | _, file, line, _ := runtime.Caller(skip) 101 | fmt.Printf("%s:%d %v should not be equal %v\n", path.Base(file), line, val1, val2) 102 | t.FailNow() 103 | } 104 | } 105 | 106 | func PanicMatches(t *testing.T, fn func(), matches string) { 107 | PanicMatchesSkip(t, 2, fn, matches) 108 | } 109 | 110 | func PanicMatchesSkip(t *testing.T, skip int, fn func(), matches string) { 111 | 112 | _, file, line, _ := runtime.Caller(skip) 113 | 114 | defer func() { 115 | if r := recover(); r != nil { 116 | err := fmt.Sprintf("%s", r) 117 | 118 | if err != matches { 119 | fmt.Printf("%s:%d Panic... expected [%s] received [%s]", path.Base(file), line, matches, err) 120 | t.FailNow() 121 | } 122 | } 123 | }() 124 | 125 | fn() 126 | } 127 | 128 | func TestColorConversionFromHEX(t *testing.T) { 129 | 130 | hex, _ := ParseHEX("#5f55f5") 131 | 132 | Equal(t, hex.ToHEX().String(), "#5f55f5") 133 | Equal(t, hex.ToRGB().String(), "rgb(95,85,245)") 134 | Equal(t, hex.ToRGBA().String(), "rgba(95,85,245,1)") 135 | 136 | hex, _ = ParseHEX("#5f5") 137 | Equal(t, hex.ToRGB().String(), "rgb(85,255,85)") 138 | 139 | hex, _ = ParseHEX("Bad Hex color!") 140 | Equal(t, hex, nil) 141 | } 142 | 143 | func TestColorConversionFromRGB(t *testing.T) { 144 | 145 | rgb, _ := ParseRGB("rgb(95%,85%,50%)") 146 | 147 | Equal(t, rgb.ToRGB().String(), "rgb(242,217,128)") 148 | Equal(t, rgb.ToRGBA().String(), "rgba(242,217,128,1)") 149 | Equal(t, rgb.ToHEX().String(), "#f2d980") 150 | 151 | rgb, _ = ParseRGB("rgb(95,85,245)") 152 | Equal(t, rgb.ToRGB().String(), "rgb(95,85,245)") 153 | Equal(t, rgb.ToRGBA().String(), "rgba(95,85,245,1)") 154 | Equal(t, rgb.ToHEX().String(), "#5f55f5") 155 | 156 | rgb, _ = RGB(95, 85, 245) 157 | Equal(t, rgb.ToRGB().String(), "rgb(95,85,245)") 158 | Equal(t, rgb.ToRGBA().String(), "rgba(95,85,245,1)") 159 | Equal(t, rgb.ToHEX().String(), "#5f55f5") 160 | 161 | rgb, _ = ParseRGB("BAD RGB COLOR") 162 | Equal(t, rgb, nil) 163 | 164 | rgb, _ = ParseRGB("rgb(95%,85%,245)") 165 | Equal(t, rgb, nil) 166 | } 167 | 168 | func TestColorConversionFromRGBA(t *testing.T) { 169 | 170 | rgba, _ := ParseRGBA("rgba(95%,85%,50%,1)") 171 | 172 | Equal(t, rgba.ToRGB().String(), "rgb(242,217,128)") 173 | Equal(t, rgba.ToRGBA().String(), "rgba(242,217,128,1)") 174 | Equal(t, rgba.ToHEX().String(), "#f2d980") 175 | 176 | rgba, _ = ParseRGBA("rgba(95,85,245,1)") 177 | Equal(t, rgba.ToRGB().String(), "rgb(95,85,245)") 178 | Equal(t, rgba.ToRGBA().String(), "rgba(95,85,245,1)") 179 | Equal(t, rgba.ToHEX().String(), "#5f55f5") 180 | 181 | rgba, _ = RGBA(95, 85, 245, 1) 182 | Equal(t, rgba.ToRGB().String(), "rgb(95,85,245)") 183 | Equal(t, rgba.ToRGBA().String(), "rgba(95,85,245,1)") 184 | Equal(t, rgba.ToHEX().String(), "#5f55f5") 185 | 186 | rgba, _ = RGBA(95, 85, 245, 6) 187 | Equal(t, rgba, nil) 188 | 189 | rgba, _ = RGBA(95, 85, 245, -1) 190 | Equal(t, rgba, nil) 191 | 192 | rgba, _ = ParseRGBA("BAD RGBA COLOR") 193 | Equal(t, rgba, nil) 194 | 195 | rgba, _ = ParseRGBA("rgba(95%,85%,245,1)") 196 | Equal(t, rgba, nil) 197 | } 198 | 199 | func TestColorConversionFromStdColor(t *testing.T) { 200 | rgba := FromStdColor(color.RGBA{242, 217, 128, 255}) 201 | Equal(t, rgba.ToRGB().String(), "rgb(242,217,128)") 202 | Equal(t, rgba.ToRGBA().String(), "rgba(242,217,128,1)") 203 | Equal(t, rgba.ToHEX().String(), "#f2d980") 204 | 205 | rgba = FromStdColor(color.RGBA{95, 85, 245, 255}) 206 | Equal(t, rgba.ToRGB().String(), "rgb(95,85,245)") 207 | Equal(t, rgba.ToRGBA().String(), "rgba(95,85,245,1)") 208 | Equal(t, rgba.ToHEX().String(), "#5f55f5") 209 | } 210 | 211 | func TestColorConversionFromToStdColor(t *testing.T) { 212 | // verify that colors are equals 213 | equalColors := func(t *testing.T, color Color, stdColor color.Color) { 214 | r, g, b, a := color.RGBA() 215 | stdR, stdG, stdB, stdA := stdColor.RGBA() 216 | Equal(t, r, stdR) 217 | Equal(t, g, stdG) 218 | Equal(t, b, stdB) 219 | Equal(t, a, stdA) 220 | } 221 | 222 | hex, _ := ParseHEX("#5f55f5") 223 | r, g, b, a := hex.RGBA() 224 | 225 | Equal(t, r, uint32(24415)) 226 | Equal(t, g, uint32(21845)) 227 | Equal(t, b, uint32(62965)) 228 | Equal(t, a, uint32(65535)) 229 | equalColors(t, hex, &color.RGBA{R: 95, G: 85, B: 245, A: 255}) 230 | 231 | rgba, _ := RGBA(242, 217, 128, 0.4) 232 | r, g, b, a = rgba.RGBA() 233 | 234 | Equal(t, r, uint32(62194)) 235 | Equal(t, g, uint32(55769)) 236 | Equal(t, b, uint32(32896)) 237 | Equal(t, a, uint32(26214)) 238 | equalColors(t, rgba, &color.RGBA{R: 242, G: 217, B: 128, A: 102}) 239 | 240 | rgb, _ := RGB(242, 217, 128) 241 | r, g, b, a = rgb.RGBA() 242 | 243 | Equal(t, r, uint32(62194)) 244 | Equal(t, g, uint32(55769)) 245 | Equal(t, b, uint32(32896)) 246 | Equal(t, a, uint32(65535)) 247 | equalColors(t, rgb, &color.RGBA{R: 242, G: 217, B: 128, A: 255}) 248 | } 249 | 250 | func TestColorEqual(t *testing.T) { 251 | 252 | hex, _ := ParseHEX("#5f55f5") 253 | rgb, _ := RGB(95, 85, 245) 254 | rgba, _ := RGBA(95, 85, 245, 1) 255 | 256 | Equal(t, hex.Equal(hex), true) 257 | Equal(t, hex.Equal(rgb), true) 258 | Equal(t, hex.Equal(rgba), true) 259 | Equal(t, rgb.Equal(rgb), true) 260 | Equal(t, rgb.Equal(hex), true) 261 | Equal(t, rgb.Equal(rgba), true) 262 | Equal(t, rgba.Equal(rgba), true) 263 | Equal(t, rgba.Equal(rgb), true) 264 | Equal(t, rgba.Equal(hex), true) 265 | 266 | hex2, _ := ParseHEX("#5f55f4") 267 | rgb2, _ := RGB(95, 87, 245) 268 | rgba2, _ := RGBA(93, 85, 245, 1) 269 | 270 | Equal(t, hex2.Equal(rgb2), false) 271 | Equal(t, hex2.Equal(rgba2), false) 272 | Equal(t, rgb2.Equal(hex2), false) 273 | Equal(t, rgb2.Equal(rgba2), false) 274 | Equal(t, rgba2.Equal(rgb2), false) 275 | Equal(t, rgba2.Equal(hex2), false) 276 | 277 | } 278 | 279 | func TestParseColor(t *testing.T) { 280 | 281 | color, _ := Parse("#FFF") 282 | NotEqual(t, color, nil) 283 | Equal(t, reflect.TypeOf(color) == reflect.TypeOf(&HEXColor{}), true) 284 | 285 | color, _ = Parse("rgb(95,85,245)") 286 | NotEqual(t, color, nil) 287 | Equal(t, reflect.TypeOf(color) == reflect.TypeOf(&RGBColor{}), true) 288 | 289 | color, _ = Parse("rgba(95,85,245,1)") 290 | NotEqual(t, color, nil) 291 | Equal(t, reflect.TypeOf(color) == reflect.TypeOf(&RGBAColor{}), true) 292 | 293 | color, _ = Parse("#ff") 294 | Equal(t, color, nil) 295 | 296 | color, _ = Parse("garbage-data") 297 | Equal(t, color, nil) 298 | 299 | c, err := Parse("rgba(127,34,94,0.534556634531)") 300 | Equal(t, err, nil) 301 | Equal(t, reflect.TypeOf(c) == reflect.TypeOf(&RGBAColor{}), true) 302 | } 303 | 304 | func TestIsLightIsDark(t *testing.T) { 305 | 306 | rgb, _ := RGB(0, 0, 0) 307 | Equal(t, rgb.IsLight(), false) 308 | Equal(t, rgb.IsDark(), true) 309 | 310 | rgb, _ = RGB(255, 255, 255) 311 | Equal(t, rgb.IsLight(), true) 312 | Equal(t, rgb.IsDark(), false) 313 | 314 | rgba, _ := RGBA(0, 0, 0, 1) 315 | Equal(t, rgba.IsLight(), false) 316 | Equal(t, rgba.IsDark(), true) 317 | 318 | rgba, _ = RGBA(255, 255, 255, 1) 319 | Equal(t, rgba.IsLight(), true) 320 | Equal(t, rgba.IsDark(), false) 321 | 322 | hex, _ := ParseHEX("#99FF33") 323 | Equal(t, hex.IsLight(), true) 324 | Equal(t, hex.IsDark(), false) 325 | 326 | hex, _ = ParseHEX("#3300FF") 327 | Equal(t, hex.IsLight(), false) 328 | Equal(t, hex.IsDark(), true) 329 | } 330 | 331 | func TestIsLightAlphaIsDarkAlpha(t *testing.T) { 332 | 333 | bg, _ := RGB(255, 255, 255) 334 | 335 | rgba, _ := RGBA(0, 0, 0, 1) 336 | Equal(t, rgba.IsLightAlpha(bg), false) 337 | Equal(t, rgba.IsDarkAlpha(bg), true) 338 | 339 | rgba, _ = RGBA(0, 0, 0, 0) 340 | Equal(t, rgba.IsLightAlpha(bg), true) 341 | Equal(t, rgba.IsDarkAlpha(bg), false) 342 | 343 | rgba, _ = RGBA(255, 255, 255, 1) 344 | Equal(t, rgba.IsLightAlpha(bg), true) 345 | Equal(t, rgba.IsDarkAlpha(bg), false) 346 | 347 | rgba, _ = RGBA(0, 0, 0, 0.5) 348 | Equal(t, rgba.IsLightAlpha(bg), false) 349 | Equal(t, rgba.IsDarkAlpha(bg), true) 350 | 351 | rgba, _ = RGBA(0, 0, 0, 0.3) 352 | Equal(t, rgba.IsLightAlpha(bg), true) 353 | Equal(t, rgba.IsDarkAlpha(bg), false) 354 | 355 | rgba, _ = RGBA(240, 100, 20, 0.5) 356 | Equal(t, rgba.IsLightAlpha(bg), true) 357 | Equal(t, rgba.IsDarkAlpha(bg), false) 358 | 359 | bg, _ = RGB(0, 0, 0) 360 | 361 | rgba, _ = RGBA(132, 100, 50, 0.5) 362 | Equal(t, rgba.IsLightAlpha(bg), false) 363 | Equal(t, rgba.IsDarkAlpha(bg), true) 364 | 365 | rgba, _ = RGBA(132, 100, 50, 0.7) 366 | Equal(t, rgba.IsLightAlpha(bg), false) 367 | Equal(t, rgba.IsDarkAlpha(bg), true) 368 | } 369 | 370 | func TestInterfaceTypes(t *testing.T) { 371 | 372 | fn := func(c Color) string { 373 | 374 | if c == nil { 375 | return "" 376 | } 377 | 378 | c.IsDark() 379 | c.IsLight() 380 | 381 | return c.String() 382 | } 383 | 384 | hex, _ := ParseHEX("#FFF") 385 | rgb, _ := ParseRGB("rgb(95,85,245)") 386 | rgba, _ := ParseRGBA("rgba(95,85,245,1)") 387 | 388 | fn(hex) 389 | fn(rgb) 390 | fn(rgba) 391 | } 392 | 393 | func BenchmarkSpeed(b *testing.B) { 394 | 395 | for n := 0; n < b.N; n++ { 396 | h, _ := ParseHEX("#FFFFFF") 397 | h.ToRGBA() 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-playground/colors 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /hex.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | hexRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" 11 | hexFormat = "#%02x%02x%02x" 12 | hexShortFormat = "#%1x%1x%1x" 13 | hexToRGBFactor = 17 14 | ) 15 | 16 | var ( 17 | hexRegex = regexp.MustCompile(hexRegexString) 18 | ) 19 | 20 | // HEXColor represents a HEX color 21 | type HEXColor struct { 22 | hex string 23 | } 24 | 25 | // ParseHEX validates an parses the provided string into a HEXColor object 26 | func ParseHEX(s string) (*HEXColor, error) { 27 | 28 | s = strings.ToLower(s) 29 | 30 | if !hexRegex.MatchString(s) { 31 | return nil, ErrBadColor 32 | } 33 | 34 | return &HEXColor{hex: s}, nil 35 | } 36 | 37 | // String returns the string representation on the HEXColor 38 | func (c *HEXColor) String() string { 39 | return c.hex 40 | } 41 | 42 | // ToHEX converts the HEXColor to a HEXColor 43 | // it's here to satisfy the Color interface 44 | func (c *HEXColor) ToHEX() *HEXColor { 45 | return c 46 | } 47 | 48 | // ToRGB converts the HEXColor to and RGBColor 49 | func (c *HEXColor) ToRGB() *RGBColor { 50 | 51 | var r, g, b uint8 52 | 53 | if len(c.hex) == 4 { 54 | fmt.Sscanf(c.hex, hexShortFormat, &r, &g, &b) 55 | r *= hexToRGBFactor 56 | g *= hexToRGBFactor 57 | b *= hexToRGBFactor 58 | } else { 59 | fmt.Sscanf(c.hex, hexFormat, &r, &g, &b) 60 | } 61 | 62 | return &RGBColor{R: r, G: g, B: b} 63 | } 64 | 65 | // ToRGBA converts the HEXColor to an RGBAColor 66 | func (c *HEXColor) ToRGBA() *RGBAColor { 67 | 68 | rgb := c.ToRGB() 69 | 70 | return &RGBAColor{R: rgb.R, G: rgb.G, B: rgb.B, A: 1} 71 | } 72 | 73 | // IsLight returns whether the color is perceived to be a light color 74 | func (c *HEXColor) IsLight() bool { 75 | return c.ToRGB().IsLight() 76 | } 77 | 78 | // IsDark returns whether the color is perceived to be a dark color 79 | func (c *HEXColor) IsDark() bool { 80 | return !c.IsLight() 81 | } 82 | 83 | // RGBA implements color.Color interface. 84 | // It returns the red, green, blue and alpha values for the color. Each value ranges within [0, 0xffff] 85 | func (c *HEXColor) RGBA() (r, g, b, a uint32) { 86 | return c.ToRGBA().RGBA() 87 | } 88 | 89 | // Equal reports whether c is the same color as d 90 | func (c *HEXColor) Equal(d Color) bool { 91 | return c.ToRGBA().String() == d.ToRGBA().String() 92 | } 93 | -------------------------------------------------------------------------------- /rgb.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | rgbString = "rgb(%d,%d,%d)" 13 | rgbCaptureRegexString = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$" 14 | rgbCaptureRegexPercentString = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*\\)$" 15 | ) 16 | 17 | var ( 18 | rgbCaptureRegex = regexp.MustCompile(rgbCaptureRegexString) 19 | rgbCapturePercentRegex = regexp.MustCompile(rgbCaptureRegexPercentString) 20 | ) 21 | 22 | // RGBColor represents an RGB color 23 | type RGBColor struct { 24 | R uint8 25 | G uint8 26 | B uint8 27 | } 28 | 29 | // ParseRGB validates an parses the provided string into an RGBColor object 30 | // supports both RGB 255 and RGB as percentages 31 | func ParseRGB(s string) (*RGBColor, error) { 32 | 33 | s = strings.ToLower(s) 34 | 35 | var isPercent bool 36 | vals := rgbCaptureRegex.FindAllStringSubmatch(s, -1) 37 | 38 | if len(vals) == 0 || len(vals[0]) == 0 { 39 | 40 | vals = rgbCapturePercentRegex.FindAllStringSubmatch(s, -1) 41 | 42 | if len(vals) == 0 || len(vals[0]) == 0 { 43 | return nil, ErrBadColor 44 | } 45 | 46 | isPercent = true 47 | } 48 | 49 | r, _ := strconv.ParseUint(vals[0][1], 10, 8) 50 | g, _ := strconv.ParseUint(vals[0][2], 10, 8) 51 | b, _ := strconv.ParseUint(vals[0][3], 10, 8) 52 | 53 | if isPercent { 54 | r = uint64(math.Floor(float64(r)/100*255 + .5)) 55 | g = uint64(math.Floor(float64(g)/100*255 + .5)) 56 | b = uint64(math.Floor(float64(b)/100*255 + .5)) 57 | } 58 | 59 | return &RGBColor{R: uint8(r), G: uint8(g), B: uint8(b)}, nil 60 | } 61 | 62 | // RGB validates and returns a new RGBColor object from the provided r, g, b values 63 | func RGB(r, g, b uint8) (*RGBColor, error) { 64 | return &RGBColor{R: r, G: g, B: b}, nil 65 | } 66 | 67 | // String returns the string representation on the RGBColor 68 | func (c *RGBColor) String() string { 69 | return fmt.Sprintf(rgbString, c.R, c.G, c.B) 70 | } 71 | 72 | // ToHEX converts the RGBColor to a HEXColor 73 | func (c *RGBColor) ToHEX() *HEXColor { 74 | return &HEXColor{hex: fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)} 75 | } 76 | 77 | // ToRGB converts the RGBColor to an RGBColor 78 | // it's here to satisfy the Color interface 79 | func (c *RGBColor) ToRGB() *RGBColor { 80 | return c 81 | } 82 | 83 | // ToRGBA converts the RGBColor to an RGBAColor 84 | func (c *RGBColor) ToRGBA() *RGBAColor { 85 | return &RGBAColor{R: c.R, G: c.G, B: c.B, A: 1} 86 | } 87 | 88 | // IsLight returns whether the color is perceived to be a light color 89 | func (c *RGBColor) IsLight() bool { 90 | 91 | r := float64(c.R) 92 | g := float64(c.G) 93 | b := float64(c.B) 94 | 95 | hsp := math.Sqrt(0.299*math.Pow(r, 2) + 0.587*math.Pow(g, 2) + 0.114*math.Pow(b, 2)) 96 | 97 | return hsp > 130 98 | } 99 | 100 | // IsDark returns whether the color is perceived to be a dark color 101 | func (c *RGBColor) IsDark() bool { 102 | return !c.IsLight() 103 | } 104 | 105 | // RGBA implements color.Color interface. 106 | // It returns the red, green, blue and alpha values for the color. Each value ranges within [0, 0xffff] 107 | func (c *RGBColor) RGBA() (r, g, b, a uint32) { 108 | return c.ToRGBA().RGBA() 109 | } 110 | 111 | // Equal reports whether c is the same color as d 112 | func (c *RGBColor) Equal(d Color) bool { 113 | return c.ToRGBA().String() == d.ToRGBA().String() 114 | } 115 | -------------------------------------------------------------------------------- /rgba.go: -------------------------------------------------------------------------------- 1 | package colors 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | rgbaString = "rgba(%d,%d,%d,%g)" 14 | rgbaCaptureRegexString = "^rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0\\.[0-9]*|[01])\\s*\\)$" 15 | rgbaCaptureRegexPercentString = "^rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(0\\.[0-9]*|[01])\\s*\\)$" 16 | ) 17 | 18 | var ( 19 | rgbaCaptureRegex = regexp.MustCompile(rgbaCaptureRegexString) 20 | rgbaCapturePercentRegex = regexp.MustCompile(rgbaCaptureRegexPercentString) 21 | ) 22 | 23 | // RGBAColor represents an RGBA color 24 | type RGBAColor struct { 25 | R uint8 26 | G uint8 27 | B uint8 28 | A float64 29 | } 30 | 31 | // ParseRGBA validates an parses the provided string into an RGBAColor object 32 | // supports both RGBA 255 and RGBA as percentages 33 | func ParseRGBA(s string) (*RGBAColor, error) { 34 | 35 | s = strings.ToLower(s) 36 | 37 | var isPercent bool 38 | 39 | vals := rgbaCaptureRegex.FindAllStringSubmatch(s, -1) 40 | 41 | if len(vals) == 0 || len(vals[0]) == 0 { 42 | 43 | vals = rgbaCapturePercentRegex.FindAllStringSubmatch(s, -1) 44 | 45 | if len(vals) == 0 || len(vals[0]) == 0 { 46 | return nil, ErrBadColor 47 | } 48 | 49 | isPercent = true 50 | } 51 | 52 | r, _ := strconv.ParseUint(vals[0][1], 10, 8) 53 | g, _ := strconv.ParseUint(vals[0][2], 10, 8) 54 | b, _ := strconv.ParseUint(vals[0][3], 10, 8) 55 | a, _ := strconv.ParseFloat(vals[0][4], 64) 56 | 57 | if isPercent { 58 | r = uint64(math.Floor(float64(r)/100*255 + .5)) 59 | g = uint64(math.Floor(float64(g)/100*255 + .5)) 60 | b = uint64(math.Floor(float64(b)/100*255 + .5)) 61 | } 62 | 63 | return &RGBAColor{R: uint8(r), G: uint8(g), B: uint8(b), A: a}, nil 64 | } 65 | 66 | // RGBA validates and returns a new RGBAColor object from the provided r, g, b, a values 67 | func RGBA(r, g, b uint8, a float64) (*RGBAColor, error) { 68 | 69 | if a < 0 || a > 1 { 70 | return nil, ErrBadColor 71 | } 72 | 73 | return &RGBAColor{R: r, G: g, B: b, A: a}, nil 74 | } 75 | 76 | func FromStdColor(c color.Color) *RGBAColor { 77 | r, g, b, a := c.RGBA() 78 | return &RGBAColor{ 79 | R: uint8(r >> 8), 80 | G: uint8(g >> 8), 81 | B: uint8(b >> 8), 82 | A: float64(a>>8) / 0xff, 83 | } 84 | } 85 | 86 | // String returns the string representation on the RGBAColor 87 | func (c *RGBAColor) String() string { 88 | return fmt.Sprintf(rgbaString, c.R, c.G, c.B, c.A) 89 | } 90 | 91 | // ToHEX converts the RGBAColor to a HEXColor 92 | func (c *RGBAColor) ToHEX() *HEXColor { 93 | return &HEXColor{hex: fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)} 94 | } 95 | 96 | // ToRGB converts the RGBAColor to an RGBColor 97 | func (c *RGBAColor) ToRGB() *RGBColor { 98 | return &RGBColor{R: c.R, G: c.G, B: c.B} 99 | } 100 | 101 | // ToRGBA converts the RGBAColor to an RGBAColor 102 | // it's here to satisfy the Color interface 103 | func (c *RGBAColor) ToRGBA() *RGBAColor { 104 | return c 105 | } 106 | 107 | // IsLight returns whether the color is perceived to be a light color 108 | // NOTE: this is determined only by the RGB values, if you need to take 109 | // the alpha into account see the IsLightAlpha function 110 | func (c *RGBAColor) IsLight() bool { 111 | return c.ToRGB().IsLight() 112 | } 113 | 114 | // IsDark returns whether the color is perceived to be a dark color 115 | // NOTE: this is determined only by the RGB values, if you need to take 116 | // the alpha into account see the IsLightAlpha function 117 | func (c *RGBAColor) IsDark() bool { 118 | return !c.IsLight() 119 | } 120 | 121 | // IsLightAlpha returns whether the color is perceived to be a light color 122 | // based on RGBA values and the provided background color 123 | // algorithm based of of post here: http://stackoverflow.com/a/12228643/3158232 124 | func (c *RGBAColor) IsLightAlpha(bg Color) bool { 125 | 126 | // if alpha is 1 then RGB3 == RGB1 127 | if c.A == 1 { 128 | return c.IsLight() 129 | } 130 | 131 | // if alpha is 0 then RGB3 == RGB2 132 | if c.A == 0 { 133 | return bg.IsLight() 134 | } 135 | 136 | rgb2 := bg.ToRGB() 137 | 138 | r1 := float64(c.R) 139 | g1 := float64(c.G) 140 | b1 := float64(c.B) 141 | r2 := float64(rgb2.R) 142 | g2 := float64(rgb2.G) 143 | b2 := float64(rgb2.B) 144 | 145 | r3 := r2 + (r1-r2)*c.A 146 | g3 := g2 + (g1-g2)*c.A 147 | b3 := b2 + (b1-b2)*c.A 148 | 149 | rgb, _ := RGB(uint8(r3), uint8(g3), uint8(b3)) 150 | 151 | return rgb.IsLight() 152 | } 153 | 154 | // IsDarkAlpha returns whether the color is perceived to be a dark color 155 | // based on RGBA values and the provided background color 156 | // algorithm based of of post here: http://stackoverflow.com/a/12228643/3158232 157 | func (c *RGBAColor) IsDarkAlpha(bg Color) bool { 158 | return !c.IsLightAlpha(bg) 159 | } 160 | 161 | // RGBA implements color.Color interface. 162 | // It returns the red, green, blue and alpha values for the color. Each value ranges within [0, 0xffff] 163 | func (c *RGBAColor) RGBA() (r, g, b, a uint32) { 164 | r = uint32(c.R) 165 | r |= r << 8 166 | g = uint32(c.G) 167 | g |= g << 8 168 | b = uint32(c.B) 169 | b |= b << 8 170 | a = uint32(c.A*0xffff + .5) 171 | return r, g, b, a 172 | } 173 | 174 | // Equal reports whether c is the same color as d 175 | func (c *RGBAColor) Equal(d Color) bool { 176 | return c.ToRGBA().String() == d.ToRGBA().String() 177 | } 178 | --------------------------------------------------------------------------------