#![allow(clippy::too_many_arguments)]
#![allow(clippy::needless_doctest_main)]
pub mod metadata;
mod util;
use crate::metadata::{Attributes, Files, Json, MetadataHeader, Properties};
use crate::rand::Rng;
use rayon::prelude::*;
use std::{collections::HashMap, fs, io::Write, sync::Arc, thread, thread::JoinHandle};
extern crate image;
extern crate rand;
extern crate sha256;
const DEFAULT_ASSET_DIR_NAME: &str = "assets";
const DEFAULT_ASSET_SUBDIR_IMG_NAME: &str = "images";
const DEFAULT_ASSET_SUBDIR_METADATA_NAME: &str = "metadata";
const DEFAULT_METADATA_DIR_NAME: &str = "metadata";
#[derive(Debug, Clone)]
pub enum Network {
Eth,
Sol,
}
impl Network {
fn start_index(&self) -> usize {
match self {
Network::Eth => 1,
Network::Sol => 0,
}
}
}
#[derive(Debug, Clone)]
pub struct ImageGenerator<'a> {
root_asset_path: &'a str,
output_path: &'a str,
network: Network,
base_uri: &'a str,
layer_order: Vec<&'a str>,
layer_exclusion_prob: Vec<f32>,
num_assets: u64,
delimeter: char,
metadata_header: MetadataHeader,
idx: usize,
}
impl<'a> ImageGenerator<'a> {
pub fn new(
root_asset_path: &'a str,
output_path: &'a str,
network: Network,
base_uri: &'a str,
layer_order: Vec<&'a str>,
layer_exclusion_prob: Option<Vec<f32>>,
num_assets: u64,
delimeter: char,
metadata_header: MetadataHeader,
) -> ImageGenerator<'a> {
let _network = network.clone();
let layer_exclusion_prob = ImageGenerator::verify_layer_exclusion_probability(
layer_order.len(),
layer_exclusion_prob,
);
if layer_exclusion_prob.len() != layer_order.len() {
panic!(
"Layer order length ({}) not equal to layer exclusion probability length ({})",
layer_order.len(),
layer_exclusion_prob.len()
);
};
let idx = network.start_index();
ImageGenerator {
root_asset_path,
output_path,
network,
base_uri,
layer_order,
layer_exclusion_prob,
num_assets,
delimeter,
metadata_header,
idx,
}
}
fn verify_layer_exclusion_probability(
num_layers: usize,
layer_exclusion_probability: Option<Vec<f32>>,
) -> Vec<f32> {
if let Some(val) = layer_exclusion_probability {
return val;
}
vec![0.0; num_layers]
}
fn set_up_output_directory(&self) -> std::io::Result<()> {
let path = self.output_path.to_owned();
util::create_dir(&util::generate_path(vec![&path]))?;
util::create_dir(&util::generate_path(vec![
&path,
&DEFAULT_ASSET_DIR_NAME.to_owned(),
]))?;
util::create_dir(&util::generate_path(vec![
&path,
&DEFAULT_METADATA_DIR_NAME.to_owned(),
]))?;
util::create_dir(&util::generate_path(vec![
&path,
&DEFAULT_ASSET_DIR_NAME.to_owned(),
&DEFAULT_ASSET_SUBDIR_IMG_NAME.to_owned(),
]))?;
util::create_dir(&util::generate_path(vec![
&path,
&DEFAULT_ASSET_DIR_NAME.to_owned(),
&DEFAULT_ASSET_SUBDIR_METADATA_NAME.to_owned(),
]))?;
Ok(())
}
fn get_output_paths(&self, num_paths: usize) -> Vec<String> {
let mut output_paths = vec![String::new(); num_paths];
let mut idx = self.idx;
for i in 0..num_paths {
let output_path = util::generate_path(vec![
&String::from(self.output_path),
&String::from(DEFAULT_ASSET_DIR_NAME),
&String::from(DEFAULT_ASSET_SUBDIR_IMG_NAME),
&format!("{}.png", idx),
]);
output_paths[i] = output_path;
idx += 1;
}
output_paths
}
pub fn generate(&self) -> Result<(), std::io::Error> {
util::remove_dir_all(self.output_path)?;
let mixed_asset_paths = self.gen_mix_assets_paths();
let output_paths: Vec<String> = self.get_output_paths(mixed_asset_paths.len());
self.set_up_output_directory()?;
let num = self.num_assets as usize;
let img_handler = Arc::new(ImageHandler::new(
num,
mixed_asset_paths,
output_paths,
self.network.clone(),
self.output_path.to_owned(),
self.delimeter,
self.idx,
self.metadata_header.clone(),
self.base_uri.to_owned(),
));
let img_handler2 = img_handler.clone();
let img_handler3 = img_handler.clone();
let handle = thread::spawn(move || {
img_handler.display_progress();
});
let handle2 = thread::spawn(move || {
img_handler2.image_to_disk();
});
let handle3 = thread::spawn(move || {
img_handler3.gen_img_metadata().unwrap();
});
let threads: [JoinHandle<()>; 3] = [handle, handle2, handle3];
for h in threads.into_iter() {
h.join().unwrap();
}
Ok(())
}
fn get_weight_from_file_name(&self, file_name: &str) -> f32 {
let reg_exp = &format!(r"{}([0-9\\.]*)[^\\.a-z]", self.delimeter)[..];
let re = regex::Regex::new(reg_exp).unwrap();
if !file_name.contains(self.delimeter) {
return 1.0;
}
let mut w: f32 = 0.0;
for cap in re.captures_iter(file_name) {
w = String::from(&cap[0])
.replace(self.delimeter, "")
.parse::<f32>()
.unwrap();
if w == 0.0 {
w = 1.0;
}
}
w
}
fn get_asset_weights(&self, _layer: &Vec<String>) -> Vec<f32> {
let mut weights: Vec<f32> = Vec::with_capacity(_layer.len());
_layer
.iter()
.for_each(|l| weights.push(self.get_weight_from_file_name(l)));
weights
}
fn get_prob_from_weights(&self, weights: &Vec<f32>) -> Vec<f32> {
let mut prob: Vec<f32> = vec![0.0; weights.len()];
let weights_sum: f32 = weights.iter().sum();
for i in 0..weights.len() {
prob[i] = weights[i] / weights_sum;
}
prob
}
fn create_prob_range_vec(&self, prob: Vec<f32>) -> Vec<(f32, f32)> {
let mut prob_range: Vec<(f32, f32)> = vec![(0.0, 0.0); prob.len()];
let mut current_range: f32 = 0.0;
for (idx, val) in prob.into_iter().enumerate() {
prob_range[idx] = (current_range, current_range + val);
current_range += val;
}
prob_range
}
fn get_weighted_index(&self, prob_range: &[(f32, f32)]) -> usize {
let mut rng = rand::thread_rng();
let rand = rng.gen::<f32>();
let mut _idx: usize = 0;
for (idx, range) in prob_range.iter().enumerate() {
if rand >= range.0 && rand < range.1 {
return idx;
}
}
_idx
}
fn skip_layer(&self, layer_name: &str) -> bool {
let idx = self
.layer_order
.iter()
.position(|&l| l == layer_name)
.unwrap();
let mut rng = rand::thread_rng();
let rand = rng.gen::<f32>();
if rand < self.layer_exclusion_prob[idx] {
return true;
}
false
}
fn get_weighted_asset(&self, prob_range: &[(f32, f32)]) -> usize {
self.get_weighted_index(prob_range)
}
fn create_asset_path(
&self,
assets: &HashMap<String, Vec<String>>,
layer_prob_ranges: &HashMap<String, Vec<(f32, f32)>>,
) -> (Vec<String>, String) {
let mut asset_path: Vec<String> = Vec::new();
let mut concat_path = String::new();
let mut _idx: usize = 0;
for odr in &self.layer_order {
if !assets.contains_key(*odr) {
panic!(
"Layer {} does not exist in assets {:?}",
*odr,
assets.keys()
);
}
if self.skip_layer(*odr) {
continue;
}
let _layer = assets.get(*odr).unwrap();
let prob_range = layer_prob_ranges.get(&odr.to_string()).unwrap();
let _asset_name = _layer[self.get_weighted_asset(prob_range)].clone();
let full_asset_path = util::generate_path(vec![
&String::from(self.root_asset_path),
&String::from(*odr),
&_asset_name,
]);
concat_path.push_str(&full_asset_path);
asset_path.push(full_asset_path.clone());
_idx += 1;
}
(asset_path, sha256::digest(concat_path))
}
fn create_layer_prob_ranges(
&self,
assets: &HashMap<String, Vec<String>>,
) -> HashMap<String, Vec<(f32, f32)>> {
let mut layer_prob_ranges: HashMap<String, Vec<(f32, f32)>> = HashMap::new();
for (key, val) in assets.iter() {
let weights = self.get_asset_weights(val);
let prob = self.get_prob_from_weights(&weights);
let prob_range = self.create_prob_range_vec(prob);
layer_prob_ranges.insert(key.to_string(), prob_range);
}
layer_prob_ranges
}
fn gen_mix_assets_paths(&self) -> Vec<Vec<String>> {
let assets = self.get_assets();
let mut _assets_path_store: Vec<Vec<String>> = vec![Vec::new(); self.num_assets as usize];
let layer_prob_ranges = self.create_layer_prob_ranges(&assets);
let mut ctr: usize = 0;
let mut dna_store: Vec<String> = vec![String::new(); self.num_assets as usize];
while ctr < _assets_path_store.len() {
print!("\rFound: {}/{} unique assets", ctr + 1, self.num_assets);
std::io::stdout().flush().unwrap();
let (path, hash) = self.create_asset_path(&assets, &layer_prob_ranges);
if dna_store.contains(&hash) {
continue;
}
dna_store[ctr] = hash;
_assets_path_store[ctr] = path;
ctr += 1;
}
print!("\n");
_assets_path_store
}
pub fn get_assets(&self) -> HashMap<String, Vec<String>> {
let mut asset_store: HashMap<String, Vec<String>> = HashMap::new();
let paths = fs::read_dir(&self.root_asset_path).unwrap();
for path in paths {
let p = path.unwrap().path().into_os_string().into_string().unwrap();
let key = util::get_subdirectory(&p[..], 1);
let inner_paths = fs::read_dir(&p[..]).unwrap();
let mut img_name_store: Vec<String> = Vec::new();
for inner_path in inner_paths {
let p = inner_path
.unwrap()
.path()
.into_os_string()
.into_string()
.unwrap();
let img_name = util::get_subdirectory(&p[..], 1);
img_name_store.push(img_name);
}
asset_store.insert(key, img_name_store);
}
asset_store
}
pub fn print_assets(&self) {
let paths = fs::read_dir(&self.root_asset_path).unwrap();
for path in paths {
let p = path.unwrap().path().into_os_string().into_string().unwrap();
let inner_paths = fs::read_dir(&p[..]).unwrap();
for inner_path in inner_paths {
println!("{}", inner_path.unwrap().path().display());
}
}
}
}
#[derive(Debug, Clone)]
struct ImageHandler {
num_assets: usize,
mixed_asset_paths: Vec<Vec<String>>,
output_paths: Vec<String>,
network: Network,
output_path: String,
delimeter: char,
idx: usize,
metadata_header: MetadataHeader,
base_uri: String,
}
impl ImageHandler {
fn new(
num_assets: usize,
mixed_asset_paths: Vec<Vec<String>>,
output_paths: Vec<String>,
network: Network,
output_path: String,
delimeter: char,
idx: usize,
metadata_header: MetadataHeader,
base_uri: String,
) -> Self {
Self {
num_assets,
mixed_asset_paths,
output_paths,
network,
output_path,
delimeter,
idx,
metadata_header,
base_uri,
}
}
fn image_to_disk(&self) {
self.mixed_asset_paths
.clone()
.into_par_iter()
.enumerate()
.for_each(|(idx, paths)| {
let img_store = self.layer_img_store(&paths);
let mut base_img = img_store[0].clone();
for img in img_store.into_iter() {
image::imageops::overlay(&mut base_img, &img, 0, 0);
}
base_img.save(self.output_paths[idx].clone()).unwrap();
});
}
fn layer_img_store(&self, paths: &Vec<String>) -> Vec<image::DynamicImage> {
paths
.into_par_iter()
.map(|path| image::open(path).unwrap())
.collect()
}
fn sol_metadata(&self, asset_paths: &[String], idx: usize) -> json::JsonValue {
let mut metadata = self.metadata_header.clone();
let append_to_name = format!(" {}{}", self.delimeter, idx);
metadata.collection_name.push_str(&append_to_name);
let creators = metadata.creators.clone();
let mut img_metadata = metadata.to_json();
img_metadata["image"] = format!("{}.png", idx).into();
img_metadata["edition"] = idx.into();
img_metadata.remove("creators");
let mut att_store: Vec<json::JsonValue> = Vec::new();
for path in asset_paths {
let trait_type = util::get_subdirectory(&path[..], 2);
let value = self.get_value_from_asset(&util::get_subdirectory(&path[..], 1));
let att: Attributes = Attributes::new(trait_type, value);
att_store.push(att.to_json());
}
let files = Files::new(format!("{}.png", idx), "image/png".to_owned());
let category = "image".to_owned();
let prop = Properties::new(vec![files], category, creators);
let prop_json = prop.to_json();
img_metadata["attributes"] = att_store.into();
img_metadata["properties"] = prop_json;
img_metadata
}
fn eth_metadata(&self, asset_paths: &[String], idx: usize) -> json::JsonValue {
let mut metadata = self.metadata_header.clone();
let append_to_name = format!(" {}{}", self.delimeter, idx);
metadata.collection_name.push_str(&append_to_name);
let mut img_metadata = metadata.to_json();
img_metadata["image"] = format!("{}{}{}.png", &self.base_uri, '/', idx).into();
img_metadata["edition"] = idx.into();
img_metadata.remove("creators");
let mut att_store: Vec<json::JsonValue> = Vec::new();
for path in asset_paths {
let trait_type = util::get_subdirectory(&path[..], 2);
let value = self.get_value_from_asset(&util::get_subdirectory(&path[..], 1));
let att: Attributes = Attributes::new(trait_type, value);
att_store.push(att.to_json());
}
img_metadata["attributes"] = att_store.into();
let compiler: String = "Rust NFT image and metadata generator".to_owned();
img_metadata["compiler"] = compiler.into();
img_metadata.remove("symbol");
img_metadata.remove("seller_fee_basis_points");
img_metadata
}
fn gen_img_metadata(&self) -> std::io::Result<()> {
let full_metadata: Vec<String> = self
.mixed_asset_paths
.clone()
.into_par_iter()
.enumerate()
.map(|(idx, asset_path)| {
let metadata = match self.network {
Network::Eth => self.eth_metadata(&asset_path, self.idx + idx),
Network::Sol => self.sol_metadata(&asset_path, self.idx + idx),
};
let data = metadata.to_string();
let path = util::generate_path(vec![
&self.output_path.clone(),
&String::from(DEFAULT_ASSET_DIR_NAME),
&String::from(DEFAULT_ASSET_SUBDIR_METADATA_NAME),
&format!("{}.json", self.idx + idx),
]);
self.output_metadata_to_disk(path, &data).unwrap();
data
})
.collect();
let full_metadata = self.format_full_metadata(full_metadata);
let path = util::generate_path(vec![
&self.output_path.clone(),
&String::from(DEFAULT_METADATA_DIR_NAME),
&String::from("_metadata.json"),
]);
self.output_metadata_to_disk(path, &full_metadata)?;
Ok(())
}
fn get_value_from_asset(&self, asset_name: &str) -> String {
let split_asset_name: Vec<&str> = asset_name.split(self.delimeter).collect();
String::from(split_asset_name[0])
}
fn output_metadata_to_disk(&self, path: String, metadata: &String) -> std::io::Result<()> {
std::fs::write(path, metadata)?;
Ok(())
}
fn format_full_metadata(&self, data: Vec<String>) -> String {
let data_len = data.len();
let mut data_store: Vec<String> = vec![String::new(); data_len];
for (idx, mut val) in data.into_iter().enumerate() {
if idx < data_len - 1 {
val.push(',');
}
data_store[idx] = val
}
let str_data_store = String::from_iter(data_store);
let mut s_bracket = String::from("[");
let e_bracket = String::from("]");
s_bracket.push_str(&str_data_store);
s_bracket.push_str(&e_bracket);
s_bracket
}
fn display_progress(&self) {
let path_imgs = util::generate_path(vec![
&self.output_path,
&DEFAULT_ASSET_DIR_NAME.to_owned(),
&DEFAULT_ASSET_SUBDIR_IMG_NAME.to_owned(),
]);
let path_mtd = util::generate_path(vec![
&self.output_path,
&DEFAULT_ASSET_DIR_NAME.to_owned(),
&DEFAULT_ASSET_SUBDIR_METADATA_NAME.to_owned(),
]);
let mut count = 0;
while count < self.num_assets as u64 {
let path_imgs = fs::read_dir(&path_imgs).unwrap();
let path_mtd = fs::read_dir(&path_mtd).unwrap();
let mut total_imgs = 0;
let mut total_mtd = 0;
for _ in path_imgs {
total_imgs += 1;
}
for _ in path_mtd {
total_mtd += 1;
}
print!(
"\rGenerating images and metadata... Images: {}/{}, Metadata: {}/{}",
total_imgs, self.num_assets, total_mtd, self.num_assets
);
std::io::stdout().flush().unwrap();
count = total_imgs;
}
print!("\n");
println!("Saving concatenated metadata to disk...");
}
}