Files
go-jdenticon/internal/engine/colorutils.go
Kevin McIntyre d9e84812ff Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
2026-01-03 23:41:48 -05:00

179 lines
6.9 KiB
Go

package engine
import (
"fmt"
"image/color"
"regexp"
"strconv"
"sync"
)
var (
// Compiled regex pattern for hex color validation
hexColorRegex *regexp.Regexp
// Initialization guard for hex color regex
hexColorRegexOnce sync.Once
)
// getHexColorRegex returns the compiled hex color regex pattern, compiling it only once.
// Supports formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
func getHexColorRegex() *regexp.Regexp {
hexColorRegexOnce.Do(func() {
hexColorRegex = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$`)
})
return hexColorRegex
}
// ParseHexColorToRGBA is the consolidated hex color parsing function for the entire codebase.
// It parses a hexadecimal color string and returns color.RGBA and an error.
// Supports formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
// Returns error if the format is invalid.
//
// This function replaces all other hex color parsing implementations and provides
// consistent error handling for all color operations, following REQ-1.3.
func ParseHexColorToRGBA(hexStr string) (color.RGBA, error) {
if len(hexStr) == 0 || hexStr[0] != '#' {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: invalid color format: %s", hexStr)
}
// Validate the hex color format using regex
if !getHexColorRegex().MatchString(hexStr) {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: invalid hex color format: %s", hexStr)
}
hex := hexStr[1:] // Remove '#' prefix
var r, g, b, a uint8 = 0, 0, 0, 255 // Default alpha is fully opaque
// Helper to parse a 2-character hex component
parse := func(target *uint8, hexStr string) error {
val, err := hexToByte(hexStr)
if err != nil {
return err
}
*target = val
return nil
}
// Helper to parse a single hex digit and expand it (e.g., 'F' -> 'FF' = 255)
parseShort := func(target *uint8, hexChar byte) error {
var val uint8
if hexChar >= '0' && hexChar <= '9' {
val = hexChar - '0'
} else if hexChar >= 'a' && hexChar <= 'f' {
val = hexChar - 'a' + 10
} else if hexChar >= 'A' && hexChar <= 'F' {
val = hexChar - 'A' + 10
} else {
return fmt.Errorf("jdenticon: engine: hex digit parsing failed: invalid hex character: %c", hexChar)
}
*target = val * 17 // Expand single digit: 0xF * 17 = 0xFF
return nil
}
switch len(hex) {
case 3: // #RGB -> expand to #RRGGBB
if err := parseShort(&r, hex[0]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse red component: %w", err)
}
if err := parseShort(&g, hex[1]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse green component: %w", err)
}
if err := parseShort(&b, hex[2]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse blue component: %w", err)
}
case 4: // #RGBA -> expand to #RRGGBBAA
if err := parseShort(&r, hex[0]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse red component: %w", err)
}
if err := parseShort(&g, hex[1]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse green component: %w", err)
}
if err := parseShort(&b, hex[2]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse blue component: %w", err)
}
if err := parseShort(&a, hex[3]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse alpha component: %w", err)
}
case 6: // #RRGGBB
if err := parse(&r, hex[0:2]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse red component: %w", err)
}
if err := parse(&g, hex[2:4]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse green component: %w", err)
}
if err := parse(&b, hex[4:6]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse blue component: %w", err)
}
case 8: // #RRGGBBAA
if err := parse(&r, hex[0:2]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse red component: %w", err)
}
if err := parse(&g, hex[2:4]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse green component: %w", err)
}
if err := parse(&b, hex[4:6]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse blue component: %w", err)
}
if err := parse(&a, hex[6:8]); err != nil {
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: failed to parse alpha component: %w", err)
}
default:
// This case should be unreachable due to the regex validation above.
// Return an error instead of panicking to ensure library never panics.
return color.RGBA{}, fmt.Errorf("jdenticon: engine: hex color parsing failed: unsupported color format with length %d", len(hex))
}
return color.RGBA{R: r, G: g, B: b, A: a}, nil
}
// ValidateHexColor validates that a color string is a valid hex color format.
// Returns nil if valid, error if invalid.
func ValidateHexColor(hexStr string) error {
if !getHexColorRegex().MatchString(hexStr) {
return fmt.Errorf("jdenticon: engine: hex color validation failed: color must be a hex color like #fff, #ffffff, or #ffffff80")
}
return nil
}
// ParseHexColorToEngine parses a hex color string and returns an engine.Color.
// This is a convenience function for converting hex colors to the engine's internal Color type.
func ParseHexColorToEngine(hexStr string) (Color, error) {
rgba, err := ParseHexColorToRGBA(hexStr)
if err != nil {
return Color{}, err
}
return NewColorRGBA(rgba.R, rgba.G, rgba.B, rgba.A), nil
}
// ParseHexColorForRenderer parses a hex color for use in renderers.
// Returns color.RGBA with the specified opacity applied.
// This function provides compatibility with the fast PNG renderer's parseColor function.
func ParseHexColorForRenderer(hexStr string, opacity float64) (color.RGBA, error) {
rgba, err := ParseHexColorToRGBA(hexStr)
if err != nil {
return color.RGBA{}, err
}
// Apply opacity to the alpha channel
rgba.A = uint8(float64(rgba.A) * opacity)
return rgba, nil
}
// hexToByte converts a 2-character hex string to a byte value.
// This is a helper function used by ParseHexColor.
func hexToByte(hex string) (uint8, error) {
if len(hex) != 2 {
return 0, fmt.Errorf("jdenticon: engine: hex byte parsing failed: invalid hex string length: expected 2 characters, got %d", len(hex))
}
n, err := strconv.ParseUint(hex, 16, 8)
if err != nil {
return 0, fmt.Errorf("jdenticon: engine: hex byte parsing failed: invalid hex value '%s': %w", hex, err)
}
return uint8(n), nil
}