rust-adorable-avatars 0.1.0

Port of adorable avatars WIP
Documentation

extern crate image;
extern crate uuid;
extern crate crypto;

#[macro_use] extern crate failure;

use std::path::Path;

use image::imageops;
use image::GenericImageView;
use crypto::digest::Digest;
use crypto::sha2::Sha256;

use std::fs;
use std::u8;

use std::convert::From;

#[derive(Clone, Debug, Fail)]
pub enum Error {
    #[fail(display = "could not open file: {}", filename)]
    ReadingFromFileFailed {
        filename: String,
    },
    #[fail(display = "could not write to file: {}", filename)]
    WritingToFileFailed {
        filename: String,
    },
    #[fail(display = "error decoding filename")]
    DecodeError,
    #[fail(display = "features not found in image directory")]
    FeaturesNotFound,
    #[fail(display = "dimensions must match")]
    DimensionsDontMatch,
}

fn get_colors() -> Vec<Color> {
    vec![
        "#81bef1".into(),
        "#ad8bf2".into(),
        "#bff288".into(),
        "#de7878".into(),
        "#a5aac5".into(),
        "#6ff2c5".into(),
        "#f0da5e".into(),
        "#eb5972".into(),
        "#f6be5d".into(),
    ]
}

fn generate_result(face: &Face, color: &Color, size: &Size) -> image::DynamicImage {

    let Size { width, height } = face.dimensions;
    let canvas_raw = image::ImageBuffer::from_pixel(width, height, image::Rgb([color.r, color.g, color.b]));
    let mut canvas = image::DynamicImage::ImageRgb8(canvas_raw);
    imageops::overlay(&mut canvas, &face.eyes, 0, 0);
    imageops::overlay(&mut canvas, &face.mouth, 0, 0);
    imageops::overlay(&mut canvas, &face.nose, 0, 0);

    let output_width = size.width;
    let output_height = size.height;

    let output = canvas.resize(output_width, output_height, image::FilterType::Lanczos3);
 
    output
}

fn get_feature_image(path: &str) -> Result<image::DynamicImage, Error> {
    let feature = image::open(&Path::new(path))
        .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: path.to_string() }))?;
    Ok(feature)
}

fn get_filenames(dirpath: &str) -> Result<Vec<String>, Error> {

    let paths = fs::read_dir(dirpath)
        .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: dirpath.to_string() }))?;

    let mut entities: Vec<String> = vec![];
    for path in paths {
        let unwrapped_path = path
            .or_else(|_err| Err(Error::ReadingFromFileFailed { filename: dirpath.to_string() }))?;

        let full_path = unwrapped_path.path();
        let filename = full_path.file_stem()
            .ok_or_else(|| Error::DecodeError)?
            .to_str()
            .ok_or_else(|| Error::DecodeError)?;

           
        entities.push(filename.to_string());
    }

    Ok(entities)

}

fn get_seed_as_nums(seed: &str) -> [u64;4] {
    let mut hasher = Sha256::new();
    hasher.input_str(seed);

    let mut hash_result = [0u8;32];
    hasher.result(&mut hash_result);

    let mut output = [0u64;4];
    for i in 0..4 {
        let begin = i * 8;
        let end = begin + 8;
        let x = &hash_result[begin..end];

        let mut y: u64 = 0;
        y |= (x[0] as u64) << 56;
        y |= (x[1] as u64) << 48;
        y |= (x[2] as u64) << 40;
        y |= (x[3] as u64) << 32;
        y |= (x[4] as u64) << 24;
        y |= (x[5] as u64) << 16;
        y |= (x[6] as u64) << 8;
        y |= x[7] as u64;

        output[i] = y;
    }

    output

}

