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 }