- 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
179 lines
6.9 KiB
Go
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
|
|
}
|