use glob::{glob_with, MatchOptions}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::cli_args::CrunchCommand; use crate::{commands, load_image}; #[derive(Error, Debug)] pub enum PipelineError { #[error("Use a file ending with '.toml' or '.json' to configure your pipeline")] FormatDetection, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum PipelineType { Pipeline { input_path: String, output_path: String, actions: Vec<CrunchCommand>, }, Ref { input_path: String, output_path: String, reference: String, }, GlobRef { pattern: String, output_dir: String, reference: String, }, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PipelineRef { pub actions: Vec<CrunchCommand>, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PipelineFile { pub refs: HashMap<String, PipelineRef>, pub pipelines: Vec<PipelineType>, } pub fn execute_pipeline<IN: ToString, OUT: ToString>( file_path: IN, _outpath: OUT, ) -> anyhow::Result<()> { let path = file_path.to_string(); if !&path.ends_with(".toml") && !&path.ends_with(".json") { Err(PipelineError::FormatDetection)?; } let file_contents = std::fs::read(file_path.to_string())?; let pipeline_data: PipelineFile = if path.ends_with(".toml") { toml::from_slice(&file_contents)? } else { serde_json::from_slice(&file_contents)? }; get_targets(&pipeline_data).for_each(|(input_path, output_path, actions)| { let mut file = match load_image(&input_path, None) { Ok(image) => image, Err(e) => { log::error!("Error loading {}; {:?}", &input_path, e); return; } }; let mut count = 1; for step in actions { match step { CrunchCommand::Extrude { tile_size, space_y, space_x, pad_y, pad_x, extrude, } => { file = match commands::extrude( file, tile_size, pad_x, pad_y, space_x, space_y, extrude, ) { Ok(f) => f, Err(e) => { log::error!( "Failed to extrude {} at step {}; {}", input_path, count, e ); return; } }; } CrunchCommand::Remap { palette_file } => { let palette_data = match load_image(&palette_file, None) { Ok(p) => p, Err(e) => { log::error!("Failed to load {} at step {}; {:?}", input_path, count, e); return; } }; let image_palette = match commands::palette(&file) { Ok(ip) => ip, Err(e) => { log::error!( "Failed to extract palette from {} at step {}; {}", input_path, count, e ); return; } }; let target_palette = match commands::palette(&palette_data) { Ok(tp) => tp, Err(e) => { log::error!( "Failed to extract palette from {} at step {}; {}", &palette_file, count, e ); return; } }; let mappings = commands::calculate_mapping(&image_palette, &target_palette); file = match commands::remap_image(file, mappings) { Ok(f) => f, Err(e) => { log::error!("Failed to remap {} at step {}; {}", input_path, count, e); return; } }; } CrunchCommand::Scale { factor } => { file = match commands::rescale(&file, factor) { Ok(f) => f, Err(e) => { log::error!("Failed to scale {} at step {}; {}", input_path, count, e); return; } }; } CrunchCommand::Rotate { amount } => { file = match commands::rotate(&file, amount) { Ok(f) => f, Err(e) => { log::error!( "Failed to rotate {} by {:?} step(s); {}", input_path, amount, e ); return; } }; } CrunchCommand::Flip { direction } => { file = match commands::flip(&file, direction) { Ok(f) => f, Err(e) => { log::error!( "Failed to flip {} in the following direction: {:?}; {}", input_path, direction, e ); return; } }; } CrunchCommand::Palette { .. } | CrunchCommand::Pipeline => continue, } count += 1; } let mut outer_target_path = PathBuf::from(&output_path); outer_target_path.pop(); if let Err(e) = std::fs::create_dir(&outer_target_path) { match e.kind() { std::io::ErrorKind::AlreadyExists => { /* This is fine */ } _ => log::error!( "Failed to create containing directory {}; {}", outer_target_path.to_string_lossy(), e ), } } match file.save(&output_path) { Ok(_) => {} Err(e) => { log::error!("Failed to save to {}; {}", output_path, e); } } }); Ok(()) } fn get_targets( pipeline_data: &PipelineFile, ) -> impl ParallelIterator<Item = (String, String, Vec<CrunchCommand>)> + '_ { pipeline_data .pipelines .par_iter() .flat_map(|pipe| match pipe { PipelineType::Pipeline { input_path, output_path, actions, } => vec![(input_path.clone(), output_path.clone(), actions.clone())], PipelineType::Ref { input_path, output_path, reference, } => pipeline_data .refs .get(reference.as_str()) .iter() .map(|value| { ( input_path.clone(), output_path.clone(), (*value).actions.clone(), ) }) .collect(), PipelineType::GlobRef { pattern, output_dir, reference, } => pipeline_data .refs .get(reference.as_str()) .iter() .map(|value| (*value).actions.clone()) .flat_map(|actions| { let mut paths = Vec::new(); for entry in glob_with( pattern.as_str(), MatchOptions { case_sensitive: true, ..Default::default() }, ) .unwrap() { paths.push((actions.clone(), entry)); } paths }) .filter_map(|(actions, inner)| inner.ok().map(|p| (actions, p))) .filter_map(|(actions, path)| { if let Some(filename) = path.file_name().and_then(|osstr| osstr.to_str()) { let output_path = Path::new(output_dir.as_str()); let output_path = output_path.join(filename); Some(( format!("{}", path.display()), format!("{}", output_path.display()), actions, )) } else { None } }) .collect(), }) }