fn generate_pseudorandom_face_and_color(seed: &str) -> Result<(Face, Color), Error> {
    let nums = get_seed_as_nums(seed);

    let eyes = get_filenames("img/eyes/")?;
    let mouthes = get_filenames("img/mouth/")?;
    let noses = get_filenames("img/nose/")?;

    if eyes.len() == 0 || mouthes.len() == 0 || noses.len() == 0 {
        return Err(Error::FeaturesNotFound);
    }

    let random_eyes = &eyes[nums[0] as usize % eyes.len()];
    let random_mouth = &mouthes[nums[1] as usize % mouthes.len()];
    let random_nose = &noses[nums[2] as usize % noses.len()];

    let random_face = Face::new(random_eyes, random_mouth, random_nose)?;

    let colors = get_colors();
    let random_color = (&colors[nums[3] as usize % colors.len()]).to_owned();

    Ok((random_face, random_color))
}

fn generate_random_string() -> String {
    uuid::Uuid::new_v4().to_string()
}


#[derive(Clone, PartialEq, Eq)]
pub struct Color {
    r: u8,
    g: u8,
    b: u8, 
}

impl From<&'static str> for Color {
    fn from(hexcode: &str) -> Self {
        let r = u8::from_str_radix(&hexcode[1..3], 16).unwrap_or_default();
        let g = u8::from_str_radix(&hexcode[3..5], 16).unwrap_or_default();
        let b = u8::from_str_radix(&hexcode[5..7], 16).unwrap_or_default();
        Self { r, g, b, }
    }
}


#[derive(Clone, PartialEq, Eq)]
pub struct Size {
    width: u32,
    height: u32,
}

impl From<(u32, u32)> for Size {
    fn from(size: (u32, u32)) -> Self {
        Self {
            width: size.0,
            height: size.1,
        }
    }
}

pub struct Face {
    eyes: image::DynamicImage,
    mouth: image::DynamicImage,
    nose: image::DynamicImage,
    dimensions: Size,
}

impl Face {

    pub fn new(eyes: &str, mouth: &str, nose: &str) -> Result<Self, Error> {
        let eyes_image = get_feature_image(&format!("img/eyes/{}.png", eyes))?;
        let mouth_image = get_feature_image(&format!("img/mouth/{}.png", mouth))?;
        let nose_image = get_feature_image(&format!("img/nose/{}.png", nose))?;

        let eyes_dim: Size = eyes_image.dimensions().into();
        let mouth_dim: Size = mouth_image.dimensions().into();
        let nose_dim: Size = nose_image.dimensions().into();

        if eyes_dim == mouth_dim && mouth_dim == nose_dim {
            Ok(Self {
                eyes: eyes_image, 
                mouth: mouth_image,
                nose: nose_image,
                dimensions: eyes_dim 
            })
        } else {
            Err(Error::DimensionsDontMatch)
        }
    }
}


pub struct Avatar {
    output: image::DynamicImage,
}

impl Avatar {

    pub fn generate_from_id<S>(id: &str, size: S) -> Result<Self, Error>
        where Size: From<S>,
    {
        let (face, color) = generate_pseudorandom_face_and_color(id)?;
        let final_size: Size = size.into();
        Ok(Self { output: generate_result(&face, &color, &final_size) })
    }

    pub fn generate_random<S>(size: S) -> Result<Self, Error>
        where Size: From<S>,
    {
        let (face, color) = generate_pseudorandom_face_and_color(&generate_random_string())?;
        let final_size: Size = size.into();
        Ok(Self { output: generate_result(&face, &color, &final_size) })
    }

    pub fn build_avatar<S>(face: Face, color: Color, size: S) -> Result<Self, Error>
        where Size: From<S>,
    {
        let final_size: Size = size.into();
        Ok(Self { output: generate_result(&face, &color, &final_size) })
    }

    pub fn output(self, filename: &str) -> Result<(), Error> {
        self.output.save(filename)
            .or_else(|_err| Err(Error::WritingToFileFailed { filename: filename.to_string() }))
            .and_then(|_res| Ok(()))
    }
}




#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn check_sha256_is_correct() {
        let result = get_seed_as_nums("hunter2");
        let formatted_result = format!("{:X?}", result);

        assert_eq!(formatted_result, "[F52FBD32B2B3B86F, F88EF6C490628285, F482AF15DDCB2954, 1F94BCF526A3F6C7]");
    }
}