Skip to main content

rust_adorable_avatars/
lib.rs

1
2extern crate image;
3extern crate uuid;
4extern crate crypto;
5
6#[macro_use] extern crate failure;
7
8use std::path::Path;
9
10use image::imageops;
11use image::GenericImageView;
12use crypto::digest::Digest;
13use crypto::sha2::Sha256;
14
15use std::fs;
16use std::u8;
17
18use std::convert::From;
19
20#[derive(Clone, Debug, Fail)]
21pub enum Error {
22    #[fail(display = "could not open file: {}", filename)]
23    ReadingFromFileFailed {
24        filename: String,
25    },
26    #[fail(display = "could not write to file: {}", filename)]
27    WritingToFileFailed {
28        filename: String,
29    },
30    #[fail(display = "error decoding filename")]
31    DecodeError,
32    #[fail(display = "features not found in image directory")]
33    FeaturesNotFound,
34    #[fail(display = "dimensions must match")]
35    DimensionsDontMatch,
36}
37
38fn get_colors() -> Vec<Color> {
39    vec![
40        "#81bef1".into(),
41        "#ad8bf2".into(),
42        "#bff288".into(),
43        "#de7878".into(),
44        "#a5aac5".into(),
45        "#6ff2c5".into(),
46        "#f0da5e".into(),
47        "#eb5972".into(),
48        "#f6be5d".into(),
49    ]
50}
51
52fn generate_result(face: &Face, color: &Color, size: &Size) -> image::DynamicImage {
53
54    let Size { width, height } = face.dimensions;
55    let canvas_raw = image::ImageBuffer::from_pixel(width, height, image::Rgb([color.r, color.g, color.b]));
56    let mut canvas = image::DynamicImage::ImageRgb8(canvas_raw);
57    imageops::overlay(&mut canvas, &face.eyes, 0, 0);
58    imageops::overlay(&mut canvas, &face.mouth, 0, 0);
59    imageops::overlay(&mut canvas, &face.nose, 0, 0);
60
61    let output_width = size.width;
62    let output_height = size.height;
63
64    let output = canvas.resize(output_width, output_height, image::FilterType::Lanczos3);
65 
66    output
67}
68
69fn get_feature_image(path: &str) -> Result<image::DynamicImage, Error> {
70    let feature = image::open(&Path::new(path))
71        .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: path.to_string() }))?;
72    Ok(feature)
73}
74
75fn get_filenames(dirpath: &str) -> Result<Vec<String>, Error> {
76
77    let paths = fs::read_dir(dirpath)
78        .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: dirpath.to_string() }))?;
79
80    let mut entities: Vec<String> = vec![];
81    for path in paths {
82        let unwrapped_path = path
83            .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: dirpath.to_string() }))?;
84
85        let full_path = unwrapped_path.path();
86        let filename = full_path.file_stem()
87            .ok_or_else(|| Error::DecodeError)?
88            .to_str()
89            .ok_or_else(|| Error::DecodeError)?;
90
91           
92        entities.push(filename.to_string());
93    }
94
95    Ok(entities)
96
97}
98
99fn get_seed_as_nums(seed: &str) -> [u64;4] {
100    let mut hasher = Sha256::new();
101    hasher.input_str(seed);
102
103    let mut hash_result = [0u8;32];
104    hasher.result(&mut hash_result);
105
106    let mut output = [0u64;4];
107    for i in 0..4 {
108        let begin = i * 8;
109        let end = begin + 8;
110        let x = &hash_result[begin..end];
111
112        let mut y: u64 = 0;
113        y |= (x[0] as u64) << 56;
114        y |= (x[1] as u64) << 48;
115        y |= (x[2] as u64) << 40;
116        y |= (x[3] as u64) << 32;
117        y |= (x[4] as u64) << 24;
118        y |= (x[5] as u64) << 16;
119        y |= (x[6] as u64) << 8;
120        y |= x[7] as u64;
121
122        output[i] = y;
123    }
124
125    output
126
127}
128
129fn generate_pseudorandom_face_and_color(seed: &str) -> Result<(Face, Color), Error> {
130    let nums = get_seed_as_nums(seed);
131
132    let eyes = get_filenames("img/eyes/")?;
133    let mouthes = get_filenames("img/mouth/")?;
134    let noses = get_filenames("img/nose/")?;
135
136    if eyes.len() == 0 || mouthes.len() == 0 || noses.len() == 0 {
137        return Err(Error::FeaturesNotFound);
138    }
139
140    let random_eyes = &eyes[nums[0] as usize % eyes.len()];
141    let random_mouth = &mouthes[nums[1] as usize % mouthes.len()];
142    let random_nose = &noses[nums[2] as usize % noses.len()];
143
144    let random_face = Face::new(random_eyes, random_mouth, random_nose)?;
145
146    let colors = get_colors();
147    let random_color = (&colors[nums[3] as usize % colors.len()]).to_owned();
148
149    Ok((random_face, random_color))
150}
151
152fn generate_random_string() -> String {
153    uuid::Uuid::new_v4().to_string()
154}
155
156
157#[derive(Clone, PartialEq, Eq)]
158pub struct Color {
159    r: u8,
160    g: u8,
161    b: u8, 
162}
163
164impl From<&'static str> for Color {
165    fn from(hexcode: &str) -> Self {
166        let r = u8::from_str_radix(&hexcode[1..3], 16).unwrap_or_default();
167        let g = u8::from_str_radix(&hexcode[3..5], 16).unwrap_or_default();
168        let b = u8::from_str_radix(&hexcode[5..7], 16).unwrap_or_default();
169        Self { r, g, b, }
170    }
171}
172
173
174#[derive(Clone, PartialEq, Eq)]
175pub struct Size {
176    width: u32,
177    height: u32,
178}
179
180impl From<(u32, u32)> for Size {
181    fn from(size: (u32, u32)) -> Self {
182        Self {
183            width: size.0,
184            height: size.1,
185        }
186    }
187}
188
189pub struct Face {
190    eyes: image::DynamicImage,
191    mouth: image::DynamicImage,
192    nose: image::DynamicImage,
193    dimensions: Size,
194}
195
196impl Face {
197
198    pub fn new(eyes: &str, mouth: &str, nose: &str) -> Result<Self, Error> {
199        let eyes_image = get_feature_image(&format!("img/eyes/{}.png", eyes))?;
200        let mouth_image = get_feature_image(&format!("img/mouth/{}.png", mouth))?;
201        let nose_image = get_feature_image(&format!("img/nose/{}.png", nose))?;
202
203        let eyes_dim: Size = eyes_image.dimensions().into();
204        let mouth_dim: Size = mouth_image.dimensions().into();
205        let nose_dim: Size = nose_image.dimensions().into();
206
207        if eyes_dim == mouth_dim && mouth_dim == nose_dim {
208            Ok(Self {
209                eyes: eyes_image, 
210                mouth: mouth_image,
211                nose: nose_image,
212                dimensions: eyes_dim 
213            })
214        } else {
215            Err(Error::DimensionsDontMatch)
216        }
217    }
218}
219
220
221pub struct Avatar {
222    output: image::DynamicImage,
223}
224
225impl Avatar {
226
227    pub fn generate_from_id<S>(id: &str, size: S) -> Result<Self, Error>
228        where Size: From<S>,
229    {
230        let (face, color) = generate_pseudorandom_face_and_color(id)?;
231        let final_size: Size = size.into();
232        Ok(Self { output: generate_result(&face, &color, &final_size) })
233    }
234
235    pub fn generate_random<S>(size: S) -> Result<Self, Error>
236        where Size: From<S>,
237    {
238        let (face, color) = generate_pseudorandom_face_and_color(&generate_random_string())?;
239        let final_size: Size = size.into();
240        Ok(Self { output: generate_result(&face, &color, &final_size) })
241    }
242
243    pub fn build_avatar<S>(face: Face, color: Color, size: S) -> Result<Self, Error>
244        where Size: From<S>,
245    {
246        let final_size: Size = size.into();
247        Ok(Self { output: generate_result(&face, &color, &final_size) })
248    }
249
250    pub fn output(self, filename: &str) -> Result<(), Error> {
251        self.output.save(filename)
252            .or_else(|_err| Err(Error::WritingToFileFailed { filename: filename.to_string() }))
253            .and_then(|_res| Ok(()))
254    }
255}
256
257
258
259
260#[cfg(test)]
261mod test {
262    use super::*;
263
264    #[test]
265    fn check_sha256_is_correct() {
266        let result = get_seed_as_nums("hunter2");
267        let formatted_result = format!("{:X?}", result);
268
269        assert_eq!(formatted_result, "[F52FBD32B2B3B86F, F88EF6C490628285, F482AF15DDCB2954, 1F94BCF526A3F6C7]");
270    }
271}
272
273
274