...

Source file src/github.com/mazznoer/csscolorparser/colorparser.go

Documentation: github.com/mazznoer/csscolorparser

     1  // Package csscolorparser provides function for parsing CSS color string as defined in the W3C's CSS color module level 4.
     2  package csscolorparser
     3  
     4  import (
     5  	"fmt"
     6  	"math"
     7  	"strconv"
     8  	"strings"
     9  )
    10  
    11  // Inspired by https://github.com/deanm/css-color-parser-js
    12  
    13  // R, G, B, A values in the range 0..1
    14  type Color struct {
    15  	R, G, B, A float64
    16  }
    17  
    18  // Implement the Go color.Color interface.
    19  func (c Color) RGBA() (r, g, b, a uint32) {
    20  	r = uint32(c.R*c.A*65535 + 0.5)
    21  	g = uint32(c.G*c.A*65535 + 0.5)
    22  	b = uint32(c.B*c.A*65535 + 0.5)
    23  	a = uint32(c.A*65535 + 0.5)
    24  	return
    25  }
    26  
    27  // RGBA255 returns R, G, B, A values in the range 0..255
    28  func (c Color) RGBA255() (r, g, b, a uint8) {
    29  	r = uint8(c.R*255 + 0.5)
    30  	g = uint8(c.G*255 + 0.5)
    31  	b = uint8(c.B*255 + 0.5)
    32  	a = uint8(c.A*255 + 0.5)
    33  	return
    34  }
    35  
    36  // HexString returns CSS hexadecimal string.
    37  func (c Color) HexString() string {
    38  	r, g, b, a := c.RGBA255()
    39  	if a < 255 {
    40  		return fmt.Sprintf("#%02x%02x%02x%02x", r, g, b, a)
    41  	}
    42  	return fmt.Sprintf("#%02x%02x%02x", r, g, b)
    43  }
    44  
    45  // RGBString returns CSS RGB string.
    46  func (c Color) RGBString() string {
    47  	r, g, b, _ := c.RGBA255()
    48  	if c.A < 1 {
    49  		return fmt.Sprintf("rgba(%d,%d,%d,%v)", r, g, b, c.A)
    50  	}
    51  	return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b)
    52  }
    53  
    54  // Name returns name of this color if its available.
    55  func (c Color) Name() (string, bool) {
    56  	r, g, b, _ := c.RGBA255()
    57  	rgb := [3]uint8{r, g, b}
    58  	for k, v := range namedColors {
    59  		if v == rgb {
    60  			return k, true
    61  		}
    62  	}
    63  	return "", false
    64  }
    65  
    66  // Implement the Go TextUnmarshaler interface
    67  func (c *Color) UnmarshalText(text []byte) error {
    68  	col, err := Parse(string(text))
    69  	if err != nil {
    70  		return err
    71  	}
    72  	c.R = col.R
    73  	c.G = col.G
    74  	c.B = col.B
    75  	c.A = col.A
    76  	return nil
    77  }
    78  
    79  // Implement the Go TextMarshaler interface
    80  func (c Color) MarshalText() ([]byte, error) {
    81  	return []byte(c.HexString()), nil
    82  }
    83  
    84  var black = Color{0, 0, 0, 1}
    85  
    86  // Parse parses CSS color string and returns, if successful, a Color.
    87  func Parse(s string) (Color, error) {
    88  	input := s
    89  	s = strings.TrimSpace(strings.ToLower(s))
    90  
    91  	if s == "transparent" {
    92  		return Color{0, 0, 0, 0}, nil
    93  	}
    94  
    95  	// Predefined name / keyword
    96  	c, ok := namedColors[s]
    97  	if ok {
    98  		return Color{float64(c[0]) / 255, float64(c[1]) / 255, float64(c[2]) / 255, 1}, nil
    99  	}
   100  
   101  	// Hexadecimal
   102  	if strings.HasPrefix(s, "#") {
   103  		c, ok := parseHex(s[1:])
   104  		if ok {
   105  			return c, nil
   106  		}
   107  		return black, fmt.Errorf("Invalid hex color, %s", input)
   108  	}
   109  
   110  	op := strings.Index(s, "(")
   111  
   112  	if (op != -1) && strings.HasSuffix(s, ")") {
   113  		fname := strings.TrimSpace(s[:op])
   114  		alpha := 1.0
   115  		okA := true
   116  		s = s[op+1 : len(s)-1]
   117  		s = strings.ReplaceAll(s, ",", " ")
   118  		s = strings.ReplaceAll(s, "/", " ")
   119  		params := strings.Fields(s)
   120  
   121  		if fname == "rgb" || fname == "rgba" {
   122  			if len(params) != 3 && len(params) != 4 {
   123  				return black, fmt.Errorf("%s() format needs 3 or 4 parameters, %s", fname, input)
   124  			}
   125  			r, okR := parsePercentOr255(params[0])
   126  			g, okG := parsePercentOr255(params[1])
   127  			b, okB := parsePercentOr255(params[2])
   128  			if len(params) == 4 {
   129  				alpha, okA = parsePercentOrFloat(params[3])
   130  			}
   131  			if okR && okG && okB && okA {
   132  				return Color{
   133  					clamp0_1(r),
   134  					clamp0_1(g),
   135  					clamp0_1(b),
   136  					clamp0_1(alpha),
   137  				}, nil
   138  			}
   139  			return black, fmt.Errorf("Wrong %s() components, %s", fname, input)
   140  
   141  		} else if fname == "hsl" || fname == "hsla" {
   142  			if len(params) != 3 && len(params) != 4 {
   143  				return black, fmt.Errorf("%s() format needs 3 or 4 parameters, %s", fname, input)
   144  			}
   145  			h, okH := parseAngle(params[0])
   146  			s, okS := parsePercentOrFloat(params[1])
   147  			l, okL := parsePercentOrFloat(params[2])
   148  			if len(params) == 4 {
   149  				alpha, okA = parsePercentOrFloat(params[3])
   150  			}
   151  			if okH && okS && okL && okA {
   152  				r, g, b := hslToRgb(normalizeAngle(h), clamp0_1(s), clamp0_1(l))
   153  				return Color{r, g, b, clamp0_1(alpha)}, nil
   154  			}
   155  			return black, fmt.Errorf("Wrong %s() components, %s", fname, input)
   156  
   157  		} else if fname == "hwb" || fname == "hwba" {
   158  			if len(params) != 3 && len(params) != 4 {
   159  				return black, fmt.Errorf("hwb() format needs 3 or 4 parameters, %s", input)
   160  			}
   161  			H, okH := parseAngle(params[0])
   162  			W, okW := parsePercentOrFloat(params[1])
   163  			B, okB := parsePercentOrFloat(params[2])
   164  			if len(params) == 4 {
   165  				alpha, okA = parsePercentOrFloat(params[3])
   166  			}
   167  			if okH && okW && okB && okA {
   168  				r, g, b := hwbToRgb(normalizeAngle(H), clamp0_1(W), clamp0_1(B))
   169  				return Color{r, g, b, clamp0_1(alpha)}, nil
   170  			}
   171  			return black, fmt.Errorf("Wrong hwb() components, %s", input)
   172  
   173  		} else if fname == "hsv" || fname == "hsva" {
   174  			if len(params) != 3 && len(params) != 4 {
   175  				return black, fmt.Errorf("hsv() format needs 3 or 4 parameters, %s", input)
   176  			}
   177  			h, okH := parseAngle(params[0])
   178  			s, okS := parsePercentOrFloat(params[1])
   179  			v, okV := parsePercentOrFloat(params[2])
   180  			if len(params) == 4 {
   181  				alpha, okA = parsePercentOrFloat(params[3])
   182  			}
   183  			if okH && okS && okV && okA {
   184  				r, g, b := hsvToRgb(normalizeAngle(h), clamp0_1(s), clamp0_1(v))
   185  				return Color{r, g, b, clamp0_1(alpha)}, nil
   186  			}
   187  			return black, fmt.Errorf("Wrong hsv() components, %s", input)
   188  		}
   189  	}
   190  
   191  	// RGB hexadecimal format without '#' prefix
   192  	c2, ok2 := parseHex(s)
   193  	if ok2 {
   194  		return c2, nil
   195  	}
   196  
   197  	return black, fmt.Errorf("Invalid color format, %s", input)
   198  }
   199  
   200  // https://stackoverflow.com/questions/54197913/parse-hex-string-to-image-color
   201  
   202  func parseHex(s string) (c Color, ok bool) {
   203  	c.A = 1
   204  	ok = true
   205  
   206  	hexToByte := func(b byte) byte {
   207  		switch {
   208  		case b >= '0' && b <= '9':
   209  			return b - '0'
   210  		case b >= 'a' && b <= 'f':
   211  			return b - 'a' + 10
   212  		}
   213  		ok = false
   214  		return 0
   215  	}
   216  
   217  	n := len(s)
   218  	if n == 6 || n == 8 {
   219  		c.R = float64(hexToByte(s[0])<<4+hexToByte(s[1])) / 255
   220  		c.G = float64(hexToByte(s[2])<<4+hexToByte(s[3])) / 255
   221  		c.B = float64(hexToByte(s[4])<<4+hexToByte(s[5])) / 255
   222  		if n == 8 {
   223  			c.A = float64(hexToByte(s[6])<<4+hexToByte(s[7])) / 255
   224  		}
   225  	} else if n == 3 || n == 4 {
   226  		c.R = float64(hexToByte(s[0])*17) / 255
   227  		c.G = float64(hexToByte(s[1])*17) / 255
   228  		c.B = float64(hexToByte(s[2])*17) / 255
   229  		if n == 4 {
   230  			c.A = float64(hexToByte(s[3])*17) / 255
   231  		}
   232  	} else {
   233  		ok = false
   234  	}
   235  	return
   236  }
   237  
   238  func modulo(x, y float64) float64 {
   239  	return math.Mod(math.Mod(x, y)+y, y)
   240  }
   241  
   242  func hueToRgb(n1, n2, h float64) float64 {
   243  	h = modulo(h, 6)
   244  	if h < 1 {
   245  		return n1 + ((n2 - n1) * h)
   246  	}
   247  	if h < 3 {
   248  		return n2
   249  	}
   250  	if h < 4 {
   251  		return n1 + ((n2 - n1) * (4 - h))
   252  	}
   253  	return n1
   254  }
   255  
   256  // h = 0..360
   257  // s, l = 0..1
   258  // r, g, b = 0..1
   259  func hslToRgb(h, s, l float64) (r, g, b float64) {
   260  	if s == 0 {
   261  		return l, l, l
   262  	}
   263  	var n2 float64
   264  	if l < 0.5 {
   265  		n2 = l * (1 + s)
   266  	} else {
   267  		n2 = l + s - (l * s)
   268  	}
   269  	n1 := 2*l - n2
   270  	h /= 60
   271  	r = clamp0_1(hueToRgb(n1, n2, h+2))
   272  	g = clamp0_1(hueToRgb(n1, n2, h))
   273  	b = clamp0_1(hueToRgb(n1, n2, h-2))
   274  	return
   275  }
   276  
   277  func hwbToRgb(hue, white, black float64) (r, g, b float64) {
   278  	if white+black >= 1 {
   279  		gray := white / (white + black)
   280  		return gray, gray, gray
   281  	}
   282  	r, g, b = hslToRgb(hue, 1, 0.5)
   283  	r = r*(1-white-black) + white
   284  	g = g*(1-white-black) + white
   285  	b = b*(1-white-black) + white
   286  	return
   287  }
   288  
   289  func hsvToHsl(H, S, V float64) (h, s, l float64) {
   290  	h = H
   291  	s = S
   292  	l = (2 - S) * V / 2
   293  	if l != 0 {
   294  		if l == 1 {
   295  			s = 0
   296  		} else if l < 0.5 {
   297  			s = S * V / (l * 2)
   298  		} else {
   299  			s = S * V / (2 - l*2)
   300  		}
   301  	}
   302  	return
   303  }
   304  
   305  func hsvToRgb(H, S, V float64) (r, g, b float64) {
   306  	h, s, l := hsvToHsl(H, S, V)
   307  	return hslToRgb(h, s, l)
   308  }
   309  
   310  func clamp0_1(t float64) float64 {
   311  	if t < 0 {
   312  		return 0
   313  	}
   314  	if t > 1 {
   315  		return 1
   316  	}
   317  	return t
   318  }
   319  
   320  func parseFloat(s string) (float64, bool) {
   321  	f, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
   322  	return f, err == nil
   323  }
   324  
   325  func parsePercentOrFloat(s string) (float64, bool) {
   326  	if strings.HasSuffix(s, "%") {
   327  		f, ok := parseFloat(s[:len(s)-1])
   328  		if ok {
   329  			return f / 100, true
   330  		}
   331  		return 0, false
   332  	}
   333  	return parseFloat(s)
   334  }
   335  
   336  func parsePercentOr255(s string) (float64, bool) {
   337  	if strings.HasSuffix(s, "%") {
   338  		f, ok := parseFloat(s[:len(s)-1])
   339  		if ok {
   340  			return f / 100, true
   341  		}
   342  		return 0, false
   343  	}
   344  	f, ok := parseFloat(s)
   345  	if ok {
   346  		return f / 255, true
   347  	}
   348  	return 0, false
   349  }
   350  
   351  // Result angle in degrees (not normalized)
   352  func parseAngle(s string) (float64, bool) {
   353  	if strings.HasSuffix(s, "deg") {
   354  		return parseFloat(s[:len(s)-3])
   355  	}
   356  	if strings.HasSuffix(s, "grad") {
   357  		f, ok := parseFloat(s[:len(s)-4])
   358  		if ok {
   359  			return f / 400 * 360, true
   360  		}
   361  		return 0, false
   362  	}
   363  	if strings.HasSuffix(s, "rad") {
   364  		f, ok := parseFloat(s[:len(s)-3])
   365  		if ok {
   366  			return f / math.Pi * 180, true
   367  		}
   368  		return 0, false
   369  	}
   370  	if strings.HasSuffix(s, "turn") {
   371  		f, ok := parseFloat(s[:len(s)-4])
   372  		if ok {
   373  			return f * 360, true
   374  		}
   375  		return 0, false
   376  	}
   377  	return parseFloat(s)
   378  }
   379  
   380  func normalizeAngle(t float64) float64 {
   381  	t = math.Mod(t, 360)
   382  	if t < 0 {
   383  		t += 360
   384  	}
   385  	return t
   386  }
   387  

View as plain text