package jdenticon import ( "fmt" "regexp" ) // Config holds configuration options for identicon generation. type Config struct { // HueRestrictions specifies the hues (in degrees) that are allowed for the identicon. // If empty, any hue can be used. Values should be between 0 and 360. HueRestrictions []float64 // ColorLightnessRange specifies the lightness range for colored shapes [min, max]. // Values should be between 0.0 and 1.0. Default: [0.4, 0.8] ColorLightnessRange [2]float64 // GrayscaleLightnessRange specifies the lightness range for grayscale shapes [min, max]. // Values should be between 0.0 and 1.0. Default: [0.3, 0.9] GrayscaleLightnessRange [2]float64 // ColorSaturation controls the saturation of colored shapes. // Values should be between 0.0 and 1.0. Default: 0.5 ColorSaturation float64 // GrayscaleSaturation controls the saturation of grayscale shapes. // Values should be between 0.0 and 1.0. Default: 0.0 GrayscaleSaturation float64 // BackgroundColor sets the background color for the identicon. // Accepts hex colors like "#fff", "#ffffff", "#ffffff80" (with alpha). // If empty, the background will be transparent. BackgroundColor string // Padding controls the padding around the identicon as a percentage of the size. // Values should be between 0.0 and 0.5. Default: 0.08 Padding float64 } // DefaultConfig returns a default configuration that matches jdenticon-js defaults. func DefaultConfig() Config { return Config{ HueRestrictions: nil, // No restrictions ColorLightnessRange: [2]float64{0.4, 0.8}, GrayscaleLightnessRange: [2]float64{0.3, 0.9}, ColorSaturation: 0.5, GrayscaleSaturation: 0.0, BackgroundColor: "", // Transparent Padding: 0.08, } } // ConfigOption represents a configuration option function. type ConfigOption func(*Config) error // WithHueRestrictions sets the allowed hues in degrees (0-360). func WithHueRestrictions(hues []float64) ConfigOption { return func(c *Config) error { for _, hue := range hues { if hue < 0 || hue >= 360 { return fmt.Errorf("hue must be between 0 and 360, got %f", hue) } } c.HueRestrictions = make([]float64, len(hues)) copy(c.HueRestrictions, hues) return nil } } // WithColorLightnessRange sets the lightness range for colored shapes. func WithColorLightnessRange(min, max float64) ConfigOption { return func(c *Config) error { if min < 0 || min > 1 || max < 0 || max > 1 { return fmt.Errorf("lightness values must be between 0 and 1, got min=%f max=%f", min, max) } if min > max { return fmt.Errorf("minimum lightness cannot be greater than maximum, got min=%f max=%f", min, max) } c.ColorLightnessRange = [2]float64{min, max} return nil } } // WithGrayscaleLightnessRange sets the lightness range for grayscale shapes. func WithGrayscaleLightnessRange(min, max float64) ConfigOption { return func(c *Config) error { if min < 0 || min > 1 || max < 0 || max > 1 { return fmt.Errorf("lightness values must be between 0 and 1, got min=%f max=%f", min, max) } if min > max { return fmt.Errorf("minimum lightness cannot be greater than maximum, got min=%f max=%f", min, max) } c.GrayscaleLightnessRange = [2]float64{min, max} return nil } } // WithColorSaturation sets the saturation for colored shapes. func WithColorSaturation(saturation float64) ConfigOption { return func(c *Config) error { if saturation < 0 || saturation > 1 { return fmt.Errorf("saturation must be between 0 and 1, got %f", saturation) } c.ColorSaturation = saturation return nil } } // WithGrayscaleSaturation sets the saturation for grayscale shapes. func WithGrayscaleSaturation(saturation float64) ConfigOption { return func(c *Config) error { if saturation < 0 || saturation > 1 { return fmt.Errorf("saturation must be between 0 and 1, got %f", saturation) } c.GrayscaleSaturation = saturation return nil } } // WithBackgroundColor sets the background color. func WithBackgroundColor(color string) ConfigOption { return func(c *Config) error { if color != "" { if err := validateHexColor(color); err != nil { return fmt.Errorf("invalid background color: %w", err) } } c.BackgroundColor = color return nil } } // WithPadding sets the padding around the identicon. func WithPadding(padding float64) ConfigOption { return func(c *Config) error { if padding < 0 || padding > 0.5 { return fmt.Errorf("padding must be between 0 and 0.5, got %f", padding) } c.Padding = padding return nil } } // Configure creates a new configuration with the given options. func Configure(options ...ConfigOption) (Config, error) { config := DefaultConfig() for _, option := range options { if err := option(&config); err != nil { return Config{}, err } } return config, nil } // validateHexColor validates that a color string is a valid hex color. func validateHexColor(color string) error { hexColorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3,8}$`) if !hexColorPattern.MatchString(color) { return fmt.Errorf("color must be a hex color like #fff, #ffffff, or #ffffff80") } // Validate length - must be 3, 4, 6, or 8 characters after # length := len(color) - 1 if length != 3 && length != 4 && length != 6 && length != 8 { return fmt.Errorf("hex color must be 3, 4, 6, or 8 characters after #") } return nil } // ParseColor normalizes a hex color string to the full #RRGGBB or #RRGGBBAA format. func ParseColor(color string) (string, error) { if color == "" { return "", nil } if err := validateHexColor(color); err != nil { return "", err } // Normalize short forms to full forms switch len(color) { case 4: // #RGB -> #RRGGBB r, g, b := color[1], color[2], color[3] return fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b), nil case 5: // #RGBA -> #RRGGBBAA r, g, b, a := color[1], color[2], color[3], color[4] return fmt.Sprintf("#%c%c%c%c%c%c%c%c", r, r, g, g, b, b, a, a), nil case 7, 9: // #RRGGBB or #RRGGBBAA - already normalized return color, nil default: return "", fmt.Errorf("unsupported color format") } } // GetHue returns the hue for the given original hue value, taking into account hue restrictions. func (c *Config) GetHue(originalHue float64) float64 { if len(c.HueRestrictions) == 0 { return originalHue } // Map originalHue [0,1] to one of the restricted hues index := int(originalHue * 0.999 * float64(len(c.HueRestrictions))) if index >= len(c.HueRestrictions) { index = len(c.HueRestrictions) - 1 } hue := c.HueRestrictions[index] // Convert degrees to [0,1] range and normalize return ((hue/360.0)+1.0) - float64(int((hue/360.0)+1.0)) } // GetColorLightness returns a lightness value within the color lightness range. func (c *Config) GetColorLightness(value float64) float64 { return c.getLightness(value, c.ColorLightnessRange) } // GetGrayscaleLightness returns a lightness value within the grayscale lightness range. func (c *Config) GetGrayscaleLightness(value float64) float64 { return c.getLightness(value, c.GrayscaleLightnessRange) } // getLightness maps a value [0,1] to the specified lightness range. func (c *Config) getLightness(value float64, lightnessRange [2]float64) float64 { result := lightnessRange[0] + value*(lightnessRange[1]-lightnessRange[0]) if result < 0 { return 0 } if result > 1 { return 1 } return result }