Skip to content
Snippets Groups Projects
pipeline.rs 7.68 KiB
Newer Older
Louis's avatar
Louis committed
use std::collections::HashMap;
Louis's avatar
Louis committed
use std::path::{Path, PathBuf};
Louis's avatar
Louis committed

use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::cli_args::CrunchCommand;
use crate::format::make_paths;
use crate::utils::normalize_path;
Louis's avatar
Louis committed
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,
Louis's avatar
Louis committed
		actions: Vec<CrunchCommand>,
Louis's avatar
Louis committed
	},
	Ref {
		input_path: String,
		output_path: String,
		reference: String,
	},
Louis's avatar
Louis committed
	GlobRef {
		pattern: String,
		output_dir: String,
		reference: String,
	},
Louis's avatar
Louis committed
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PipelineRef {
Louis's avatar
Louis committed
	pub actions: Vec<CrunchCommand>,
Louis's avatar
Louis committed
}

#[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 = std::env::current_dir().map(|path| path.join(file_path.to_string()))?;
	let path_string = format!("{}", &path.display());

	log::debug!("Trying to read from input file: {}", &path.display());

	if !&path_string.ends_with(".toml") && !&path_string.ends_with(".json") {
Louis's avatar
Louis committed
		Err(PipelineError::FormatDetection)?;
	}

	let file_contents = std::fs::read(&path)?;

	log::debug!("Found correct file type and read bytes, trying to parse");
	let pipeline_data: PipelineFile = if path_string.ends_with(".toml") {
Louis's avatar
Louis committed
		toml::from_slice(&file_contents)?
	} else {
		serde_json::from_slice(&file_contents)?
	};

	log::debug!("Expanding pipeline file into targets");
	let base_path = PathBuf::from(path.parent().unwrap());
	get_targets(base_path.clone(), &pipeline_data).for_each(
		|(input_path, output_path, actions)| {
			match make_paths(&output_path) {
				Ok(_) => {}
				Err(e) => {
					log::error!("Failed to create target directory {}; {}", &output_path, e);
					return;
			if actions.is_empty() {
				match std::fs::copy(&input_path, &output_path) {
					Ok(_) => {}
					Err(e) => {
						log::error!("Failed to copy {} to {}; {}", input_path, output_path, e);
					}
				};
Louis's avatar
Louis committed
				return;
			}

			let mut file = match load_image(&input_path, None) {
				Ok(image) => image,
				Err(e) => {
					log::error!("Error loading {}; {:?}", &input_path, e);
					return;
Louis's avatar
Louis committed
				}
			log::debug!(
				"Loaded {}, Executing {} actions",
				&input_path,
				actions.len()
			);
			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(join(&base_path, &palette_file), None) {
							Ok(p) => p,
							Err(e) => {
								log::error!(
									"Failed to load palette {} at step {}; {:?}",
									&palette_file,
									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,
			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);
				}
fn join<T: AsRef<Path>>(root: &Path, rest: &T) -> String {
	let path = normalize_path(root.join(rest));
	format!("{}", path.display())
}

Louis's avatar
Louis committed
fn get_targets(
	base_path: PathBuf,
Louis's avatar
Louis committed
	pipeline_data: &PipelineFile,
) -> impl ParallelIterator<Item = (String, String, Vec<CrunchCommand>)> + '_ {
Louis's avatar
Louis committed
	pipeline_data
		.pipelines
		.par_iter()
		.flat_map(move |pipe| match pipe {
Louis's avatar
Louis committed
			PipelineType::Pipeline {
				input_path,
				output_path,
				actions,
			} => vec![(
				join(&base_path, &input_path),
				join(&base_path, &output_path),
				actions.clone(),
			)],
Louis's avatar
Louis committed
			PipelineType::Ref {
				input_path,
				output_path,
				reference,
			} => pipeline_data
				.refs
Louis's avatar
Louis committed
				.get(reference.as_str())
Louis's avatar
Louis committed
				.iter()
				.map(|value| {
					(
						join(&base_path, &input_path),
						join(&base_path, &output_path),
Louis's avatar
Louis committed
						(*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();
					let target_path = join(&base_path, pattern);

					log::debug!("Mapping glob paths for '{}'", &target_path);
					for entry in glob::glob(target_path.as_str()).unwrap() {
						log::debug!("Found a glob match: [{:?}]", entry);
Louis's avatar
Louis committed
						paths.push((actions.clone(), entry));
Louis's avatar
Louis committed
					}
Louis's avatar
Louis committed
					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);
Louis's avatar
Louis committed

Louis's avatar
Louis committed
						Some((
							join(&base_path, &path),
							join(&base_path, &output_path),
Louis's avatar
Louis committed
							actions,
						))
					} else {
						None
					}
				})
				.collect(),
		})
Louis's avatar
Louis committed
}