Files
go-jdenticon/jdenticon/generate.go
Kevin McIntyre f84b511895 init
2025-06-18 01:00:00 -04:00

344 lines
9.2 KiB
Go

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
}