From 1f1ff9efdf69e972997e9780cd8e362b037941df Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Mon, 8 Aug 2022 00:43:57 +0100
Subject: [PATCH] Add glob support to pipelines

---
 CHANGELOG.md             |   6 +
 Cargo.lock               |   9 +-
 Cargo.toml               |   3 +-
 rustfmt.toml             |   2 +-
 src/cli_args.rs          |   2 +-
 src/commands/pipeline.rs | 370 ++++++++++++++++++++++-----------------
 6 files changed, 225 insertions(+), 167 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7d13e2..e0ee4e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.4.0] - Unreleased
+### Added
+- Support for GlobRef definitions in a `pipeline.toml` file
+  - Takes a `pattern` glob expression instead of an `input_path`
+  - Takes a directory path as `output_dir` instead of a file path as `output_file`, and will construct the an `output_file` by appending the file name of a matched file to the output directory
+
 ## [0.3.0] - 2022-07-08
 ### Added
 - Added `flip` command, for flipping an image along one or both axes.
diff --git a/Cargo.lock b/Cargo.lock
index 469dd3b..9b2200f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -183,13 +183,14 @@ dependencies = [
 
 [[package]]
 name = "crunch"
-version = "0.3.0"
+version = "0.4.0"
 dependencies = [
  "anyhow",
  "clap",
  "deltae",
  "env_logger",
  "glam",
+ "glob",
  "image",
  "lab",
  "log",
@@ -315,6 +316,12 @@ version = "0.20.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f"
 
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
 [[package]]
 name = "half"
 version = "1.8.2"
diff --git a/Cargo.toml b/Cargo.toml
index 4d76282..a9b05e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "crunch"
-version = "0.3.0"
+version = "0.4.0"
 edition = "2021"
 
 [dependencies]
@@ -18,3 +18,4 @@ thiserror = "1.0.30"
 serde = { version = "1.0.131", features = ["derive"] }
 toml = "0.5.9"
 serde_json = "1.0.81"
+glob = "0.3.0"
diff --git a/rustfmt.toml b/rustfmt.toml
index ddac46a..d62aed7 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,4 +1,4 @@
 hard_tabs = true
-group_imports = "StdExternalCrate"
+#group_imports = "StdExternalCrate"
 use_field_init_shorthand = true
 use_try_shorthand = true
\ No newline at end of file
diff --git a/src/cli_args.rs b/src/cli_args.rs
index 159e911..f178bbb 100644
--- a/src/cli_args.rs
+++ b/src/cli_args.rs
@@ -12,7 +12,7 @@ use crate::{load_image, Format};
 #[derive(Parser, Debug, Clone)]
 #[clap(name = "Crunch")]
 #[clap(author = "Louis Capitanchik <louis@microhacks.co.uk>")]
-#[clap(version = "0.3.0")]
+#[clap(version = "0.4.0")]
 #[clap(about, long_about = None)]
 pub struct Args {
 	/// The path to the spritesheet file
diff --git a/src/commands/pipeline.rs b/src/commands/pipeline.rs
index b439d3a..90e860f 100644
--- a/src/commands/pipeline.rs
+++ b/src/commands/pipeline.rs
@@ -1,5 +1,6 @@
+use glob::{glob_with, MatchOptions};
 use std::collections::HashMap;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 
 use rayon::prelude::*;
 use serde::{Deserialize, Serialize};
@@ -27,6 +28,11 @@ pub enum PipelineType {
 		output_path: String,
 		reference: String,
 	},
+	GlobRef {
+		pattern: String,
+		output_dir: String,
+		reference: String,
+	},
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -56,15 +62,165 @@ pub fn execute_pipeline<IN: ToString, OUT: ToString>(
 		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()
-		.filter_map(|pipe| match pipe {
+		.flat_map(|pipe| match pipe {
 			PipelineType::Pipeline {
 				input_path,
 				output_path,
 				actions,
-			} => Some((input_path, output_path, actions.clone())),
+			} => vec![(input_path.clone(), output_path.clone(), actions.clone())],
 			PipelineType::Ref {
 				input_path,
 				output_path,
@@ -72,166 +228,54 @@ pub fn execute_pipeline<IN: ToString, OUT: ToString>(
 			} => pipeline_data
 				.refs
 				.get(reference.as_str())
-				.map(|value| (input_path, output_path, value.actions.clone())),
-		})
-		.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;
-							}
-						};
+				.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));
 					}
-					CrunchCommand::Palette { .. } | CrunchCommand::Pipeline => continue,
-				}
+					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);
 
-				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(())
+						Some((
+							format!("{}", path.display()),
+							format!("{}", output_path.display()),
+							actions,
+						))
+					} else {
+						None
+					}
+				})
+				.collect(),
+		})
 }
-- 
GitLab