// Package csscolorparser provides function for parsing CSS color string as defined in the W3C's CSS color module level 4.
package csscolorparser import ( ) // Inspired by https://github.com/deanm/css-color-parser-js // R, G, B, A values in the range 0..1 type Color struct { R, G, B, A float64 } // Implement the Go color.Color interface. func ( Color) () (, , , uint32) { = uint32(.R*.A*65535 + 0.5) = uint32(.G*.A*65535 + 0.5) = uint32(.B*.A*65535 + 0.5) = uint32(.A*65535 + 0.5) return } // RGBA255 returns R, G, B, A values in the range 0..255 func ( Color) () (, , , uint8) { = uint8(.R*255 + 0.5) = uint8(.G*255 + 0.5) = uint8(.B*255 + 0.5) = uint8(.A*255 + 0.5) return } func ( Color) () Color { return Color{ R: math.Max(math.Min(.R, 1), 0), G: math.Max(math.Min(.G, 1), 0), B: math.Max(math.Min(.B, 1), 0), A: math.Max(math.Min(.A, 1), 0), } } // HexString returns CSS hexadecimal string. func ( Color) () string { , , , := .RGBA255() if < 255 { return fmt.Sprintf("#%02x%02x%02x%02x", , , , ) } return fmt.Sprintf("#%02x%02x%02x", , , ) } // RGBString returns CSS RGB string. func ( Color) () string { , , , := .RGBA255() if .A < 1 { return fmt.Sprintf("rgba(%d,%d,%d,%v)", , , , .A) } return fmt.Sprintf("rgb(%d,%d,%d)", , , ) } // Name returns name of this color if its available. func ( Color) () (string, bool) { , , , := .RGBA255() := [3]uint8{, , } for , := range namedColors { if == { return , true } } return "", false } // Implement the Go TextUnmarshaler interface func ( *Color) ( []byte) error { , := Parse(string()) if != nil { return } .R = .R .G = .G .B = .B .A = .A return nil } // Implement the Go TextMarshaler interface func ( Color) () ([]byte, error) { return []byte(.HexString()), nil } func (, , , float64) Color { , , := hsvToRgb(normalizeAngle(), clamp0_1(), clamp0_1()) return Color{, , , clamp0_1()} } func (, , , float64) Color { , , := hslToRgb(normalizeAngle(), clamp0_1(), clamp0_1()) return Color{, , , clamp0_1()} } func (, , , float64) Color { , , := hwbToRgb(normalizeAngle(), clamp0_1(), clamp0_1()) return Color{, , , clamp0_1()} } func fromLinear( float64) float64 { if >= 0.0031308 { return 1.055*math.Pow(, 1.0/2.4) - 0.055 } return 12.92 * } func (, , , float64) Color { return Color{fromLinear(), fromLinear(), fromLinear(), clamp0_1()} } func (, , , float64) Color { := math.Pow(+0.3963377774*+0.2158037573*, 3) := math.Pow(-0.1055613458*-0.0638541728*, 3) := math.Pow(-0.0894841775*-1.2914855480*, 3) := 4.0767416621* - 3.3077115913* + 0.2309699292* := -1.2684380046* + 2.6097574011* - 0.3413193965* := -0.0041960863* - 0.7034186147* + 1.7076147010* return FromLinearRGB(, , , ) } func (, , , float64) Color { return FromOklab(, *math.Cos(), *math.Sin(), ) } var black = Color{0, 0, 0, 1} // Parse parses CSS color string and returns, if successful, a Color. func ( string) (Color, error) { := = strings.TrimSpace(strings.ToLower()) if == "transparent" { return Color{0, 0, 0, 0}, nil } // Predefined name / keyword , := namedColors[] if { return Color{float64([0]) / 255, float64([1]) / 255, float64([2]) / 255, 1}, nil } // Hexadecimal if strings.HasPrefix(, "#") { , := parseHex([1:]) if { return , nil } return black, fmt.Errorf("Invalid hex color, %s", ) } := strings.Index(, "(") if ( != -1) && strings.HasSuffix(, ")") { := strings.TrimSpace([:]) := 1.0 := true = [+1 : len()-1] = strings.ReplaceAll(, ",", " ") = strings.ReplaceAll(, "/", " ") := strings.Fields() if == "rgb" || == "rgba" { if len() != 3 && len() != 4 { return black, fmt.Errorf("%s() format needs 3 or 4 parameters, %s", , ) } , , := parsePercentOr255([0]) , , := parsePercentOr255([1]) , , := parsePercentOr255([2]) if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { return Color{ clamp0_1(), clamp0_1(), clamp0_1(), clamp0_1(), }, nil } return black, fmt.Errorf("Wrong %s() components, %s", , ) } else if == "hsl" || == "hsla" { if len() != 3 && len() != 4 { return black, fmt.Errorf("%s() format needs 3 or 4 parameters, %s", , ) } , := parseAngle([0]) , , := parsePercentOrFloat([1]) , , := parsePercentOrFloat([2]) if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { return FromHsl(, , , ), nil } return black, fmt.Errorf("Wrong %s() components, %s", , ) } else if == "hwb" || == "hwba" { if len() != 3 && len() != 4 { return black, fmt.Errorf("hwb() format needs 3 or 4 parameters, %s", ) } , := parseAngle([0]) , , := parsePercentOrFloat([1]) , , := parsePercentOrFloat([2]) if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { return FromHwb(, , , ), nil } return black, fmt.Errorf("Wrong hwb() components, %s", ) } else if == "hsv" || == "hsva" { if len() != 3 && len() != 4 { return black, fmt.Errorf("hsv() format needs 3 or 4 parameters, %s", ) } , := parseAngle([0]) , , := parsePercentOrFloat([1]) , , := parsePercentOrFloat([2]) if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { return FromHsv(, , , ), nil } return black, fmt.Errorf("Wrong hsv() components, %s", ) } else if == "oklab" { if len() != 3 && len() != 4 { return black, fmt.Errorf("oklab() format needs 3 or 4 parameters, %s", ) } , , := parsePercentOrFloat([0]) , , := parsePercentOrFloat([1]) , , := parsePercentOrFloat([2]) := true if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { if { = remap(, -1.0, 1.0, -0.4, 0.4) } if { = remap(, -1.0, 1.0, -0.4, 0.4) } return FromOklab(math.Max(, 0), , , ), nil } return black, fmt.Errorf("Wrong oklab() components, %s", ) } else if == "oklch" { if len() != 3 && len() != 4 { return black, fmt.Errorf("oklch() format needs 3 or 4 parameters, %s", ) } , , := parsePercentOrFloat([0]) , , := parsePercentOrFloat([1]) , := parseAngle([2]) if len() == 4 { , , _ = parsePercentOrFloat([3]) } if && && && { if { = * 0.4 } return FromOklch(math.Max(, 0), math.Max(, 0), *math.Pi/180, ), nil } return black, fmt.Errorf("Wrong oklch() components, %s", ) } } // RGB hexadecimal format without '#' prefix , := parseHex() if { return , nil } return black, fmt.Errorf("Invalid color format, %s", ) } // https://stackoverflow.com/questions/54197913/parse-hex-string-to-image-color func parseHex( string) ( Color, bool) { .A = 1 = true := func( byte) byte { switch { case >= '0' && <= '9': return - '0' case >= 'a' && <= 'f': return - 'a' + 10 } = false return 0 } := len() if == 6 || == 8 { .R = float64(([0])<<4+([1])) / 255 .G = float64(([2])<<4+([3])) / 255 .B = float64(([4])<<4+([5])) / 255 if == 8 { .A = float64(([6])<<4+([7])) / 255 } } else if == 3 || == 4 { .R = float64(([0])*17) / 255 .G = float64(([1])*17) / 255 .B = float64(([2])*17) / 255 if == 4 { .A = float64(([3])*17) / 255 } } else { = false } return } func modulo(, float64) float64 { return math.Mod(math.Mod(, )+, ) } func hueToRgb(, , float64) float64 { = modulo(, 6) if < 1 { return + (( - ) * ) } if < 3 { return } if < 4 { return + (( - ) * (4 - )) } return } // h = 0..360 // s, l = 0..1 // r, g, b = 0..1 func hslToRgb(, , float64) (, , float64) { if == 0 { return , , } var float64 if < 0.5 { = * (1 + ) } else { = + - ( * ) } := 2* - /= 60 = clamp0_1(hueToRgb(, , +2)) = clamp0_1(hueToRgb(, , )) = clamp0_1(hueToRgb(, , -2)) return } func hwbToRgb(, , float64) (, , float64) { if + >= 1 { := / ( + ) return , , } , , = hslToRgb(, 1, 0.5) = *(1--) + = *(1--) + = *(1--) + return } func hsvToHsl(, , float64) (, , float64) { = = = (2 - ) * / 2 if != 0 { if == 1 { = 0 } else if < 0.5 { = * / ( * 2) } else { = * / (2 - *2) } } return } func hsvToRgb(, , float64) (, , float64) { , , := hsvToHsl(, , ) return hslToRgb(, , ) } func clamp0_1( float64) float64 { if < 0 { return 0 } if > 1 { return 1 } return } func parseFloat( string) (float64, bool) { , := strconv.ParseFloat(strings.TrimSpace(), 64) return , == nil } // Returns (result, ok?, percentage?) func parsePercentOrFloat( string) (float64, bool, bool) { if strings.HasSuffix(, "%") { , := parseFloat([:len()-1]) if { return / 100, true, true } return 0, false, true } , := parseFloat() return , , false } // Returns (result, ok?, percentage?) func parsePercentOr255( string) (float64, bool, bool) { if strings.HasSuffix(, "%") { , := parseFloat([:len()-1]) if { return / 100, true, true } return 0, false, true } , := parseFloat() if { return / 255, true, false } return 0, false, false } // Result angle in degrees (not normalized) func parseAngle( string) (float64, bool) { if strings.HasSuffix(, "deg") { return parseFloat([:len()-3]) } if strings.HasSuffix(, "grad") { , := parseFloat([:len()-4]) if { return / 400 * 360, true } return 0, false } if strings.HasSuffix(, "rad") { , := parseFloat([:len()-3]) if { return / math.Pi * 180, true } return 0, false } if strings.HasSuffix(, "turn") { , := parseFloat([:len()-4]) if { return * 360, true } return 0, false } return parseFloat() } func normalizeAngle( float64) float64 { = math.Mod(, 360) if < 0 { += 360 } return } // Map t which is in range [a, b] to range [c, d] func remap(, , , , float64) float64 { return (-)*((-)/(-)) + }