grokify/mogo

View on GitHub
image/colors/convert.go

Summary

Maintainability
A
0 mins
Test Coverage
package colors

import (
    "fmt"
    "image"
    "image/color"
    "math"
    "regexp"
    "strconv"
    "strings"

    "github.com/grokify/mogo/errors/errorsutil"
    "github.com/grokify/mogo/type/stringsutil"
    "golang.org/x/image/colornames"
)

var (
    rxColorHex  = regexp.MustCompile(`^(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$`)
    rxGoogleCol = regexp.MustCompile(`^google([0-9]+)$`)
)

// Parse returns a `color.RGBA` given a color name or hex color code.
func Parse(colorName string) (color.RGBA, error) {
    colorName = strings.ToLower(strings.TrimSpace(colorName))
    if col, ok := colornames.Map[colorName]; ok {
        return col, nil
    }
    colorHTML, err := ParseHex(colorName)
    if err == nil {
        return colorHTML, nil
    }
    colorGoog, err := ParseGoogle(colorName)
    if err == nil {
        return colorGoog, nil
    }
    return color.RGBA{}, fmt.Errorf("E_COLOR_NOT_FOUND [%s]", colorName)
}

// MustParse returns a `color.RGBA` given a hex color code or Google color string.
// It panics if the input string cannot be parsed.
func MustParse(input string) color.RGBA {
    c, err := Parse(input)
    if err != nil {
        panic(err)
    }
    return c
}

func CanonicalHex(hexRGB string, upperCase, addHash bool) (string, error) {
    rgb, err := ParseHex(hexRGB)
    if err != nil {
        return "", err
    }
    hex := ColorRGBAToHex(rgb)
    if upperCase {
        hex = strings.ToUpper(hex)
    }
    if addHash {
        hex = "#" + hex
    }
    return hex, nil
}

func CanonicalHexes(hexRGBs []string, upperCase, addHash, dedupeResults, sortResults bool) ([]string, error) {
    var canonical []string
    for _, h := range hexRGBs {
        can, err := CanonicalHex(h, upperCase, addHash)
        if err != nil {
            return canonical, err
        }
        canonical = append(canonical, can)
    }
    if dedupeResults || sortResults {
        return stringsutil.SliceCondenseSpace(canonical, dedupeResults, sortResults), nil
    }

    return canonical, nil
}

// ParseHex returns a `color.RGBA` given a hex color code.
func ParseHex(hexRGB string) (color.RGBA, error) {
    m := rxColorHex.FindStringSubmatch(strings.ToLower(strings.TrimSpace(hexRGB)))
    if len(m) == 0 {
        return color.RGBA{}, fmt.Errorf("E_COLOR_NOT_HEX_STRING [%s]", hexRGB)
    }
    rdecimal, errR := strconv.ParseUint(m[1], 16, 64)
    gdecimal, errG := strconv.ParseUint(m[2], 16, 64)
    bdecimal, errB := strconv.ParseUint(m[3], 16, 64)
    err := errorsutil.Join(false, errR, errG, errB)
    if err != nil {
        return color.RGBA{}, fmt.Errorf("E_COLOR_NOT_HEX_PARSE [%s]", err.Error())
    }
    return color.RGBA{
        R: uint8(rdecimal),
        G: uint8(gdecimal),
        B: uint8(bdecimal),
        A: 0xff}, nil
}

func ParseGoogle(googString string) (color.RGBA, error) {
    m := rxGoogleCol.FindStringSubmatch(googString)
    if len(m) == 0 {
        return color.RGBA{},
            fmt.Errorf("E_COLOR_NOT_GOOG_MATCH [%s]", googString)
    }
    idxInt, err := strconv.Atoi(m[1])
    if err != nil {
        panic(err)
    }
    col := GoogleChartColorX(uint64(idxInt))
    return col, nil
}

func ColorRGBAToHex(c color.RGBA) string {
    return fmt.Sprintf("%02x%02x%02x", c.R, c.G, c.B)
}

// ColorToHex returns 6 byte hex code in lower case.
func ColorToHex(c color.Color) string {
    r, g, b, _ := c.RGBA()
    return fmt.Sprintf("%02x%02x%02x", uint8(r), uint8(g), uint8(b))
}

// ColorString returns a full 16-bit color representation.
func ColorString(c color.Color) string {
    r, g, b, a := c.RGBA()
    five0fmt := "%05d"
    return strings.Join(
        []string{
            fmt.Sprintf(five0fmt, int(r)),
            fmt.Sprintf(five0fmt, int(g)),
            fmt.Sprintf(five0fmt, int(b)),
            fmt.Sprintf(five0fmt, int(a)),
        }, ".")
}

func ColorAverageImage(i image.Image) color.Color {
    var r, g, b uint64

    bounds := i.Bounds()

    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            pr, pg, pb, _ := i.At(x, y).RGBA()

            r += uint64(pr * pr)
            g += uint64(pg * pg)
            b += uint64(pb * pg)
        }
    }

    d := uint64(bounds.Dy() * bounds.Dx())

    r /= d
    g /= d
    b /= d

    return color.RGBA{
        uint8(math.Sqrt(float64(r)) / 0x101),
        uint8(math.Sqrt(float64(g)) / 0x101),
        uint8(math.Sqrt(float64(b)) / 0x101),
        uint8(255)}
}

/*

https://jimsaunders.net/2015/05/22/manipulating-colors-in-go.html
https://sighack.com/post/averaging-rgb-colors-the-right-way

*/

func ColorAverage(c ...color.Color) color.Color {
    if len(c) == 0 {
        return color.Black
    }
    var r, g, b uint64

    for _, ci := range c {
        pr, pg, pb, _ := ci.RGBA()

        r += uint64(pr * pr)
        g += uint64(pg * pg)
        b += uint64(pb * pg)
    }

    d := uint64(len(c))

    r /= d
    g /= d
    b /= d

    return color.RGBA{
        uint8(math.Sqrt(float64(r)) / 0x101),
        uint8(math.Sqrt(float64(g)) / 0x101),
        uint8(math.Sqrt(float64(b)) / 0x101),
        uint8(255)}
}