#![allow(
clippy::cast_precision_loss,
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
mod config;
mod hsl;
pub use config::*;
use hsl::corrected_hsl_to_rgb;
use ril::prelude::*;
pub use ril::{self, ImageFormat};
struct ColorCandidates {
light_gray: Rgba,
dark_gray: Rgba,
light_color: Rgba,
mid_color: Rgba,
dark_color: Rgba,
}
impl ColorCandidates {
#[inline]
const fn get_from_rotation_index(&self, index: usize) -> Rgba {
match index {
0 => self.dark_gray,
1 => self.mid_color,
2 => self.light_gray,
3 => self.light_color,
_ => self.dark_color,
}
}
}
impl Config {
pub(crate) fn resolve_hue(&self, hue: f64) -> f64 {
if self.hues.is_empty() {
hue
} else {
self.hues[(hue / 360.0 * self.hues.len() as f64) as usize]
}
}
#[inline]
pub(crate) fn resolve_color_lightness(&self, lightness: f64) -> f64 {
(self.color_lightness.end() - self.color_lightness.start())
.mul_add(lightness, *self.color_lightness.start())
}
#[inline]
pub(crate) fn resolve_grayscale_lightness(&self, lightness: f64) -> f64 {
(self.grayscale_lightness.end() - self.grayscale_lightness.start())
.mul_add(lightness, *self.grayscale_lightness.start())
}
pub(crate) fn color_candidates(&self, hue: f64) -> ColorCandidates {
let hue = self.resolve_hue(hue);
macro_rules! resolve {
($s:ident, $l_meth:ident, $l_value:literal) => {{
corrected_hsl_to_rgb(hue, self.$s, self.$l_meth($l_value)).into_rgba()
}};
(@grayscale $l_value:literal) => {{
resolve!(grayscale_saturation, resolve_grayscale_lightness, $l_value)
}};
(@color $l_value:literal) => {{
resolve!(color_saturation, resolve_color_lightness, $l_value)
}};
}
ColorCandidates {
light_gray: resolve!(@grayscale 1.0),
dark_gray: resolve!(@grayscale 0.0),
light_color: resolve!(@color 1.0),
mid_color: resolve!(@color 0.5),
dark_color: resolve!(@color 0.0),
}
}
}
#[derive(Copy, Clone, Default)]
struct Transform {
x: u32,
y: u32,
pub rotation: u8,
right: u32,
bottom: u32,
}
impl Transform {
pub(crate) const fn new(x: u32, y: u32, size: u32, rotation: u8) -> Self {
Self {
x,
y,
rotation,
right: x + size,
bottom: y + size,
}
}
pub(crate) const fn transform(&self, (x, y): (u32, u32), (w, h): (u32, u32)) -> (u32, u32) {
match self.rotation {
0 => (self.x + x, self.y + y),
1 => (self.right - y - h, self.y + x),
2 => (self.right - x - w, self.bottom - y - h),
_ => (self.x + y, self.bottom - x - w),
}
}
}
struct ShapeRenderer<'a> {
image: &'a mut Image<Rgba>,
pub current_transform: Transform,
}
impl<'a> ShapeRenderer<'a> {
pub fn new(image: &'a mut Image<Rgba>) -> Self {
Self {
image,
current_transform: Transform::default(),
}
}
pub fn polygon(
&mut self,
color: Rgba,
points: impl IntoIterator<Item = (u32, u32)>,
) -> &mut Self {
let polygon = Polygon::from_vertices(
points
.into_iter()
.map(|pos| self.current_transform.transform(pos, (0, 0))),
)
.with_fill(color);
self.image.draw(&polygon);
self
}
pub fn circle(&mut self, color: Rgba, top_left: (u32, u32), diameter: u32) -> &mut Self {
let (x, y) = self
.current_transform
.transform(top_left, (diameter, diameter));
let circle = Ellipse::from_bounding_box(x, y, x + diameter, y + diameter).with_fill(color);
self.image.draw(&circle);
self
}
pub fn triangle<const ROTATION: usize>(
&mut self,
color: Rgba,
(x, y): (u32, u32),
(w, h): (u32, u32),
) -> &mut Self {
let (a, b, c, d) = ((x + w, y), (x + w, y + h), (x, y + h), (x, y));
let points = match ROTATION % 4 {
0 => [b, c, d],
1 => [a, c, d],
2 => [a, b, d],
3 => [a, b, c],
_ => unsafe { std::hint::unreachable_unchecked() },
};
self.polygon(color, points);
self
}
pub fn rectangle(&mut self, color: Rgba, top_left: (u32, u32), size: (u32, u32)) -> &mut Self {
let (x, y) = self.current_transform.transform(top_left, size);
let rect = Rectangle::new()
.with_position(x, y)
.with_size(size.0, size.1)
.with_fill(color);
self.image.draw(&rect);
self
}
pub fn rhombus(&mut self, color: Rgba, top_left: (u32, u32), size: (u32, u32)) -> &mut Self {
self.polygon(
color,
[
(top_left.0 + size.0 / 2, top_left.1),
(top_left.0 + size.0, top_left.1 + size.1 / 2),
(top_left.0 + size.0 / 2, top_left.1 + size.1),
(top_left.0, top_left.1 + size.1 / 2),
],
)
}
}
#[inline]
fn pad_zeroes<const FROM: usize, const TO: usize>(arr: [u8; FROM]) -> [u8; TO] {
let mut b = [0; TO];
b[TO - FROM..].copy_from_slice(&arr);
b
}
fn into_nibbles(hash: [u8; 20]) -> [u8; 40] {
let mut nibbles = [0; 40];
for i in 0..20 {
nibbles[i * 2] = hash[i] >> 4;
nibbles[i * 2 + 1] = hash[i] & 0x0f;
}
nibbles
}
#[inline]
fn hash_substring_u32<const LEN: usize>(nibbles: &[u8; 40], start: usize) -> u32 {
let nibbles = pad_zeroes::<LEN, 8>(unsafe {
nibbles[start..start + LEN].try_into().unwrap_unchecked()
});
let mut bytes = [0; 4];
for i in 0..4 {
bytes[i] = nibbles[i * 2] << 4 | nibbles[i * 2 + 1];
}
u32::from_be_bytes(bytes)
}
#[allow(clippy::too_many_arguments)]
fn render_shape(
hash: &[u8; 40],
shape_index: usize,
rotation_index: Option<usize>,
renderer: &mut ShapeRenderer,
color: Rgba,
cell_offset: u32,
cell_size: u32,
render_fn: impl Fn(&mut ShapeRenderer, Rgba, u32, u8, usize),
render_positions: impl IntoIterator<Item = (u32, u32)>,
) {
let mut rotation = rotation_index.map(|idx| hash[idx]).unwrap_or_default();
let shape_index = hash[shape_index];
render_positions
.into_iter()
.enumerate()
.for_each(|(i, (x, y))| {
renderer.current_transform = Transform::new(
cell_offset + x * cell_size,
cell_offset + y * cell_size,
cell_size,
rotation % 4,
);
rotation += 1;
render_fn(renderer, color, cell_size, shape_index, i);
});
}
fn render_outer(
renderer: &mut ShapeRenderer,
color: Rgba,
cell_size: u32,
shape_index: u8,
_position_index: usize,
) {
match shape_index % 4 {
0 => renderer.triangle::<0>(color, (0, 0), (cell_size, cell_size)),
1 => renderer.triangle::<0>(color, (0, cell_size / 2), (cell_size, cell_size / 2)),
2 => renderer.rhombus(color, (0, 0), (cell_size, cell_size)),
_ => {
let m = cell_size / 6;
renderer.circle(color, (m, m), cell_size - 2 * m)
},
};
}
#[allow(clippy::too_many_lines)]
fn render_center(
renderer: &mut ShapeRenderer,
color: Rgba,
cell_size: u32,
shape_index: u8,
position_index: usize,
) {
match shape_index % 14 {
0 => {
let k = (cell_size as f64 * 0.42) as u32;
renderer.polygon(
color,
[
(0, 0),
(cell_size, 0),
(cell_size, cell_size - k * 2),
(cell_size - k, cell_size),
(0, cell_size),
],
);
}
1 => {
let w = cell_size / 2;
let h = (cell_size as f64 * 0.8) as u32;
renderer.triangle::<2>(color, (cell_size - w, 0), (w, h));
}
2 => {
let w = cell_size / 3;
let dw = cell_size - w;
renderer.rectangle(color, (w, w), (dw, dw));
}
3 => {
let inner = cell_size as f64 / 10.0;
let outer = if cell_size < 6 {
1
} else if cell_size < 8 {
2
} else {
cell_size / 4
};
let inner = if inner > 1.0 { inner as u32 } else { 1 };
let p = cell_size - inner - outer;
renderer.rectangle(color, (outer, outer), (p, p));
}
4 => {
let m = (cell_size as f64 * 0.15) as u32;
let w = cell_size / 2;
let p = cell_size - w - m;
renderer.circle(color, (p, p), w);
}
5 => {
let inner = cell_size / 10;
let outer = (cell_size as f64 * 0.4) as u32;
renderer
.rectangle(color, (0, 0), (cell_size, cell_size))
.polygon(
color,
[
(outer, outer),
(cell_size - inner, outer),
(outer + (cell_size - outer - inner) / 2, cell_size - inner),
],
);
}
6 => {
let tenth = cell_size / 10;
let four_tenths = tenth * 4;
let seven_tenths = tenth * 7;
renderer.polygon(
color,
[
(0, 0),
(cell_size, 0),
(cell_size, seven_tenths),
(four_tenths, four_tenths),
(seven_tenths, cell_size),
(0, cell_size),
],
);
}
7 | 11 => {
let half_cell = cell_size / 2;
let diff = cell_size - half_cell;
renderer.triangle::<3>(color, (half_cell, half_cell), (diff, diff));
}
8 => {
let half_cell = cell_size / 2;
let diff = cell_size - half_cell;
renderer
.rectangle(color, (0, 0), (cell_size, diff))
.rectangle(color, (0, half_cell), (diff, diff))
.triangle::<1>(color, (half_cell, half_cell), (diff, diff));
}
9 => {
let inner = (cell_size as f64 * 0.14) as u32;
let outer = if cell_size < 4 {
1
} else if cell_size < 6 {
2
} else {
(cell_size as f64 * 0.35) as u32
};
let p = cell_size - outer - inner;
renderer
.rectangle(color, (0, 0), (cell_size, cell_size))
.rectangle(color, (outer, outer), (p, p));
}
10 => {
let inner = cell_size as f64 * 0.12;
let outer = (inner * 3.0) as u32;
let inner = inner as u32;
renderer
.rectangle(color, (0, 0), (cell_size, cell_size))
.circle(color, (outer, outer), cell_size - inner - outer);
}
12 => {
let m = cell_size / 4;
let p = cell_size - m;
renderer
.rectangle(color, (0, 0), (cell_size, cell_size))
.rectangle(color, (m, m), (p, p));
}
13 if position_index == 0 => {
let fcell = cell_size as f64;
let m = (fcell * 0.4) as u32;
let w = (fcell * 1.2) as u32;
renderer.circle(color, (m, m), w);
}
_ => (),
}
}
pub fn render_identicon(hash: [u8; 20], config: &Config) -> Image<Rgba> {
const SIDE_POSITIONS: [(u32, u32); 8] = [
(1, 0),
(2, 0),
(2, 3),
(1, 3),
(0, 1),
(3, 1),
(3, 2),
(0, 2),
];
const CORNER_POSITIONS: [(u32, u32); 4] = [(0, 0), (3, 0), (3, 3), (0, 3)];
const CENTER_POSITIONS: [(u32, u32); 4] = [(1, 1), (2, 1), (2, 2), (1, 2)];
let mut image = Image::new(config.size, config.size, config.background_color);
let padding = (config.padding * config.size as f64).round() as u32;
let size = config.size - padding * 2;
let cell = size / 4;
let offset = padding + size / 2 - cell * 2;
let hash = into_nibbles(hash);
let hue = 360.0 * hash_substring_u32::<7>(&hash, 33) as f64 / 0xfffffff as f64;
let color_candidates = config.color_candidates(hue);
let mut selected_indices = [!0; 3];
macro_rules! contains_opt {
($value:literal) => {{
selected_indices[0] == $value
|| selected_indices[1] == $value
|| selected_indices[2] == $value
}};
}
for i in 0..3 {
let index = hash[i + 8] % 5;
let index = match index {
0 | 4 if contains_opt!(0) || contains_opt!(4) => 1,
2 | 3 if contains_opt!(2) || contains_opt!(3) => 1,
_ => index,
};
selected_indices[i] = index;
}
let [side_color, corner_color, center_color] = selected_indices;
let (side_color, corner_color, center_color) = (
color_candidates.get_from_rotation_index(side_color as usize),
color_candidates.get_from_rotation_index(corner_color as usize),
color_candidates.get_from_rotation_index(center_color as usize),
);
let mut renderer = ShapeRenderer::new(&mut image);
macro_rules! render {
(
$shape_index:literal,
$rotation_index:expr,
$color:ident,
$render_fn:ident,
$render_positions:ident
) => {
render_shape(
&hash,
$shape_index,
$rotation_index,
&mut renderer,
$color,
offset,
cell,
$render_fn,
$render_positions,
);
};
}
render!(2, Some(3), side_color, render_outer, SIDE_POSITIONS);
render!(4, Some(5), corner_color, render_outer, CORNER_POSITIONS);
render!(1, None, center_color, render_center, CENTER_POSITIONS);
image
}
pub fn generate_identicon(message: impl AsRef<str>, config: &Config) -> Image<Rgba> {
let hash = sha1_smol::Sha1::from(message.as_ref()).digest().bytes();
render_identicon(hash, config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rdenticon() -> ril::Result<()> {
let config = Config::builder()
.size(512) .padding(0.1) .background_color(Rgba::white())
.build()
.expect("invalid config");
let image = generate_identicon("jay3332", &config);
image.save_inferred("identicon.png")
}
}