Skip to content
Snippets Groups Projects
pipeline.rs 6.23 KiB
Newer Older
Louis's avatar
Louis committed
use glob::{glob_with, MatchOptions};
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::{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 = 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())?;
Louis's avatar
Louis committed
	let pipeline_data: PipelineFile = if path.ends_with(".toml") {
Louis's avatar
Louis committed
		toml::from_slice(&file_contents)?
	} else {
		serde_json::from_slice(&file_contents)?
	};

Louis's avatar
Louis committed
	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>)> + '_ {
Louis's avatar
Louis committed
	pipeline_data
		.pipelines
		.par_iter()
Louis's avatar
Louis committed
		.flat_map(|pipe| match pipe {
Louis's avatar
Louis committed
			PipelineType::Pipeline {
				input_path,
				output_path,
				actions,
Louis's avatar
Louis committed
			} => vec![(input_path.clone(), output_path.clone(), 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| {
					(
						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));
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((
							format!("{}", path.display()),
							format!("{}", output_path.display()),
							actions,
						))
					} else {
						None
					}
				})
				.collect(),
		})
Louis's avatar
Louis committed
}