@@ -5,68 +5,142 @@ package cameron
55
66import (
77 "bytes"
8+ "crypto/md5"
89 "image"
910 "image/color"
10-
11- "github.com/cespare/xxhash/v2"
11+ "math"
1212)
1313
14- // Identicon returns an identicon avatar based on the data with the length and
15- // blockLength.
16- func Identicon (data []byte , length , blockLength int ) image.Image {
17- digest := xxhash .Sum64 (data )
18- img := image .NewPaletted (
19- image .Rect (0 , 0 , length , length ),
20- color.Palette {
21- color.NRGBA {
22- R : byte (digest ),
23- G : byte (digest >> 8 ),
24- B : byte (digest >> 16 ),
25- A : 0xff ,
26- },
27- color.NRGBA {
28- R : 0xff ^ byte (digest ),
29- G : 0xff ^ byte (digest >> 8 ),
30- B : 0xff ^ byte (digest >> 16 ),
31- A : 0xff ,
32- },
33- },
34- )
14+ // Identicon returns an identicon avatar as an [image.Image] that is visually
15+ // identical to https://github.com/identicons/{login}.png. All geometric rules,
16+ // color calculations, and pixel layouts match the implementation GitHub uses
17+ // in production.
18+ //
19+ // Note that the final image is a square of 6*cell pixels: five grid
20+ // columns/rows plus a half-cell margin on every side.
21+ func Identicon (data []byte , cell int ) image.Image {
22+ digest := md5 .Sum (data )
23+ if cell < 1 {
24+ cell = 1
25+ }
3526
36- if blockLength > length {
37- blockLength = length
27+ // Split the 16-byte digest into 32 individual 4-bit nibbles.
28+ var nib [32 ]byte
29+ for i := 0 ; i < 16 ; i ++ {
30+ nib [2 * i ] = digest [i ] >> 4 // High 4 bits.
31+ nib [2 * i + 1 ] = digest [i ] & 0x0F // Low 4 bits.
3832 }
3933
40- columnsCount := length / blockLength
41- padding := blockLength / 2
42- if length % blockLength != 0 {
43- padding = (length - blockLength * columnsCount ) / 2
44- } else if columnsCount > 1 {
45- columnsCount --
46- } else {
47- padding = 0
34+ // Build the 5×5 symmetry mask.
35+ //
36+ // The first 15 nibbles decide the left half of the grid:
37+ // - Nibbles 0–4 fill the center column (column index 2).
38+ // - Nibbles 5–9 fill the column immediately left of center (index 1).
39+ // - Nibbles 10–14 fill the left-most column (index 0).
40+ //
41+ // A pixel is set only when its nibble value is even.
42+ //
43+ // Once the left half is filled, copy it to columns 3 and 4 to
44+ // complete the grid and guarantee horizontal symmetry.
45+ var mask [5 ][5 ]bool
46+ for i := 0 ; i < 15 ; i ++ {
47+ if nib [i ]% 2 == 0 {
48+ row := i % 5
49+ col := 2 - i / 5
50+ mask [row ][col ] = true
51+ }
52+ }
53+ for r := 0 ; r < 5 ; r ++ {
54+ mask [r ][3 ] = mask [r ][1 ]
55+ mask [r ][4 ] = mask [r ][0 ]
4856 }
4957
50- filled := columnsCount == 1
51- pixels := bytes .Repeat ([]byte {1 }, blockLength )
52- for i , ri , ci := 0 , 0 , 0 ; i < columnsCount * (columnsCount + 1 )/ 2 ; i ++ {
53- if filled || digest >> uint (i % 64 )& 1 == 1 {
54- for i := 0 ; i < blockLength ; i ++ {
55- x := padding + ri * blockLength
56- y := padding + ci * blockLength + i
57- copy (img .Pix [img .PixOffset (x , y ):], pixels )
58+ // Derive the foreground color from HSL.
59+ //
60+ // The final 7 nibbles are interpreted as HHHSSLL, where:
61+ // - HHH (12 bits) maps to hue in [0, 360) degrees.
62+ // - SS (8 bits) maps to saturation in [45, 65] percent.
63+ // - LL (8 bits) maps to lightness in [55, 75] percent.
64+ var v uint32
65+ for i := 25 ; i < 32 ; i ++ {
66+ v = (v << 4 ) | uint32 (nib [i ])
67+ }
68+ hueBits := v >> 16
69+ satBits := (v >> 8 ) & 0xFF
70+ lgtBits := v & 0xFF
71+ h := float64 (hueBits ) * 360 / 4095
72+ s := 65.0 - float64 (satBits )* 20 / 255
73+ l := 75.0 - float64 (lgtBits )* 20 / 255
74+ fg := hslToNRGBA (h , s , l )
5875
59- x = padding + (columnsCount - 1 - ri )* blockLength
60- copy (img .Pix [img .PixOffset (x , y ):], pixels )
76+ // Use a light gray background as in GitHub's implementation.
77+ bg := color.NRGBA {R : 240 , G : 240 , B : 240 , A : 255 }
78+
79+ // Allocate the palette-based image and fill it.
80+ //
81+ // The bitmap is six logical cells per side: five pattern cells plus a
82+ // half-cell margin on each edge. Using a palette keeps memory small.
83+ size := 6 * cell
84+ img := image .NewPaletted (image .Rect (0 , 0 , size , size ), color.Palette {bg , fg })
85+ margin := cell / 2 // Half-cell margin in pixels.
86+ rowBuf := bytes .Repeat ([]byte {1 }, cell ) // Palette index 1 is fg.
87+ for r := 0 ; r < 5 ; r ++ {
88+ for c := 0 ; c < 5 ; c ++ {
89+ if ! mask [r ][c ] {
90+ continue
91+ }
92+ x := margin + c * cell
93+ y := margin + r * cell
94+ for dy := 0 ; dy < cell ; dy ++ {
95+ off := img .PixOffset (x , y + dy )
96+ copy (img .Pix [off :], rowBuf )
6197 }
6298 }
99+ }
100+ return img
101+ }
63102
64- ci ++
65- if ci == columnsCount {
66- ci = 0
67- ri ++
68- }
103+ // hslToNRGBA converts HSL values to an opaque [color.NRGBA].
104+ func hslToNRGBA (h , s , l float64 ) color.NRGBA {
105+ h /= 360
106+ s /= 100
107+ l /= 100
108+
109+ var q float64
110+ if l < 0.5 {
111+ q = l * (1 + s )
112+ } else {
113+ q = l + s - l * s
69114 }
115+ p := 2 * l - q
70116
71- return img
117+ r := hueToRGB (p , q , h + 1.0 / 3.0 )
118+ g := hueToRGB (p , q , h )
119+ b := hueToRGB (p , q , h - 1.0 / 3.0 )
120+ return color.NRGBA {
121+ R : uint8 (math .Round (r * 255 )),
122+ G : uint8 (math .Round (g * 255 )),
123+ B : uint8 (math .Round (b * 255 )),
124+ A : 255 ,
125+ }
126+ }
127+
128+ // hueToRGB converts a hue offset t into a single RGB component.
129+ func hueToRGB (p , q , t float64 ) float64 {
130+ if t < 0 {
131+ t += 1
132+ }
133+ if t > 1 {
134+ t -= 1
135+ }
136+ switch {
137+ case t < 1.0 / 6.0 :
138+ return p + (q - p )* 6 * t
139+ case t < 1.0 / 2.0 :
140+ return q
141+ case t < 2.0 / 3.0 :
142+ return p + (q - p )* (2.0 / 3.0 - t )* 6
143+ default :
144+ return p
145+ }
72146}
0 commit comments