OXYGENHUB
# 8a2dbcb56246 oxygenhub / crates / hub / src / web / avatar.rs
2.5 KiB · Rust · last changed Profile and organisation pages, with identicons 1 month ago source blame history raw
1
//! Identicons (hub spec §6): a five-by-five mirrored pattern and a colour
2
//! drawn from BLAKE3 of the lower-cased name, so every account and
3
//! organisation has a stable picture without uploads.
4
5
use axum::extract::Path;
6
use axum::http::header;
7
use axum::response::{IntoResponse, Response};
8
9
/// The SVG for `name`: 15 bits choose the cells of the left three columns
10
/// (mirrored to five), two bytes choose the hue.
11
pub fn identicon(name: &str) -> String {
12
    let h = blake3::hash(name.to_ascii_lowercase().as_bytes());
13
    let b = h.as_bytes();
14
    let hue = u16::from_le_bytes([b[0], b[1]]) % 360;
15
    let bits = u32::from_le_bytes([b[2], b[3], b[4], b[5]]);
16
    let mut cells = String::new();
17
    for row in 0..5u32 {
18
        for col in 0..3u32 {
19
            if bits >> (row * 3 + col) & 1 == 1 {
20
                for x in [col, 4 - col] {
21
                    cells.push_str(&format!(r#"<rect x="{}" y="{}" width="1" height="1"/>"#, x + 1, row + 1));
22
                    if x == 2 {
23
                        break;
24
                    }
25
                }
26
            }
27
        }
28
    }
29
    format!(
30
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 7" shape-rendering="crispEdges"><rect width="7" height="7" fill="hsl({hue},45%,92%)"/><g fill="hsl({hue},65%,45%)">{cells}</g></svg>"#
31
    )
32
}
33
34
/// `GET /avatar/{name}`: the identicon, cached for a day.
35
pub async fn avatar(Path(name): Path<String>) -> Response {
36
    let name = name.trim_end_matches(".svg");
37
    ([(header::CONTENT_TYPE, "image/svg+xml"), (header::CACHE_CONTROL, "public, max-age=86400")], identicon(name)).into_response()
38
}
39
40
#[cfg(test)]
41
mod tests {
42
    use super::identicon;
43
44
    #[test]
45
    fn identicons_are_stable_symmetric_and_case_blind() {
46
        let a = identicon("andy");
47
        assert_eq!(a, identicon("Andy"), "names are case-insensitive");
48
        assert_ne!(a, identicon("bob"));
49
        assert!(a.starts_with("<svg") && a.ends_with("</svg>"));
50
        // Every cell has its mirror image.
51
        let cells: Vec<(u32, u32)> = a
52
            .split(r#"<rect x=""#)
53
            .skip(1)
54
            .filter_map(|r| {
55
                let x: u32 = r.split('"').next()?.parse().ok()?;
56
                let y: u32 = r.split(r#"y=""#).nth(1)?.split('"').next()?.parse().ok()?;
57
                Some((x, y))
58
            })
59
            .filter(|(x, _)| (1..=5).contains(x))
60
            .collect();
61
        for (x, y) in &cells {
62
            assert!(cells.contains(&(6 - x, *y)), "({x},{y}) has no mirror in {a}");
63
        }
64
    }
65
}