init
This commit is contained in:
344
jdenticon/generate.go
Normal file
344
jdenticon/generate.go
Normal file
@@ -0,0 +1,344 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user