1#![allow(
2 clippy::cast_precision_loss,
3 clippy::cast_lossless,
4 clippy::cast_possible_truncation,
5 clippy::cast_sign_loss
6)]
7
8mod config;
9mod hsl;
10
11pub use config::*;
12use hsl::corrected_hsl_to_rgb;
13use ril::prelude::*;
14pub use ril::{self, ImageFormat};
15
16struct ColorCandidates {
18 light_gray: Rgba,
19 dark_gray: Rgba,
20 light_color: Rgba,
21 mid_color: Rgba,
22 dark_color: Rgba,
23}
24
25impl ColorCandidates {
26 #[inline]
27 const fn get_from_rotation_index(&self, index: usize) -> Rgba {
28 match index {
29 0 => self.dark_gray,
30 1 => self.mid_color,
31 2 => self.light_gray,
32 3 => self.light_color,
33 _ => self.dark_color,
34 }
35 }
36}
37
38impl Config {
39 pub(crate) fn resolve_hue(&self, hue: f64) -> f64 {
41 if self.hues.is_empty() {
42 hue
43 } else {
44 self.hues[(hue / 360.0 * self.hues.len() as f64) as usize]
45 }
46 }
47
48 #[inline]
51 pub(crate) fn resolve_color_lightness(&self, lightness: f64) -> f64 {
52 (self.color_lightness.end() - self.color_lightness.start())
53 .mul_add(lightness, *self.color_lightness.start())
54 }
55
56 #[inline]
59 pub(crate) fn resolve_grayscale_lightness(&self, lightness: f64) -> f64 {
60 (self.grayscale_lightness.end() - self.grayscale_lightness.start())
61 .mul_add(lightness, *self.grayscale_lightness.start())
62 }
63
64 pub(crate) fn color_candidates(&self, hue: f64) -> ColorCandidates {
66 let hue = self.resolve_hue(hue);
67
68 macro_rules! resolve {
69 ($s:ident, $l_meth:ident, $l_value:literal) => {{
70 corrected_hsl_to_rgb(hue, self.$s, self.$l_meth($l_value)).into_rgba()
71 }};
72 (@grayscale $l_value:literal) => {{
73 resolve!(grayscale_saturation, resolve_grayscale_lightness, $l_value)
74 }};
75 (@color $l_value:literal) => {{
76 resolve!(color_saturation, resolve_color_lightness, $l_value)
77 }};
78 }
79
80 ColorCandidates {
81 light_gray: resolve!(@grayscale 1.0),
82 dark_gray: resolve!(@grayscale 0.0),
83 light_color: resolve!(@color 1.0),
84 mid_color: resolve!(@color 0.5),
85 dark_color: resolve!(@color 0.0),
86 }
87 }
88}
89
90#[derive(Copy, Clone, Default)]
92struct Transform {
93 x: u32,
94 y: u32,
95 pub rotation: u8,
96 right: u32,
97 bottom: u32,
98}
99
100impl Transform {
101 pub(crate) const fn new(x: u32, y: u32, size: u32, rotation: u8) -> Self {
102 Self {
103 x,
104 y,
105 rotation,
106 right: x + size,
107 bottom: y + size,
108 }
109 }
110
111 pub(crate) const fn transform(&self, (x, y): (u32, u32), (w, h): (u32, u32)) -> (u32, u32) {
112 match self.rotation {
113 0 => (self.x + x, self.y + y),
114 1 => (self.right - y - h, self.y + x),
115 2 => (self.right - x - w, self.bottom - y - h),
116 _ => (self.x + y, self.bottom - x - w),
117 }
118 }
119}
120
121struct ShapeRenderer<'a> {
122 image: &'a mut Image<Rgba>,
123 pub current_transform: Transform,
124}
125
126impl<'a> ShapeRenderer<'a> {
127 pub fn new(image: &'a mut Image<Rgba>) -> Self {
128 Self {
129 image,
130 current_transform: Transform::default(),
131 }
132 }
133
134 pub fn polygon(
135 &mut self,
136 color: Rgba,
137 points: impl IntoIterator<Item = (u32, u32)>,
138 ) -> &mut Self {
139 let polygon = Polygon::from_vertices(
140 points
141 .into_iter()
142 .map(|pos| self.current_transform.transform(pos, (0, 0))),
143 )
144 .with_fill(color);
145
146 self.image.draw(&polygon);
147 self
148 }
149
150 pub fn circle(&mut self, color: Rgba, top_left: (u32, u32), diameter: u32) -> &mut Self {
151 let (x, y) = self
152 .current_transform
153 .transform(top_left, (diameter, diameter));
154 let circle = Ellipse::from_bounding_box(x, y, x + diameter, y + diameter).with_fill(color);
155
156 self.image.draw(&circle);
157 self
158 }
159
160 pub fn triangle<const ROTATION: usize>(
163 &mut self,
164 color: Rgba,
165 (x, y): (u32, u32),
166 (w, h): (u32, u32),
167 ) -> &mut Self {
168 let (a, b, c, d) = ((x + w, y), (x + w, y + h), (x, y + h), (x, y));
169 let points = match ROTATION % 4 {
170 0 => [b, c, d],
171 1 => [a, c, d],
172 2 => [a, b, d],
173 3 => [a, b, c],
174 _ => unsafe { std::hint::unreachable_unchecked() },
176 };
177
178 self.polygon(color, points);
179 self
180 }
181
182 pub fn rectangle(
183 &mut self,
184 color: Rgba,
185 top_left: (u32, u32),
186 mut size: (u32, u32),
187 ) -> &mut Self {
188 let (x, y) = self.current_transform.transform(top_left, size);
189 if self.current_transform.rotation & 1 == 1 {
190 std::mem::swap(&mut size.0, &mut size.1);
191 }
192
193 let rect = Rectangle::new()
194 .with_position(x, y)
195 .with_size(size.0, size.1 + 1)
196 .with_fill(color);
197
198 self.image.draw(&rect);
199 self
200 }
201
202 pub fn rhombus(&mut self, color: Rgba, top_left: (u32, u32), size: (u32, u32)) -> &mut Self {
204 self.polygon(
205 color,
206 [
207 (top_left.0 + size.0 / 2, top_left.1),
208 (top_left.0 + size.0, top_left.1 + size.1 / 2),
209 (top_left.0 + size.0 / 2, top_left.1 + size.1),
210 (top_left.0, top_left.1 + size.1 / 2),
211 ],
212 )
213 }
214}
215
216#[inline]
217fn pad_zeroes<const FROM: usize, const TO: usize>(arr: [u8; FROM]) -> [u8; TO] {
218 let mut b = [0; TO];
219 b[TO - FROM..].copy_from_slice(&arr);
220 b
221}
222
223fn into_nibbles(hash: [u8; 20]) -> [u8; 40] {
224 let mut nibbles = [0; 40];
225 for i in 0..20 {
226 nibbles[i * 2] = hash[i] >> 4;
227 nibbles[i * 2 + 1] = hash[i] & 0x0f;
228 }
229 nibbles
230}
231
232#[inline]
233fn hash_substring_u32<const LEN: usize>(nibbles: &[u8; 40], start: usize) -> u32 {
234 let nibbles = pad_zeroes::<LEN, 8>(unsafe {
235 nibbles[start..start + LEN].try_into().unwrap_unchecked()
237 });
238 let mut bytes = [0; 4];
240 for i in 0..4 {
241 bytes[i] = nibbles[i * 2] << 4 | nibbles[i * 2 + 1];
242 }
243
244 u32::from_be_bytes(bytes)
245}
246
247#[allow(clippy::too_many_arguments)]
248fn render_shape(
249 hash: &[u8; 40],
250 shape_index: usize,
251 rotation_index: Option<usize>,
252 renderer: &mut ShapeRenderer,
253 color: Rgba,
254 background_color: Rgba,
255 cell_offset: u32,
256 cell_size: u32,
257 render_fn: impl Fn(&mut ShapeRenderer, Rgba, Rgba, u32, u8, usize),
258 render_positions: impl IntoIterator<Item = (u32, u32)>,
259) {
260 let mut rotation = rotation_index.map(|idx| hash[idx]).unwrap_or_default();
261 let shape_index = hash[shape_index];
262
263 render_positions
264 .into_iter()
265 .enumerate()
266 .for_each(|(i, (x, y))| {
267 renderer.current_transform = Transform::new(
268 cell_offset + x * cell_size,
269 cell_offset + y * cell_size,
270 cell_size,
271 rotation % 4,
272 );
273 rotation += 1;
274
275 render_fn(renderer, color, background_color, cell_size, shape_index, i);
276 });
277}
278
279fn render_outer(
280 renderer: &mut ShapeRenderer,
281 color: Rgba,
282 _background_color: Rgba,
283 cell_size: u32,
284 shape_index: u8,
285 _position_index: usize,
286) {
287 match shape_index % 4 {
288 0 => renderer.triangle::<0>(color, (0, 0), (cell_size, cell_size)),
289 1 => renderer.triangle::<0>(color, (0, cell_size / 2), (cell_size, cell_size / 2)),
290 2 => renderer.rhombus(color, (0, 0), (cell_size, cell_size)),
291 _ => {
292 let m = cell_size / 6;
293 renderer.circle(color, (m, m), cell_size - 2 * m)
294 },
295 };
296}
297
298#[allow(clippy::too_many_lines)]
299fn render_center(
300 renderer: &mut ShapeRenderer,
301 color: Rgba,
302 background_color: Rgba,
303 cell_size: u32,
304 shape_index: u8,
305 position_index: usize,
306) {
307 match shape_index % 14 {
308 0 => {
309 let k = (cell_size as f64 * 0.42) as u32;
310 renderer.polygon(
311 color,
312 [
313 (0, 0),
314 (cell_size, 0),
315 (cell_size, cell_size - k * 2),
316 (cell_size - k, cell_size),
317 (0, cell_size),
318 ],
319 );
320 }
321 1 => {
322 let w = cell_size / 2;
323 let h = (cell_size as f64 * 0.8) as u32;
324
325 renderer.triangle::<2>(color, (cell_size - w, 0), (w, h));
326 }
327 2 => {
328 let w = cell_size / 3;
329 let dw = cell_size - w;
330
331 renderer.rectangle(color, (w, w), (dw, dw));
332 }
333 3 => {
334 let inner = cell_size as f64 / 10.0;
335 let outer = if cell_size < 6 {
338 1
339 } else if cell_size < 8 {
340 2
341 } else {
342 cell_size / 4
343 };
344
345 let inner = if inner > 1.0 { inner as u32 } else { 1 };
346 let p = cell_size - inner - outer;
347
348 renderer.rectangle(color, (outer, outer), (p, p));
349 }
350 4 => {
351 let m = (cell_size as f64 * 0.15) as u32;
352 let w = cell_size / 2;
353 let p = cell_size - w - m;
354
355 renderer.circle(color, (p, p), w);
356 }
357 5 => {
358 let inner = cell_size / 10;
359 let outer = (cell_size as f64 * 0.4) as u32;
360
361 renderer
362 .rectangle(color, (0, 0), (cell_size, cell_size))
363 .polygon(
364 background_color,
365 [
366 (outer, outer),
367 (cell_size - inner, outer),
368 (outer + (cell_size - outer - inner) / 2, cell_size - inner),
369 ],
370 );
371 }
372 6 => {
373 let tenth = cell_size / 10;
374 let four_tenths = tenth * 4;
375 let seven_tenths = tenth * 7;
376
377 renderer.polygon(
378 color,
379 [
380 (0, 0),
381 (cell_size, 0),
382 (cell_size, seven_tenths),
383 (four_tenths, four_tenths),
384 (seven_tenths, cell_size),
385 (0, cell_size),
386 ],
387 );
388 }
389 7 | 11 => {
390 let half_cell = cell_size / 2;
391 let diff = cell_size - half_cell;
392 renderer.triangle::<3>(color, (half_cell, half_cell), (diff, diff));
393 }
394 8 => {
395 let half_cell = cell_size / 2;
396 let diff = cell_size - half_cell;
397
398 renderer
399 .rectangle(color, (0, 0), (cell_size, diff))
400 .rectangle(color, (0, half_cell), (diff, diff))
401 .triangle::<1>(color, (half_cell, half_cell), (diff, diff));
402 }
403 9 => {
404 let inner = (cell_size as f64 * 0.14) as u32;
405 let outer = if cell_size < 4 {
406 1
407 } else if cell_size < 6 {
408 2
409 } else {
410 (cell_size as f64 * 0.35) as u32
411 };
412
413 let p = cell_size - outer - inner;
414 renderer
415 .rectangle(color, (0, 0), (cell_size, cell_size))
416 .rectangle(background_color, (outer, outer), (p, p));
417 }
418 10 => {
419 let inner = cell_size as f64 * 0.12;
420 let outer = (inner * 3.0) as u32;
421 let inner = inner as u32;
422
423 renderer
424 .rectangle(color, (0, 0), (cell_size, cell_size))
425 .circle(background_color, (outer, outer), cell_size - inner - outer);
426 }
427 12 => {
428 let m = cell_size / 4;
429 let p = cell_size - m;
430
431 renderer
432 .rectangle(color, (0, 0), (cell_size, cell_size))
433 .rectangle(background_color, (m, m), (p, p));
434 }
435 13 if position_index == 0 => {
436 let fcell = cell_size as f64;
437 let m = (fcell * 0.4) as u32;
438 let w = (fcell * 1.2) as u32;
439
440 renderer.circle(color, (m, m), w);
441 }
442 _ => (),
443 }
444}
445
446pub fn render_identicon(hash: [u8; 20], config: &Config) -> Image<Rgba> {
457 const SIDE_POSITIONS: [(u32, u32); 8] = [
458 (1, 0),
459 (2, 0),
460 (2, 3),
461 (1, 3),
462 (0, 1),
463 (3, 1),
464 (3, 2),
465 (0, 2),
466 ];
467 const CORNER_POSITIONS: [(u32, u32); 4] = [(0, 0), (3, 0), (3, 3), (0, 3)];
468 const CENTER_POSITIONS: [(u32, u32); 4] = [(1, 1), (2, 1), (2, 2), (1, 2)];
469
470 let mut image = Image::new(config.size, config.size, config.background_color);
471
472 let padding = (config.padding * config.size as f64).round() as u32;
473 let size = config.size - padding * 2;
474
475 let cell = size / 4;
476 let offset = padding + size / 2 - cell * 2;
477
478 let hash = into_nibbles(hash);
479 let hue = 360.0 * hash_substring_u32::<7>(&hash, 33) as f64 / 0xfffffff as f64;
480 let color_candidates = config.color_candidates(hue);
481
482 let mut selected_indices = [!0; 3];
483 macro_rules! contains_opt {
485 ($value:literal) => {{
486 selected_indices[0] == $value
487 || selected_indices[1] == $value
488 || selected_indices[2] == $value
489 }};
490 }
491
492 for i in 0..3 {
493 let index = hash[i + 8] % 5;
494 let index = match index {
495 0 | 4 if contains_opt!(0) || contains_opt!(4) => 1,
496 2 | 3 if contains_opt!(2) || contains_opt!(3) => 1,
497 _ => index,
498 };
499
500 selected_indices[i] = index;
501 }
502
503 let [side_color, corner_color, center_color] = selected_indices;
504 let (side_color, corner_color, center_color) = (
505 color_candidates.get_from_rotation_index(side_color as usize),
506 color_candidates.get_from_rotation_index(corner_color as usize),
507 color_candidates.get_from_rotation_index(center_color as usize),
508 );
509
510 let mut renderer = ShapeRenderer::new(&mut image);
511 macro_rules! render {
512 (
513 $shape_index:literal,
514 $rotation_index:expr,
515 $color:ident,
516 $render_fn:ident,
517 $render_positions:ident
518 ) => {
519 render_shape(
520 &hash,
521 $shape_index,
522 $rotation_index,
523 &mut renderer,
524 $color,
525 config.background_color,
526 offset,
527 cell,
528 $render_fn,
529 $render_positions,
530 );
531 };
532 }
533
534 render!(2, Some(3), side_color, render_outer, SIDE_POSITIONS);
535 render!(4, Some(5), corner_color, render_outer, CORNER_POSITIONS);
536 render!(1, None, center_color, render_center, CENTER_POSITIONS);
537
538 image
539}
540
541pub fn generate_identicon(message: impl AsRef<str>, config: &Config) -> Image<Rgba> {
582 let hash = sha1_smol::Sha1::from(message.as_ref()).digest().bytes();
583 render_identicon(hash, config)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn test_rdenticon() -> ril::Result<()> {
592 let config = Config::builder()
594 .size(512) .padding(0.1) .background_color(Rgba::white())
597 .build()
598 .expect("invalid config");
599
600 let image = generate_identicon("sample", &config);
601 image.save_inferred("identicon.png")
602 }
603}