diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-08-22 20:36:56 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-08-22 20:36:56 +0200 |
| commit | f84e22433a9dbee4d34a22a266e028e545bdd8fd (patch) | |
| tree | 936af34d6bb2a0fb32814345418d6d4038dc6f2f /identicons-cli.go | |
| download | identicons-web-f84e22433a9dbee4d34a22a266e028e545bdd8fd.tar.gz identicons-web-f84e22433a9dbee4d34a22a266e028e545bdd8fd.tar.xz identicons-web-f84e22433a9dbee4d34a22a266e028e545bdd8fd.zip | |
Diffstat (limited to 'identicons-cli.go')
| -rw-r--r-- | identicons-cli.go | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/identicons-cli.go b/identicons-cli.go new file mode 100644 index 0000000..05a7667 --- /dev/null +++ b/identicons-cli.go @@ -0,0 +1,442 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "flag" + "fmt" + "image" + "image/color" + "image/png" + "os" +) + +// OptimizedIdenticon with indexed colors for smaller file sizes +type OptimizedIdenticon struct { + source []byte + size int +} + +// NewOptimizedIdenticon creates a generator with indexed colors +func NewOptimizedIdenticon(source []byte) *OptimizedIdenticon { + return &OptimizedIdenticon{ + source: source, + size: 256, + } +} + +// NewOptimizedIdenticonWithSize creates a generator with custom size +func NewOptimizedIdenticonWithSize(source []byte, size int) *OptimizedIdenticon { + return &OptimizedIdenticon{ + source: source, + size: size, + } +} + +// getBit returns the n-th bit (0-indexed) from source +func (identicon *OptimizedIdenticon) getBit(n int) bool { + if len(identicon.source) == 0 || n < 0 { + return false + } + byteIndex := n / 8 + bitIndex := n % 8 + if byteIndex >= len(identicon.source) { + return false + } + return (identicon.source[byteIndex]>>bitIndex)&1 == 1 +} + +// getByte returns the n-th byte, wraps around if needed +func (identicon *OptimizedIdenticon) getByte(n int) byte { + if len(identicon.source) == 0 { + return 0 + } + return identicon.source[n%len(identicon.source)] +} + +// getColorIndices returns color indices for indexed version +func (identicon *OptimizedIdenticon) getColorIndices() (primaryIndex, secondaryIndex, bgIndex uint8) { + if len(identicon.source) < 32 { + return 0, 1, 2 + } + + // Primary color index (4 bits → 16 colors) + primaryIndex = 0 + for i := 0; i < 4; i++ { + if identicon.getBit(248 + i) { + primaryIndex |= 1 << i + } + } + primaryIndex %= 16 + + // Secondary color index (4 bits → 16 colors) + secondaryIndex = 0 + for i := 0; i < 4; i++ { + if identicon.getBit(244 + i) { + secondaryIndex |= 1 << i + } + } + secondaryIndex %= 16 + + // Background choice (2 bits → 4 options) + bgChoice := 0 + for i := 0; i < 2; i++ { + if identicon.getBit(252 + i) { + bgChoice |= 1 << i + } + } + bgIndex = uint8(bgChoice % 3) // 0, 1, or 2 + + return primaryIndex, secondaryIndex, bgIndex +} + +// generatePixelPattern generates 5x5 symmetric pixel grid +func (identicon *OptimizedIdenticon) generatePixelPattern() ([]bool, []bool) { + primary := make([]bool, 25) + secondary := make([]bool, 25) + + // Use bits 0-14 for primary pattern + bitIndex := 0 + for row := 0; row < 5; row++ { + for col := 0; col < 3; col++ { + paint := identicon.getBit(bitIndex) + bitIndex++ + + ix := row*5 + col + mirrorIx := row*5 + (4 - col) + primary[ix] = paint + primary[mirrorIx] = paint + } + } + + // Use bits 15-29 for secondary pattern + for row := 0; row < 5; row++ { + for col := 0; col < 3; col++ { + paint := identicon.getBit(bitIndex) + bitIndex++ + + ix := row*5 + col + mirrorIx := row*5 + (4 - col) + secondary[ix] = paint + secondary[mirrorIx] = paint + } + } + + return primary, secondary +} + +// createPalette creates an optimized palette with only necessary colors +func createPalette(primaryIdx, secondaryIdx, bgIdx uint8, darkMode bool) color.Palette { + // Optimized color palettes - only 16 colors per palette + primaryPalette := []color.Color{ + color.RGBA{0x00, 0xbf, 0x93, 0xff}, // turquoise + color.RGBA{0x2d, 0xcc, 0x70, 0xff}, // mint + color.RGBA{0x42, 0xe4, 0x53, 0xff}, // green + color.RGBA{0xf1, 0xc4, 0x0f, 0xff}, // yellowOrange + color.RGBA{0xe6, 0x7f, 0x22, 0xff}, // brown + color.RGBA{0xff, 0x94, 0x4e, 0xff}, // orange + color.RGBA{0xe8, 0x4c, 0x3d, 0xff}, // red + color.RGBA{0x35, 0x98, 0xdb, 0xff}, // blue + color.RGBA{0x9a, 0x59, 0xb5, 0xff}, // purple + color.RGBA{0xef, 0x3e, 0x96, 0xff}, // magenta + color.RGBA{0xdf, 0x21, 0xb9, 0xff}, // violet + color.RGBA{0x7d, 0xc2, 0xd2, 0xff}, // lightBlue + color.RGBA{0x16, 0xa0, 0x86, 0xff}, // turquoiseIntense + color.RGBA{0x27, 0xae, 0x61, 0xff}, // mintIntense + color.RGBA{0x24, 0xc3, 0x33, 0xff}, // greenIntense + color.RGBA{0x1c, 0xab, 0xbb, 0xff}, // lightBlueIntense + } + + secondaryPalette := []color.Color{ + color.RGBA{0x34, 0x49, 0x5e, 0xff}, // darkBlue + color.RGBA{0x95, 0xa5, 0xa5, 0xff}, // grey + color.RGBA{0xd2, 0x54, 0x00, 0xff}, // brownIntense + color.RGBA{0xc1, 0x39, 0x2b, 0xff}, // redIntense + color.RGBA{0x29, 0x7f, 0xb8, 0xff}, // blueIntense + color.RGBA{0x8d, 0x44, 0xad, 0xff}, // purpleIntense + color.RGBA{0xbe, 0x12, 0x7e, 0xff}, // violetIntense + color.RGBA{0xe5, 0x23, 0x83, 0xff}, // magentaIntense + color.RGBA{0x27, 0xae, 0x61, 0xff}, // mintIntense + color.RGBA{0x24, 0xc3, 0x33, 0xff}, // greenIntense + color.RGBA{0xd9, 0xd9, 0x21, 0xff}, // yellowIntense + color.RGBA{0xf3, 0x9c, 0x11, 0xff}, // yellowOrangeIntense + color.RGBA{0xff, 0x55, 0x00, 0xff}, // orangeIntense + color.RGBA{0x1c, 0xab, 0xbb, 0xff}, // lightBlueIntense + color.RGBA{0x23, 0x23, 0x23, 0xff}, // lightBlackIntense + color.RGBA{0x7e, 0x8c, 0x8d, 0xff}, // greyIntense + } + + // Background colors based on mode + lightBackgrounds := []color.Color{ + color.RGBA{255, 255, 255, 255}, // white + color.RGBA{243, 245, 247, 255}, // light gray 1 + color.RGBA{236, 240, 241, 255}, // light gray 2 + color.RGBA{0, 0, 0, 0}, // transparent (position 3) + } + + darkBackgrounds := []color.Color{ + color.RGBA{30, 30, 30, 255}, // dark gray + color.RGBA{45, 62, 80, 255}, // dark blue + color.RGBA{57, 57, 57, 255}, // dark gray 2 + color.RGBA{0, 0, 0, 0}, // transparent (position 3) + } + + // Palette in exact order: + // 0: Background + // 1: Primary color + // 2: Secondary color + // 3: Transparent (optional) + palette := make(color.Palette, 0, 4) + + // Background first + if darkMode { + palette = append(palette, darkBackgrounds[bgIdx]) + } else { + palette = append(palette, lightBackgrounds[bgIdx]) + } + + // Then primary and secondary colors + palette = append(palette, + primaryPalette[primaryIdx], + secondaryPalette[secondaryIdx], + color.RGBA{0, 0, 0, 0}, // transparent as last option + ) + + return palette +} + +// Generate48x48ForFace creates a 48x48 pixel image specifically for Face headers +func (identicon *OptimizedIdenticon) Generate48x48ForFace(transparent bool) *image.Paletted { + const ( + size = 48 + spriteSize = 5 + pixelSize = 6 // 48/8 = 6 + margin = (size - pixelSize*spriteSize) / 2 // = 9 + ) + + // Determine color indices + primaryIdx, secondaryIdx, bgIdx := identicon.getColorIndices() + + // For transparent background, set bgIdx to 3 (transparent) + if transparent { + bgIdx = 3 + } + + // Palette for export (always light mode for better compatibility) + palette := createPalette(primaryIdx, secondaryIdx, bgIdx, false) + + // Create image + img := image.NewPaletted(image.Rect(0, 0, size, size), palette) + + // Fill background + bgIndex := uint8(0) + if transparent { + bgIndex = 3 // transparent + } + + for i := 0; i < size; i++ { + for j := 0; j < size; j++ { + img.SetColorIndex(j, i, bgIndex) + } + } + + primaryPixels, secondaryPixels := identicon.generatePixelPattern() + + // Secondary pixels (index 2) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if secondaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + for py := y; py < y+pixelSize; py++ { + for px := x; px < x+pixelSize; px++ { + if px < size && py < size { + img.SetColorIndex(px, py, 2) + } + } + } + } + } + } + + // Primary pixels (index 1) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if primaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + for py := y; py < y+pixelSize; py++ { + for px := x; px < x+pixelSize; px++ { + if px < size && py < size { + img.SetColorIndex(px, py, 1) + } + } + } + } + } + } + + return img +} + +// GenerateForExportOptimized for indexed export (256x256) +func (identicon *OptimizedIdenticon) GenerateForExportOptimized(transparent bool) *image.Paletted { + const ( + spriteSize = 5 + ) + + pixelSize := identicon.size / 8 + margin := (identicon.size - pixelSize*spriteSize) / 2 + + // Determine color indices + primaryIdx, secondaryIdx, bgIdx := identicon.getColorIndices() + + // For transparent background, set bgIdx to 3 (transparent) + if transparent { + bgIdx = 3 + } + + // Palette for export (always light mode for better compatibility) + palette := createPalette(primaryIdx, secondaryIdx, bgIdx, false) + + // Create image + img := image.NewPaletted(image.Rect(0, 0, identicon.size, identicon.size), palette) + + // Fill background + bgIndex := uint8(0) + if transparent { + bgIndex = 3 // transparent + } + + for i := 0; i < identicon.size; i++ { + for j := 0; j < identicon.size; j++ { + img.SetColorIndex(j, i, bgIndex) + } + } + + primaryPixels, secondaryPixels := identicon.generatePixelPattern() + + // Secondary pixels (index 2) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if secondaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + for py := y; py < y+pixelSize; py++ { + for px := x; px < x+pixelSize; px++ { + if px < identicon.size && py < identicon.size { + img.SetColorIndex(px, py, 2) + } + } + } + } + } + } + + // Primary pixels (index 1) + for row := 0; row < spriteSize; row++ { + for col := 0; col < spriteSize; col++ { + if primaryPixels[row*spriteSize+col] { + x := col*pixelSize + margin + y := row*pixelSize + margin + for py := y; py < y+pixelSize; py++ { + for px := x; px < x+pixelSize; px++ { + if px < identicon.size && py < identicon.size { + img.SetColorIndex(px, py, 1) + } + } + } + } + } + } + + return img +} + +func main() { + // CLI flags + input := flag.String("input", "", "Input text (username|email|pubkey)") + size := flag.Int("size", 48, "Image size (48 or 256)") + transparent := flag.Bool("transparent", true, "Transparent background") + outputFormat := flag.String("format", "base64", "Output format: base64, dataurl, or png") + outputFile := flag.String("output", "", "Output file (for png format)") + hash := flag.String("hash", "", "Direct SHA256 hash (hex) instead of input") + + flag.Parse() + + // Validate input + if *input == "" && *hash == "" { + fmt.Fprintln(os.Stderr, "Error: -input or -hash required") + fmt.Fprintln(os.Stderr, "Usage: identicons-cli -input 'username|email|pubkey' [-size 48|256] [-transparent] [-format base64|dataurl|png] [-output file.png]") + os.Exit(1) + } + + // Generate hash + var hashBytes []byte + if *hash != "" { + // Use provided hash (hex string) + fmt.Sscanf(*hash, "%x", &hashBytes) + if len(hashBytes) != 32 { + fmt.Fprintln(os.Stderr, "Error: hash must be 32 bytes (64 hex chars)") + os.Exit(1) + } + } else { + // Hash the input + h := sha256.Sum256([]byte(*input)) + hashBytes = h[:] + } + + // Generate identicon + var img *image.Paletted + if *size == 48 { + identicon := NewOptimizedIdenticonWithSize(hashBytes, 48) + img = identicon.Generate48x48ForFace(*transparent) + } else if *size == 256 { + identicon := NewOptimizedIdenticonWithSize(hashBytes, 256) + img = identicon.GenerateForExportOptimized(*transparent) + } else { + fmt.Fprintln(os.Stderr, "Error: size must be 48 or 256") + os.Exit(1) + } + + // Encode to PNG + var buf bytes.Buffer + encoder := png.Encoder{ + CompressionLevel: png.BestCompression, + } + + if err := encoder.Encode(&buf, img); err != nil { + fmt.Fprintln(os.Stderr, "Error encoding PNG:", err) + os.Exit(1) + } + + // Output based on format + switch *outputFormat { + case "base64": + // Output base64 only (for Face header) + b64 := base64.StdEncoding.EncodeToString(buf.Bytes()) + fmt.Print(b64) + + case "dataurl": + // Output data URL (for web) + b64 := base64.StdEncoding.EncodeToString(buf.Bytes()) + fmt.Printf("data:image/png;base64,%s", b64) + + case "png": + // Output PNG file + if *outputFile == "" { + fmt.Fprintln(os.Stderr, "Error: -output required for png format") + os.Exit(1) + } + if err := os.WriteFile(*outputFile, buf.Bytes(), 0644); err != nil { + fmt.Fprintln(os.Stderr, "Error writing PNG:", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "PNG saved to %s (%d bytes)\n", *outputFile, buf.Len()) + + default: + fmt.Fprintln(os.Stderr, "Error: format must be base64, dataurl, or png") + os.Exit(1) + } +} |
