528 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			528 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use image::GenericImageView;
 | |
| use std::io::Write;
 | |
| 
 | |
| pub fn fetch(extract_destination: &std::path::Path, verbose: bool) {
 | |
|     // prep working directory
 | |
|     match create_working_directory() {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error creating working directory: {}", e);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // download pokemon.json
 | |
|     match fetch_pokemon_json() {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error fetching pokemon_raw.json: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // process pokemon_raw.json
 | |
|     match process_pokemon_json() {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error processing pokemon_raw.json: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // download colorscripts archive
 | |
|     match fetch_colorscripts_archive(crate::constants::TARGET_URL) {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error fetching colorscripts archive: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // extract colorscripts archive
 | |
|     // now we have the raw images
 | |
|     match extract_colorscripts_archive() {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error extracting colorscripts archive: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // crop images to content
 | |
|     match crop_all_images_in_directory() {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error cropping images: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // convert images to unicode, both small and big
 | |
|     match convert_images_to_ascii(extract_destination, verbose) {
 | |
|         Ok(_) => (),
 | |
|         Err(e) => {
 | |
|             eprintln!("Error converting images to ASCII: {}", e);
 | |
|             cleanup().unwrap();
 | |
|             std::process::exit(1);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // cleanup
 | |
|     // TODO: uncomment
 | |
|     // match cleanup() {
 | |
|     //     Ok(_) => (),
 | |
|     //     Err(e) => eprintln!("Error cleaning up: {}", e),
 | |
|     // };
 | |
| }
 | |
| 
 | |
| fn create_working_directory() -> std::io::Result<()> {
 | |
|     println!(
 | |
|         "Creating working directory at {:?}...",
 | |
|         &*crate::constants::CACHE_DIRECTORY
 | |
|     );
 | |
|     // create intermediate directories also
 | |
|     std::fs::create_dir(&*crate::constants::CACHE_DIRECTORY)?;
 | |
|     println!("Created working directory");
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| fn fetch_pokemon_json() -> Result<(), Box<dyn std::error::Error>> {
 | |
|     println!("Fetching pokemon_raw.json...");
 | |
| 
 | |
|     let response = reqwest::blocking::get(
 | |
|         "https://raw.githubusercontent.com/Vomitblood/pokesprite/master/data/pokemon.json",
 | |
|     )?;
 | |
| 
 | |
|     let mut dest = std::fs::File::create(
 | |
|         &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("pokemon_raw.json"),
 | |
|     )?;
 | |
| 
 | |
|     let response_body = response.error_for_status()?.bytes()?;
 | |
|     std::io::copy(&mut response_body.as_ref(), &mut dest)?;
 | |
| 
 | |
|     println!("Downloaded pokemon_raw.json");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug)]
 | |
| struct Slug {
 | |
|     eng: String,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug)]
 | |
| struct Forms {
 | |
|     // ignoring actual details in the forms and just capturing form names
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug)]
 | |
| struct Generation {
 | |
|     forms: std::collections::HashMap<String, Forms>,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug)]
 | |
| struct Pokemon {
 | |
|     idx: String,
 | |
|     slug: Slug,
 | |
|     #[serde(rename = "gen-8")]
 | |
|     gen_8: Generation,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, serde::Deserialize, Debug)]
 | |
| struct PokemonCollection {
 | |
|     #[serde(flatten)]
 | |
|     entries: std::collections::HashMap<String, Pokemon>,
 | |
| }
 | |
| 
 | |
| #[derive(serde::Serialize, Debug)]
 | |
| struct ProcessedPokemon {
 | |
|     pokedex: String,
 | |
|     name: String,
 | |
|     forms: Vec<String>,
 | |
| }
 | |
| 
 | |
| fn process_pokemon_json() -> Result<(), Box<dyn std::error::Error>> {
 | |
|     println!("Generating pokemon.json...");
 | |
| 
 | |
|     let pokemon_raw_json_path = &*crate::constants::CACHE_DIRECTORY.join("pokemon_raw.json");
 | |
| 
 | |
|     let pokemon_collection = read_pokemon_file(pokemon_raw_json_path)?;
 | |
| 
 | |
|     let processed_pokemon = transform_pokemon_data(&pokemon_collection.entries);
 | |
| 
 | |
|     // serialize the processed data to JSON
 | |
|     let serialized_pokemon = serde_json::to_string_pretty(&processed_pokemon)?;
 | |
| 
 | |
|     // write processed data to file
 | |
|     std::fs::write(
 | |
|         crate::constants::CACHE_DIRECTORY.join("processed_pokemon.json"),
 | |
|         serialized_pokemon,
 | |
|     )?;
 | |
| 
 | |
|     println!("Generated pokemon.json");
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| fn read_pokemon_file(
 | |
|     file_path: &std::path::Path,
 | |
| ) -> Result<PokemonCollection, Box<dyn std::error::Error>> {
 | |
|     // open the file in read only mode
 | |
|     let file = std::fs::File::open(file_path)?;
 | |
|     let reader = std::io::BufReader::new(file);
 | |
| 
 | |
|     // deserialize the into pokemoncollection
 | |
|     let collection = serde_json::from_reader(reader)?;
 | |
| 
 | |
|     return Ok(collection);
 | |
| }
 | |
| 
 | |
| fn transform_pokemon_data(
 | |
|     pokemons: &std::collections::HashMap<String, Pokemon>,
 | |
| ) -> Vec<ProcessedPokemon> {
 | |
|     let mut processed_pokemons: Vec<ProcessedPokemon> = pokemons
 | |
|         .iter()
 | |
|         .map(|(_key, p)| {
 | |
|             let forms = p
 | |
|                 .gen_8
 | |
|                 .forms
 | |
|                 .keys()
 | |
|                 .map(|key| match key.as_str() {
 | |
|                     "$" => "regular".to_string(),
 | |
|                     _ => key.clone(),
 | |
|                 })
 | |
|                 .collect::<Vec<String>>();
 | |
| 
 | |
|             ProcessedPokemon {
 | |
|                 // remove leading zeros from the pokedex number
 | |
|                 pokedex: p.idx.trim_start_matches('0').to_string(),
 | |
|                 // use the slug as the name
 | |
|                 // this is because i am too lazy to decapitalize the name
 | |
|                 // also in case of name =/= slug
 | |
|                 name: p.slug.eng.clone(),
 | |
|                 forms,
 | |
|             }
 | |
|         })
 | |
|         .collect();
 | |
| 
 | |
|     // sort the vector by pokedex number
 | |
|     processed_pokemons.sort_by(|a, b| {
 | |
|         a.pokedex
 | |
|             .parse::<u32>()
 | |
|             .unwrap_or(0)
 | |
|             .cmp(&b.pokedex.parse::<u32>().unwrap_or(0))
 | |
|     });
 | |
| 
 | |
|     return processed_pokemons;
 | |
| }
 | |
| 
 | |
| fn fetch_colorscripts_archive(target_url: &str) -> Result<(), Box<dyn std::error::Error>> {
 | |
|     println!("Fetching colorscripts archive...");
 | |
| 
 | |
|     let response = reqwest::blocking::get(target_url)?;
 | |
| 
 | |
|     let mut dest = std::fs::File::create(
 | |
|         &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("pokesprite.zip"),
 | |
|     )?;
 | |
| 
 | |
|     let response_body = response.error_for_status()?.bytes()?;
 | |
|     std::io::copy(&mut response_body.as_ref(), &mut dest)?;
 | |
| 
 | |
|     println!("Downloaded colorscripts archive");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| fn extract_colorscripts_archive() -> zip::result::ZipResult<()> {
 | |
|     println!("Extracting colorscripts archive...");
 | |
| 
 | |
|     let archive_file = std::fs::File::open(
 | |
|         &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("pokesprite.zip"),
 | |
|     )?;
 | |
|     let mut archive = zip::read::ZipArchive::new(std::io::BufReader::new(archive_file))?;
 | |
| 
 | |
|     // iterate over every single file in the archive
 | |
|     for i in 0..archive.len() {
 | |
|         let mut file = archive.by_index(i)?;
 | |
|         let file_path = std::path::Path::new(file.name());
 | |
| 
 | |
|         let parent_dir = file_path
 | |
|             .parent()
 | |
|             .and_then(std::path::Path::file_name)
 | |
|             .and_then(std::ffi::OsStr::to_str)
 | |
|             .unwrap_or("");
 | |
| 
 | |
|         // check if the file is a in the correct directory that is NOT a directory
 | |
|         if (file
 | |
|             .name()
 | |
|             .starts_with("pokesprite-master/pokemon-gen8/regular/")
 | |
|             && parent_dir == "regular")
 | |
|             || (file
 | |
|                 .name()
 | |
|                 .starts_with("pokesprite-master/pokemon-gen8/shiny/")
 | |
|                 && parent_dir == "shiny")
 | |
|         {
 | |
|             let file_name = file_path
 | |
|                 .file_name()
 | |
|                 .and_then(std::ffi::OsStr::to_str)
 | |
|                 .unwrap();
 | |
| 
 | |
|             let outpath = &*crate::constants::CACHE_DIRECTORY
 | |
|                 .join("raw_images")
 | |
|                 .join(parent_dir)
 | |
|                 .join(file_name);
 | |
| 
 | |
|             if !file.name().ends_with('/') {
 | |
|                 if let Some(p) = outpath.parent() {
 | |
|                     if !p.exists() {
 | |
|                         std::fs::create_dir_all(&p)?;
 | |
|                     }
 | |
|                 }
 | |
|                 let mut outfile = std::fs::File::create(&outpath)?;
 | |
|                 std::io::copy(&mut file, &mut outfile)?;
 | |
|             };
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     println!("Extracted colorscripts archive");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| fn crop_all_images_in_directory() -> std::io::Result<()> {
 | |
|     println!("Cropping images...");
 | |
| 
 | |
|     // make sure the cropped_images directory exists
 | |
|     std::fs::create_dir_all(
 | |
|         &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("cropped_images"),
 | |
|     )?;
 | |
| 
 | |
|     // do for both regular and shiny subdirectories
 | |
|     for subdirectory in ["regular", "shiny"].iter() {
 | |
|         let input_subdirectory_path = &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("raw_images")
 | |
|             .join(subdirectory);
 | |
|         let output_subdirectory_path = &*crate::constants::CACHE_DIRECTORY
 | |
|             .to_path_buf()
 | |
|             .join("cropped_images")
 | |
|             .join(subdirectory);
 | |
| 
 | |
|         std::fs::create_dir_all(&output_subdirectory_path)?;
 | |
| 
 | |
|         for entry in std::fs::read_dir(&input_subdirectory_path)? {
 | |
|             let entry = entry?;
 | |
|             let path = entry.path();
 | |
| 
 | |
|             let output_path = output_subdirectory_path.join(path.file_name().unwrap());
 | |
|             crop_to_content(&path, &output_path).unwrap();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     println!("Cropped images");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| fn crop_to_content(
 | |
|     input_path: &std::path::Path,
 | |
|     output_path: &std::path::Path,
 | |
| ) -> image::ImageResult<image::DynamicImage> {
 | |
|     // load the image
 | |
|     let img = image::open(input_path)?;
 | |
| 
 | |
|     let (width, height) = img.dimensions();
 | |
|     let mut min_x = width;
 | |
|     let mut min_y = height;
 | |
|     let mut max_x = 0;
 | |
|     let mut max_y = 0;
 | |
| 
 | |
|     for y in 0..height {
 | |
|         for x in 0..width {
 | |
|             let pixel = img.get_pixel(x, y);
 | |
|             if pixel[3] != 0 {
 | |
|                 // if pixel is not transparent
 | |
|                 if x < min_x {
 | |
|                     min_x = x;
 | |
|                 }
 | |
|                 if y < min_y {
 | |
|                     min_y = y;
 | |
|                 }
 | |
|                 if x > max_x {
 | |
|                     max_x = x;
 | |
|                 }
 | |
|                 if y > max_y {
 | |
|                     max_y = y;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     let cropped_width = max_x - min_x + 1;
 | |
|     let cropped_height = max_y - min_y + 1;
 | |
| 
 | |
|     let mut cropped_img: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =
 | |
|         image::ImageBuffer::new(cropped_width, cropped_height);
 | |
| 
 | |
|     for y in 0..cropped_height {
 | |
|         for x in 0..cropped_width {
 | |
|             let pixel = img.get_pixel(x + min_x, y + min_y);
 | |
|             cropped_img.put_pixel(x, y, pixel);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     let cropped_img = image::DynamicImage::ImageRgba8(cropped_img);
 | |
| 
 | |
|     // write the cropped image
 | |
|     cropped_img.save(output_path)?;
 | |
| 
 | |
|     return Ok(cropped_img);
 | |
| }
 | |
| 
 | |
| fn convert_images_to_ascii(
 | |
|     output_directory_path: &std::path::Path,
 | |
|     verbose: bool,
 | |
| ) -> std::io::Result<()> {
 | |
|     println!("Extract destination: {:?}", output_directory_path);
 | |
|     println!("Converting images to ASCII...");
 | |
| 
 | |
|     std::fs::create_dir_all(output_directory_path)?;
 | |
| 
 | |
|     for size in ["small", "big"].iter() {
 | |
|         for subdirectory in ["regular", "shiny"].iter() {
 | |
|             let input_subdirectory_path = &*crate::constants::CACHE_DIRECTORY
 | |
|                 .join("cropped_images")
 | |
|                 .join(subdirectory);
 | |
|             let output_subdirectory_path = output_directory_path
 | |
|                 .join("colorscripts")
 | |
|                 .join(size)
 | |
|                 .join(subdirectory);
 | |
| 
 | |
|             std::fs::create_dir_all(&output_subdirectory_path)?;
 | |
| 
 | |
|             for entry in std::fs::read_dir(&input_subdirectory_path)? {
 | |
|                 let entry = entry?;
 | |
|                 let path = entry.path();
 | |
| 
 | |
|                 if path.is_file() {
 | |
|                     let img = image::open(&path).unwrap();
 | |
|                     let ascii_art = if *size == "small" {
 | |
|                         convert_image_to_unicode_small(&img)
 | |
|                     } else {
 | |
|                         convert_image_to_unicode_big(&img)
 | |
|                     };
 | |
| 
 | |
|                     // print for fun
 | |
|                     if verbose == true {
 | |
|                         println!("{}", ascii_art);
 | |
|                     };
 | |
| 
 | |
|                     let output_path = output_subdirectory_path.join(path.file_stem().unwrap());
 | |
|                     let mut file = std::fs::File::create(output_path)?;
 | |
|                     file.write_all(ascii_art.as_bytes())?;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     println!("Converted images to ASCII");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 | |
| 
 | |
| fn convert_image_to_unicode_small(img: &image::DynamicImage) -> String {
 | |
|     let mut unicode_sprite = String::new();
 | |
|     let (width, height) = img.dimensions();
 | |
| 
 | |
|     for y in (0..height).step_by(2) {
 | |
|         for x in 0..width {
 | |
|             let upper_pixel = img.get_pixel(x, y);
 | |
|             let lower_pixel = if y + 1 < height {
 | |
|                 img.get_pixel(x, y + 1)
 | |
|             } else {
 | |
|                 // fallback to upper pixel if there's no lower pixel.
 | |
|                 upper_pixel
 | |
|             };
 | |
| 
 | |
|             if upper_pixel[3] == 0 && lower_pixel[3] == 0 {
 | |
|                 unicode_sprite.push(' ');
 | |
|             } else if upper_pixel[3] == 0 {
 | |
|                 unicode_sprite.push_str(&get_color_escape_code(lower_pixel, false));
 | |
|                 unicode_sprite.push('▄');
 | |
|             } else if lower_pixel[3] == 0 {
 | |
|                 unicode_sprite.push_str(&get_color_escape_code(upper_pixel, false));
 | |
|                 unicode_sprite.push('▀');
 | |
|             } else {
 | |
|                 unicode_sprite.push_str(&get_color_escape_code(upper_pixel, false));
 | |
|                 unicode_sprite.push_str(&get_color_escape_code(lower_pixel, true));
 | |
|                 unicode_sprite.push('▀');
 | |
|             }
 | |
|             unicode_sprite.push_str("\x1b[0m"); // Reset ANSI code after each character
 | |
|         }
 | |
|         unicode_sprite.push('\n'); // New line for each row, plus reset might be added here too if colors extend beyond.
 | |
|     }
 | |
| 
 | |
|     return unicode_sprite;
 | |
| }
 | |
| 
 | |
| fn convert_image_to_unicode_big(img: &image::DynamicImage) -> String {
 | |
|     let mut unicode_sprite = String::new();
 | |
|     let (width, height) = img.dimensions();
 | |
| 
 | |
|     for y in 0..height {
 | |
|         for x in 0..width {
 | |
|             let pixel = img.get_pixel(x, y);
 | |
| 
 | |
|             if pixel[3] == 0 {
 | |
|                 unicode_sprite.push_str("  ");
 | |
|             } else {
 | |
|                 unicode_sprite.push_str(&get_color_escape_code(pixel, false));
 | |
|                 unicode_sprite.push_str("██");
 | |
|             }
 | |
|         }
 | |
|         unicode_sprite.push('\n');
 | |
|     }
 | |
| 
 | |
|     return unicode_sprite;
 | |
| }
 | |
| 
 | |
| fn get_color_escape_code(pixel: image::Rgba<u8>, background: bool) -> String {
 | |
|     if pixel[3] == 0 {
 | |
|         return format!("{}", crossterm::style::ResetColor);
 | |
|     }
 | |
| 
 | |
|     let color = crossterm::style::Color::Rgb {
 | |
|         r: pixel[0],
 | |
|         g: pixel[1],
 | |
|         b: pixel[2],
 | |
|     };
 | |
| 
 | |
|     if background {
 | |
|         format!("{}", crossterm::style::SetBackgroundColor(color))
 | |
|     } else {
 | |
|         format!("{}", crossterm::style::SetForegroundColor(color))
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn cleanup() -> std::io::Result<()> {
 | |
|     println!("Cleaning up...");
 | |
| 
 | |
|     std::fs::remove_dir_all(&*crate::constants::CACHE_DIRECTORY)?;
 | |
| 
 | |
|     println!("Cleaned up");
 | |
| 
 | |
|     return Ok(());
 | |
| }
 |