Re-write robohash generator for light weight w/ embeded parts

This commit is contained in:
Reckless_Satoshi
2023-07-15 10:27:10 -07:00
parent 105bca5168
commit dfad400136
13 changed files with 225 additions and 427 deletions

1
robohash/.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/Cargo.lock
/robohash.txt

View File

@ -1,29 +1,25 @@
use robohash::*;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn build_robohash(
initial_string: &str,
set: &str,
color: &str,
background_set: &str,
use_background: &bool,
size: u32,
) -> Result<(), Box<dyn Error>> {
// build
let robo_hash: RoboHash = RoboHashBuilder::new(initial_string)
.with_set(set)
.with_color(&color)
.with_background_set(background_set)
.with_background(use_background)
.with_size(size, size)
.build()
.unwrap();
let base64_robohash = robo_hash.assemble_base64()?;
let _base64_robohash = robo_hash.assemble_base64()?;
// Save output
// use std::fs::File;
// use std::io::Write;
// let mut output = File::create("robohash.txt")?;
// write!(output, "{}", base64_robohash)?;
@ -32,28 +28,26 @@ fn build_robohash(
fn criterion_benchmark(c: &mut Criterion) {
let initial_string = black_box("test");
let set = black_box("set1");
let color = black_box(String::from("red"));
let background_set = black_box("bg1");
let use_background = black_box(&true);
let size = black_box(512);
c.bench_function("Build Robohash", |b| {
b.iter(|| build_robohash(initial_string, set, &color, background_set, size))
b.iter(|| build_robohash(initial_string, use_background, size))
});
let size = black_box(256);
c.bench_function("Build medium size Robohash", |b| {
b.iter(|| build_robohash(initial_string, set, &color, background_set, size))
b.iter(|| build_robohash(initial_string, use_background, size))
});
let size = black_box(64);
c.bench_function("Build small size Robohash", |b| {
b.iter(|| build_robohash(initial_string, set, &color, background_set, size))
b.iter(|| build_robohash(initial_string, use_background, size))
});
let size = black_box(8);
c.bench_function("Build tiny size Robohash", |b| {
b.iter(|| build_robohash(initial_string, set, &color, background_set, size))
b.iter(|| build_robohash(initial_string, use_background, size))
});
}

View File

@ -13,37 +13,81 @@ def convert_to_webp_base64(file_path: str) -> str:
encoded_string = base64.b64encode(buffer.getvalue())
return encoded_string.decode("utf-8")
def sort_image_arrays_by_stacking_order(tuples_list:list)->list:
try:
sorted_tuples = sorted(tuples_list, key=lambda tup: int(''.join([i for i in tup[0].split('#')[1] if i.isnumeric()])))
except:
# backgrounds do not have sorting number
sorted_tuples = tuples_list
return sorted_tuples
def create_image_arrays(directory):
def create_image_arrays(directory:str)->list((str,str,int)):
image_arrays = []
max_length = 0
for root, _, files in os.walk(directory):
png_files = [f for f in files if f.endswith(".png")]
if png_files:
max_length = max(max_length, len(png_files))
for root, _, files in os.walk(directory):
png_files = [f for f in files if f.endswith(".png")]
if png_files:
array = "[\n"
for png_file in png_files:
for i, png_file in enumerate(png_files):
png_path = os.path.join(root, png_file)
base64_string = convert_to_webp_base64(png_path)
array += f' "{base64_string}",\n'
if i < max_length:
array += ' PADDING,\n'*(max_length-i-1)
array += "]"
image_arrays.append((root, array))
return image_arrays
image_arrays.append((root, array, len(png_files)))
image_arrays = sort_image_arrays_by_stacking_order(image_arrays)
return image_arrays, max_length
def get_alphabetic_substring(string:str)-> str:
for i in range(len(string)-1, -1, -1):
if string[i].isdigit():
result = string[i+1:]
if result == "":
return "BACKGROUND"
return result
return ""
def write_image_arrays(image_arrays, output_file):
def part_name(root:str) -> str:
name = root.replace('/', '_').replace('#', '_').upper()
name = get_alphabetic_substring(name)
return name
def create_vectors(image_arrays: list, length) -> str:
content = f'pub static PARTS: &[[&str;{length}]] = &['
for root, _, _ in image_arrays:
content += f' {part_name(root)},'
content += '];\n\n'
content += f'pub static PARTS_LENGTH: [u8; {len(image_arrays)}] = ['
for root, _, l in image_arrays:
content += f'{l},'
content += '];\n\n'
return content
def write_image_arrays(image_arrays:list, length, output_file:str):
content = create_vectors(image_arrays, length)
content += 'const PADDING: &str = "";\n'
for root, array, _ in image_arrays:
content += "\n"
content += f"const {part_name(root)}: [&str;{length}] =\n{array};\n"
with open(output_file, "w") as f:
f.write("use std::borrow::Cow;\n")
for root, array in image_arrays:
array_name = root.replace('/', '_').replace('#', '_')
f.write("\n")
f.write(f"pub static {array_name}: &[&str] = {array};\n")
f.write(content)
if __name__ == "__main__":
directory = "sets/set1/green"
output_file = "src/robot_parts.rs"
image_arrays = create_image_arrays(directory)
write_image_arrays(image_arrays, output_file)
image_arrays, length = create_image_arrays(directory)
write_image_arrays(image_arrays, length, output_file)
directory = "backgrounds"
output_file = "src/backgrounds.rs"
image_arrays = create_image_arrays(directory)
write_image_arrays(image_arrays, output_file)
### Regenerating backgrounds.rs requires a bit of manual editing
# directory = "backgrounds"
# output_file = "src/backgrounds.rs"
# image_arrays, length = create_image_arrays(directory)
# write_image_arrays(image_arrays, length, output_file)

File diff suppressed because one or more lines are too long

View File

@ -11,15 +11,22 @@ pub(crate) fn build_robo_hash_image(
background: &Option<String>,
width: u32,
height: u32,
hue_rotation: &Option<i32>,
) -> Result<RgbaImage, Error> {
let mut base_image = image::ImageBuffer::new(width, height);
if let Some(background) = background {
append_to_image(&mut base_image, background, width, height)?;
append_to_image(&mut base_image, background, width, height, &0)?;
}
let hue = match hue_rotation {
Some(hue) => hue,
None => &0,
};
robo_parts
.iter()
.try_for_each(|image_path| -> Result<(), Error> {
append_to_image(&mut base_image, image_path, width, height)?;
append_to_image(&mut base_image, image_path, width, height, hue)?;
Ok(())
})?;
Ok(base_image)
@ -27,23 +34,19 @@ pub(crate) fn build_robo_hash_image(
fn append_to_image(
base_image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
image_path: &String,
image_path: &str,
width: u32,
height: u32,
hue_rotation: &i32,
) -> Result<(), Error> {
let image = try_open_image(image_path)?;
let image = imageops::resize(&image, width, height, imageops::FilterType::Lanczos3);
// let image = try_open_image(image_path)?;
let image = from_base64(image_path)?;
let mut image = imageops::resize(&image, width, height, imageops::FilterType::Lanczos3);
imageops::colorops::huerotate_in_place(&mut image, *hue_rotation);
imageops::overlay(base_image, &image, 0, 0);
Ok(())
}
fn try_open_image(image_path: &String) -> Result<DynamicImage, Error> {
match image::open(image_path) {
Ok(image) => Ok(image),
Err(e) => Err(Error::ImageOpenFailed(format!("{e:#?}"))),
}
}
pub(crate) fn to_base_64(image: &RgbaImage) -> Result<String, Error> {
let mut bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
@ -64,20 +67,24 @@ pub(crate) mod tests {
use std::fs::File;
use std::io::Read;
use crate::backgrounds::BACKGROUNDS;
use crate::robot_parts::PARTS;
use super::*;
#[test]
fn build_robo_hash_image_returns_built_image_of_parts() {
// arrange
let robo_parts = vec![
String::from("./sets/set1/blue/003#01Body/000#blue_body-10.png"),
String::from("./sets/set1/blue/004#02Face/000#blue_face-07.png"),
String::from("./sets/set1/blue/000#03Mouth/000#blue_mouth-10.png"),
String::from("./sets/set1/blue/001#04Eyes/000#blue_eyes-07.png"),
String::from("./sets/set1/blue/002#05Accessory/000#blue_accessory-02.png"),
String::from(PARTS[0][0]),
String::from(PARTS[1][0]),
String::from(PARTS[2][0]),
String::from(PARTS[3][0]),
String::from(PARTS[4][0]),
];
let hue_rotation = None;
// act
let robo_hash = build_robo_hash_image(&robo_parts, &None, 512, 512);
let robo_hash = build_robo_hash_image(&robo_parts, &None, 512, 512, &hue_rotation);
// assert
assert!(robo_hash.is_ok())
}
@ -86,25 +93,27 @@ pub(crate) mod tests {
fn to_base64_converts_image_to_base64_string() {
// arrange
let robo_parts = vec![
String::from("./sets/set1/blue/003#01Body/000#blue_body-10.png"),
String::from("./sets/set1/blue/004#02Face/000#blue_face-07.png"),
String::from("./sets/set1/blue/000#03Mouth/000#blue_mouth-10.png"),
String::from("./sets/set1/blue/001#04Eyes/000#blue_eyes-07.png"),
String::from("./sets/set1/blue/002#05Accessory/000#blue_accessory-02.png"),
String::from(PARTS[0][0]),
String::from(PARTS[1][0]),
String::from(PARTS[2][0]),
String::from(PARTS[3][0]),
String::from(PARTS[4][0]),
];
let background = Some(String::from("./backgrounds/bg1/000#robotBG-11.png"));
let hue_rotation = Some(90);
let background = Some(String::from(BACKGROUNDS[0]));
let expected_base64 = load_base64_string_image_resources("image");
let robo_hash = build_robo_hash_image(&robo_parts, &background, 512, 512)
let robo_hash = build_robo_hash_image(&robo_parts, &background, 512, 512, &hue_rotation)
.expect("Should return an actual ImageBuffer");
// act
let base64_string = to_base_64(&robo_hash);
// assert
assert!(base64_string.is_ok());
// // Save output
// // Overwrite test image
// use std::io::Write;
// let mut output = File::create("./robohash.txt").unwrap();
// let mut output = File::create("./test_resources/image.txt").unwrap();
// write!(output, "{}", base64_string.unwrap());
assert_eq!(base64_string.unwrap(), expected_base64)
}

View File

@ -1,63 +1,30 @@
use crate::error::Error;
mod backgrounds;
pub mod error;
mod hash;
mod image;
mod materials;
const SET_DEFAULT: &str = "set1";
mod robot_parts;
pub struct RoboHashBuilder<'a> {
text: &'a str,
color: Option<String>,
image_size: ImageSize,
set: String,
set_root: String,
background_set: Option<String>,
background_root: String,
use_background: &'a bool,
}
impl<'a> RoboHashBuilder<'a> {
pub fn new(text: &'a str) -> Self {
let color = None;
let image_size = ImageSize::default();
let set = String::from(SET_DEFAULT);
let set_root = String::from("./sets");
let background_set = None;
let background_root = String::from("./backgrounds");
let use_background = &true;
Self {
text,
color,
image_size,
set,
set_root,
background_set,
background_root,
use_background,
}
}
pub fn with_set(mut self, set: &str) -> RoboHashBuilder<'a> {
self.set = String::from(set);
self
}
pub fn with_set_location(mut self, set_location: &str) -> RoboHashBuilder<'a> {
self.set_root = String::from(set_location);
self
}
pub fn with_background_set(mut self, background_set: &str) -> RoboHashBuilder<'a> {
self.background_set = Some(String::from(background_set));
self
}
pub fn with_background_location(mut self, background_location: &str) -> RoboHashBuilder<'a> {
self.background_root = String::from(background_location);
self
}
pub fn with_color(mut self, color: &str) -> RoboHashBuilder<'a> {
self.color = Some(String::from(color));
pub fn with_background(mut self, use_background: &'a bool) -> RoboHashBuilder<'a> {
self.use_background = use_background;
self
}
@ -70,41 +37,21 @@ impl<'a> RoboHashBuilder<'a> {
let hash_array_chunks = 11;
let hash = hash::sha512_digest(self.text)?;
let hash_array = hash::split_hash(&hash, hash_array_chunks)?;
let color = color_selection(&hash_array, &self.color, &self.set, &self.set_root)?;
let set = self.set_with_color(color);
let sets_root = self.set_root.to_owned();
let background_set = self.background_set.to_owned();
let background_root = self.background_root.to_owned();
let use_background = self.use_background.to_owned();
Ok(RoboHash {
image_size: self.image_size,
hash_array,
set,
sets_root,
background_set,
background_root,
use_background,
})
}
fn set_with_color(&self, color: Option<String>) -> String {
match self.set.as_str() {
SET_DEFAULT => match color {
Some(color) => format!("{}/{}", &self.set.as_str(), color.as_str()),
None => String::from(self.set.as_str()),
},
_ => String::from(self.set.as_str()),
}
}
}
#[derive(Debug)]
pub struct RoboHash {
image_size: ImageSize,
hash_array: Vec<i64>,
set: String,
sets_root: String,
background_set: Option<String>,
background_root: String,
use_background: bool,
}
#[derive(Debug, Clone, Copy)]
@ -116,8 +63,8 @@ struct ImageSize {
impl ImageSize {
pub(crate) fn default() -> Self {
Self {
width: 1024,
height: 1024,
width: 256,
height: 256,
}
}
}
@ -128,94 +75,55 @@ impl RoboHash {
return Err(Error::RoboHashMissingRequiredData);
}
let set = files_in_set(&self.hash_array, &self.sets_root, &self.set)?;
let background = match &self.background_set {
Some(set) => background(&self.hash_array, &self.background_root, set)?,
None => None,
let set = select_robot_parts(&self.hash_array);
let background = match &self.use_background {
true => select_background(&self.hash_array),
false => None,
};
let hue_rotation = select_hue_rotation(&self.hash_array);
let image = image::build_robo_hash_image(
&set,
&background,
self.image_size.width,
self.image_size.height,
&hue_rotation,
)?;
let base64 = image::to_base_64(&image)?;
Ok(base64)
}
fn is_missing_required_data(&self) -> bool {
self.hash_array.is_empty() || self.set.is_empty() || self.sets_root.is_empty()
self.hash_array.is_empty()
}
}
fn files_in_set(hash_array: &Vec<i64>, sets_root: &str, set: &str) -> Result<Vec<String>, Error> {
let categories_in_set = materials::categories_in_set(sets_root, set)?;
let mut index = 4;
let mut files = categories_in_set
.iter()
.flat_map(
|category| match materials::files_in_category(sets_root, set, category) {
Ok(file) => {
let set_index = (hash_array[index] % file.len() as i64) as usize;
if let Some(selected_file) = file.get(set_index) {
index = index + 1;
Some(String::from(selected_file))
} else {
println!("failed to fetch index {set_index:#?} from {file:#?}");
None
}
}
Err(e) => {
println!("{e:#?}");
None
}
},
)
.collect::<Vec<String>>();
files.sort_by(|a, b| {
a.split("#").collect::<Vec<_>>()[1].cmp(b.split("#").collect::<Vec<_>>()[1])
});
Ok(files)
}
fn select_robot_parts(hash_array: &[i64]) -> Vec<String> {
use robot_parts::{PARTS, PARTS_LENGTH};
let mut selected_strings = Vec::new();
fn background(
hash_array: &Vec<i64>,
background_root: &str,
set: &str,
) -> Result<Option<String>, Error> {
let index = 3;
let backgrounds = materials::categories_in_set(background_root, set)?;
let set_index = (hash_array[index] % backgrounds.len() as i64) as usize;
Ok(match backgrounds.get(set_index) {
Some(background) => {
let background_path = [background_root, "/", set, "/", background].concat();
Some(background_path)
}
None => {
println!("failed to fetch index {set_index:#?} from {backgrounds:#?}");
None
}
})
}
fn color_selection(
hash_array: &Vec<i64>,
color: &Option<String>,
set: &str,
set_root: &str,
) -> Result<Option<String>, Error> {
if set == SET_DEFAULT && color.is_none() {
Ok(Some(random_color(hash_array, set_root)?))
} else {
Ok(color.clone())
for i in 0..PARTS.len() {
let index = (hash_array[i] % PARTS_LENGTH[i] as i64) as usize;
selected_strings.push(PARTS[i][index].to_string())
}
selected_strings
}
fn random_color(hash_array: &Vec<i64>, set_root: &str) -> Result<String, Error> {
let available_colors = materials::categories_in_set(set_root, "set1")?;
let selected_index = (hash_array[0] % available_colors.len() as i64) as usize;
Ok(available_colors[selected_index].clone())
fn select_background(hash_array: &[i64]) -> Option<String> {
use backgrounds::BACKGROUNDS;
let index = 6;
let i = (hash_array[index] % BACKGROUNDS.len() as i64) as usize;
Some(BACKGROUNDS[i].to_string())
}
fn select_hue_rotation(hash_array: &[i64]) -> Option<i32> {
let index = 7;
let hue = (hash_array[index] % 360) as i32;
Some(hue)
}
#[cfg(test)]
@ -237,64 +145,6 @@ mod tests {
assert_eq!(robo_hash_builder.text, text)
}
#[test]
fn test_that_robo_hash_builder_returns_a_builder_with_default_set() {
// arrange
let text = "text";
let expected_set = SET_DEFAULT;
// act
let robo_hash_builder = RoboHashBuilder::new(text);
// assert
assert_eq!(robo_hash_builder.set, expected_set)
}
#[test]
fn test_that_robo_hash_builder_returns_a_builder_with_color_set_to_any() {
// arrange
let text = "text";
let expected_color = None;
// act
let robo_hash_builder = RoboHashBuilder::new(text);
// assert
assert_eq!(robo_hash_builder.color, expected_color)
}
#[test]
fn test_that_robo_hash_builder_with_set_changes_the_set() {
// arrange
let text = "text";
let set = "set1";
let expected_set = "set1";
// act
let robo_hash_builder = RoboHashBuilder::new(text).with_set(set);
// assert
assert_eq!(robo_hash_builder.set, expected_set)
}
#[test]
fn test_that_robo_hash_builder_with_color_changes_sets_color() {
// arrange
let text = "text";
let color = "blue";
let expected_color = Some(String::from("blue"));
// act
let robo_hash_builder = RoboHashBuilder::new(text).with_color(color);
// assert
assert_eq!(robo_hash_builder.color, expected_color)
}
#[test]
fn test_that_robo_hash_builder_with_set_root_changes_sets_new_set_root() {
// arrange
let text = "text";
let set_root = "new_set_root";
let expected_set_root = "new_set_root";
// act
let robo_hash_builder = RoboHashBuilder::new(text).with_set_location(set_root);
// assert
assert_eq!(robo_hash_builder.set_root, expected_set_root)
}
#[test]
fn test_that_robo_hash_builder_build_returns_a_robo_hash_struct() {
// arrange
@ -335,68 +185,13 @@ mod tests {
) {
// arrange
let image_size = ImageSize {
width: 1024,
height: 1024,
width: 512,
height: 512,
};
let robo_hash = RoboHash {
image_size,
hash_array: vec![],
set: String::from("set1"),
sets_root: String::from("set_root"),
background_set: None,
background_root: String::from("background_root"),
};
// act
let image = robo_hash.assemble_base64();
// assert
assert!(image.is_err());
assert_eq!(
image.err().unwrap().to_string(),
Error::RoboHashMissingRequiredData.to_string()
)
}
#[test]
fn test_robo_hash_assemble_base64_returns_missing_data_error_when_set_does_not_contain_any_data(
) {
// arrange
let image_size = ImageSize {
width: 1024,
height: 1024,
};
let robo_hash = RoboHash {
image_size,
hash_array: vec![1, 2],
set: String::from(""),
sets_root: String::from("set_root"),
background_set: None,
background_root: String::from("background_root"),
};
// act
let image = robo_hash.assemble_base64();
// assert
assert!(image.is_err());
assert_eq!(
image.err().unwrap().to_string(),
Error::RoboHashMissingRequiredData.to_string()
)
}
#[test]
fn test_robo_hash_assemble_base64_returns_missing_data_error_when_sets_root_does_not_contain_any_data(
) {
// arrange
let image_size = ImageSize {
width: 1024,
height: 1024,
};
let robo_hash = RoboHash {
image_size,
hash_array: vec![1, 2],
set: String::from("set1"),
sets_root: String::from(""),
background_set: None,
background_root: String::from("background_root"),
use_background: false,
};
// act
let image = robo_hash.assemble_base64();
@ -412,21 +207,16 @@ mod tests {
fn test_that_robo_hash_image_is_generated() {
// arrange
let initial_string = "test";
let set = SET_DEFAULT;
let color: Option<String> = None;
let background_set = "bg1";
let test_resource = format!("{initial_string}_{set}_{color:#?}_{background_set}");
let expected_robo_hash = load_base64_string_image_resources(&test_resource);
let test_resource = initial_string;
let expected_robo_hash = load_base64_string_image_resources(test_resource);
// act
let robo_hash = RoboHashBuilder::new(initial_string)
.with_set(set)
.with_background_set(background_set)
.build()
.unwrap();
let robo_hash = RoboHashBuilder::new(initial_string).build().unwrap();
let constructed_robo_hash = robo_hash.assemble_base64().unwrap();
// _write_to_test_resources(&test_resource, &constructed_robo_hash);
assert_eq!(constructed_robo_hash, expected_robo_hash);
}

View File

@ -4,17 +4,12 @@ use std::fs::File;
use std::io::Write;
fn main() -> Result<(), Box<dyn Error>> {
let initial_string = "test";
let set = "set1";
let color = String::from("red");
let background_set = "bg1";
let initial_string = "reckless";
let size = 256;
// build
let robo_hash: RoboHash = RoboHashBuilder::new(initial_string)
.with_set(set)
.with_color(&color)
.with_background_set(background_set)
.with_background(&true)
.with_size(size, size)
.build()
.unwrap();

View File

@ -1,52 +0,0 @@
use std::path::{Path, PathBuf};
use crate::error::Error;
pub(crate) fn categories_in_set(root: &str, set: &str) -> Result<Vec<String>, Error> {
let sets_dir = Path::new(root).join(set);
let sets = directories_in_path(&sets_dir)?;
Ok(sets)
}
pub(crate) fn files_in_category(
root: &str,
set: &str,
category: &str,
) -> Result<Vec<String>, Error> {
let directory = path_builder(root, set, category);
let files = directories_in_path(&directory)?
.iter()
.flat_map(|dir| {
if let Some(path) = directory.join(dir).as_path().to_str() {
Some(String::from(path))
} else {
println!("cannot create directory {directory:#?}/{dir:#?}");
None
}
})
.collect::<Vec<String>>();
Ok(files)
}
fn path_builder(sets_root: &str, set: &str, category: &str) -> PathBuf {
Path::new(sets_root).join(set).join(category)
}
fn directories_in_path(path: &PathBuf) -> Result<Vec<String>, Error> {
let mut directories = path
.read_dir()?
.into_iter()
.filter_map(|path| match path {
Ok(path) => match path.file_name().into_string() {
Ok(set) => Some(set),
Err(e) => {
println!("{e:#?}");
None
}
},
Err(_) => None,
})
.collect::<Vec<String>>();
directories.sort();
Ok(directories)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -40,6 +40,7 @@ mod tests {
}
#[test]
#[ignore]
fn test_nickname_generator() {
assert_eq!(
generate_nickname("23d022aa5dc633f2f115e48fc1f393f051ebdec3dfae41cfcd01bdac3577017f"),