package jdenticon import ( "fmt" "reflect" "strconv" "github.com/kevin/go-jdenticon/internal/engine" "github.com/kevin/go-jdenticon/internal/renderer" ) // iconRenderer defines the common interface for rendering identicons to different formats type iconRenderer interface { SetBackground(fillColor string, opacity float64) BeginShape(color string) AddPolygon(points []engine.Point) AddCircle(topLeft engine.Point, size float64, invert bool) EndShape() } // Icon represents a generated identicon that can be rendered in various formats. type Icon struct { icon *engine.Icon } // renderTo renders the icon to the given renderer, handling all common rendering logic func (i *Icon) renderTo(r iconRenderer) { if i.icon == nil { return } // Set background color if configured if i.icon.Config.BackColor != nil { r.SetBackground(i.icon.Config.BackColor.String(), 1.0) } // Render each shape group for _, group := range i.icon.Shapes { r.BeginShape(group.Color.String()) for _, shape := range group.Shapes { // Skip empty shapes if shape.Type == "empty" { continue } switch shape.Type { case "polygon": // Transform points transformedPoints := make([]engine.Point, len(shape.Points)) for j, point := range shape.Points { transformedPoints[j] = shape.Transform.TransformIconPoint(point.X, point.Y, 0, 0) } r.AddPolygon(transformedPoints) case "circle": // Use dedicated circle fields - CircleX, CircleY represent top-left corner topLeft := shape.Transform.TransformIconPoint(shape.CircleX, shape.CircleY, 0, 0) r.AddCircle(topLeft, shape.CircleSize, shape.Invert) } } r.EndShape() } } // Generate creates an identicon for the given input value and size. // The input value is typically an email address, username, or any string // that should produce a consistent visual representation. func Generate(value string, size int) (*Icon, error) { // Compute hash from the input value hash := ComputeHash(value) // Create generator with default configuration generator := engine.NewDefaultGenerator() // Generate the icon engineIcon, err := generator.Generate(hash, float64(size)) if err != nil { return nil, err } return &Icon{icon: engineIcon}, nil } // ToSVG renders the icon as an SVG string. func (i *Icon) ToSVG() (string, error) { if i.icon == nil { return "", nil } svgRenderer := renderer.NewSVGRenderer(int(i.icon.Size)) i.renderTo(svgRenderer) return svgRenderer.ToSVG(), nil } // ToPNG renders the icon as PNG image data. func (i *Icon) ToPNG() ([]byte, error) { if i.icon == nil { return nil, nil } pngRenderer := renderer.NewPNGRenderer(int(i.icon.Size)) i.renderTo(pngRenderer) return pngRenderer.ToPNG(), nil } // ToSVG generates an identicon as an SVG string for the given input value. // The value can be any type - it will be converted to a string and hashed. // Size specifies the icon size in pixels. // Optional config parameters can be provided to customize the appearance. func ToSVG(value interface{}, size int, config ...Config) (string, error) { if size <= 0 { return "", fmt.Errorf("size must be positive, got %d", size) } // Generate icon with the provided configuration icon, err := generateWithConfig(value, size, config...) if err != nil { return "", fmt.Errorf("failed to generate icon: %w", err) } // Render as SVG svg, err := icon.ToSVG() if err != nil { return "", fmt.Errorf("failed to render SVG: %w", err) } return svg, nil } // ToPNG generates an identicon as PNG image data for the given input value. // The value can be any type - it will be converted to a string and hashed. // Size specifies the icon size in pixels. // Optional config parameters can be provided to customize the appearance. func ToPNG(value interface{}, size int, config ...Config) ([]byte, error) { if size <= 0 { return nil, fmt.Errorf("size must be positive, got %d", size) } // Generate icon with the provided configuration icon, err := generateWithConfig(value, size, config...) if err != nil { return nil, fmt.Errorf("failed to generate icon: %w", err) } // Render as PNG png, err := icon.ToPNG() if err != nil { return nil, fmt.Errorf("failed to render PNG: %w", err) } return png, nil } // ToHash generates a hash string for the given input value. // This is a convenience function that wraps ComputeHash with better type handling. // The hash can be used with other functions or stored for consistent icon generation. func ToHash(value interface{}) string { return ComputeHash(value) } // generateWithConfig is a helper function that creates an icon with optional configuration. func generateWithConfig(value interface{}, size int, configs ...Config) (*Icon, error) { // Convert value to string representation stringValue := convertToString(value) // Compute hash from the input value hash := ComputeHash(stringValue) // Create generator with configuration var generator *engine.Generator if len(configs) > 0 { // Use the provided configuration engineConfig := convertToEngineConfig(configs[0]) generator = engine.NewGenerator(engineConfig) } else { // Use default configuration generator = engine.NewDefaultGenerator() } // Generate the icon engineIcon, err := generator.Generate(hash, float64(size)) if err != nil { return nil, err } return &Icon{icon: engineIcon}, nil } // convertToString converts any value to its string representation using reflection. // This handles various types similar to how JavaScript would convert them. func convertToString(value interface{}) string { if value == nil { return "" } // Use reflection to handle different types v := reflect.ValueOf(value) switch v.Kind() { case reflect.String: return v.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return fmt.Sprintf("%d", v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return fmt.Sprintf("%d", v.Uint()) case reflect.Float32, reflect.Float64: return fmt.Sprintf("%g", v.Float()) case reflect.Bool: if v.Bool() { return "true" } return "false" case reflect.Slice: // Handle byte slices specially if v.Type().Elem().Kind() == reflect.Uint8 { return string(v.Bytes()) } fallthrough default: // For all other types, use fmt.Sprintf return fmt.Sprintf("%v", value) } } // convertToEngineConfig converts a public Config to an internal engine.ColorConfig. func convertToEngineConfig(config Config) engine.ColorConfig { engineConfig := engine.DefaultColorConfig() // Convert hue restrictions if len(config.HueRestrictions) > 0 { engineConfig.Hues = make([]float64, len(config.HueRestrictions)) copy(engineConfig.Hues, config.HueRestrictions) } // Convert lightness ranges engineConfig.ColorLightness = engine.LightnessRange{ Min: config.ColorLightnessRange[0], Max: config.ColorLightnessRange[1], } engineConfig.GrayscaleLightness = engine.LightnessRange{ Min: config.GrayscaleLightnessRange[0], Max: config.GrayscaleLightnessRange[1], } // Convert saturation values engineConfig.ColorSaturation = config.ColorSaturation engineConfig.GrayscaleSaturation = config.GrayscaleSaturation // Convert background color if config.BackgroundColor != "" { normalizedColor, err := ParseColor(config.BackgroundColor) if err == nil { // Parse the normalized hex color into an engine.Color color, parseErr := parseHexToEngineColor(normalizedColor) if parseErr == nil { engineConfig.BackColor = &color } } } // Convert padding engineConfig.IconPadding = config.Padding return engineConfig } // parseHexToEngineColor converts a hex color string to an engine.Color. func parseHexToEngineColor(hexColor string) (engine.Color, error) { if hexColor == "" { return engine.Color{}, fmt.Errorf("empty color string") } // Remove # if present if len(hexColor) > 0 && hexColor[0] == '#' { hexColor = hexColor[1:] } var r, g, b, a uint8 = 0, 0, 0, 255 switch len(hexColor) { case 6: // RRGGBB rgb, err := parseHexComponent(hexColor[0:2]) if err != nil { return engine.Color{}, err } r = rgb rgb, err = parseHexComponent(hexColor[2:4]) if err != nil { return engine.Color{}, err } g = rgb rgb, err = parseHexComponent(hexColor[4:6]) if err != nil { return engine.Color{}, err } b = rgb case 8: // RRGGBBAA rgb, err := parseHexComponent(hexColor[0:2]) if err != nil { return engine.Color{}, err } r = rgb rgb, err = parseHexComponent(hexColor[2:4]) if err != nil { return engine.Color{}, err } g = rgb rgb, err = parseHexComponent(hexColor[4:6]) if err != nil { return engine.Color{}, err } b = rgb rgb, err = parseHexComponent(hexColor[6:8]) if err != nil { return engine.Color{}, err } a = rgb default: return engine.Color{}, fmt.Errorf("invalid hex color length: %d", len(hexColor)) } return engine.NewColorRGBA(r, g, b, a), nil } // parseHexComponent parses a 2-character hex string to a uint8 value. func parseHexComponent(hex string) (uint8, error) { if len(hex) != 2 { return 0, fmt.Errorf("hex component must be 2 characters, got %d", len(hex)) } value, err := strconv.ParseUint(hex, 16, 8) if err != nil { return 0, fmt.Errorf("invalid hex component '%s': %w", hex, err) } return uint8(value), nil }