diff --git a/src/cli_args.rs b/src/cli_args.rs
index 2d8923f2853acde71095b88031407327fc7c2725..3ad341776e4bb9422e498c22a9bb3bbaf3b23e8d 100644
--- a/src/cli_args.rs
+++ b/src/cli_args.rs
@@ -2,7 +2,7 @@ use clap::Parser;
 use serde::{Deserialize, Serialize};
 
 // use crate::commands::{calculate_mapping, execute_pipeline, extrude, flip, palette, remap_image, rescale, rotate, write_palette, FlipDirection, RotateDegree, Rotate};
-use crate::commands::{Extrude, Flip, Palette, Pipeline, Reduce, Remap, Rotate, Scale};
+use crate::commands::{Extrude, Flip, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split};
 
 use crate::load_image;
 
@@ -39,6 +39,9 @@ pub enum Args {
 	#[clap(name = "reduce")]
 	#[serde(alias = "reduce")]
 	Reduce(Reduce),
+	#[clap(name = "split")]
+	#[serde(alias = "split")]
+	Split(Split),
 }
 
 impl Args {
@@ -97,6 +100,10 @@ impl Args {
 
 				Ok(())
 			}
+			Args::Split(split) => {
+				let image = load_image(&split.input, None)?;
+				split.run(&image)
+			}
 			Args::Pipeline(pipeline) => pipeline.execute(),
 		}
 	}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index b9cf99709e2b19afb44a6198c3793d44e51acaf5..c2538e25d4e4d57d742b0db79c879eb96989837b 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -2,16 +2,18 @@ mod extrude;
 mod flip;
 mod palette;
 mod pipeline;
+mod reduce;
 mod remap;
 mod rotate;
 mod scale;
-mod reduce;
+mod split;
 
 pub use extrude::Extrude;
 pub use flip::Flip;
 pub use palette::Palette;
 pub use pipeline::Pipeline;
+pub use reduce::Reduce;
 pub use remap::Remap;
 pub use rotate::Rotate;
 pub use scale::Scale;
-pub use reduce::Reduce;
+pub use split::Split;
diff --git a/src/commands/split.rs b/src/commands/split.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0a688a21c08ae6e0427161227d6a10813938e6e7
--- /dev/null
+++ b/src/commands/split.rs
@@ -0,0 +1,184 @@
+use crate::utils::{RgbaOutputFormat, SpriteData};
+use anyhow::Error;
+use clap::Parser;
+use image::imageops::tile;
+use image::{
+	save_buffer, save_buffer_with_format, GenericImage, GenericImageView, ImageBuffer, ImageFormat,
+	Pixel, Rgba, RgbaImage,
+};
+use num_traits::cast::ToPrimitive;
+use num_traits::AsPrimitive;
+use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator};
+use serde::{Deserialize, Serialize};
+use std::io::Read;
+use std::path::PathBuf;
+
+#[inline(always)]
+fn tile_size() -> u32 {
+	32
+}
+
+/// Take a spritesheet and split into individual sprites, skipping empty space
+#[derive(Parser, Serialize, Deserialize, Clone, Debug)]
+#[clap(author, version = "0.5.0")]
+pub struct Split {
+	/// The path to the spritesheet file
+	#[serde(default)]
+	pub input: String,
+	/// The base path for all of the created sprites. Should be a directory, which will be created if it does not exist
+	#[serde(default)]
+	pub output: String,
+
+	/// The amount of horizontal space between each sprite in the image
+	#[clap(short = 'x', long, default_value_t = 0)]
+	#[serde(default)]
+	space_x: u32,
+	/// The amount of vertical space between each sprite in the image
+	#[clap(short = 'y', long, default_value_t = 0)]
+	#[serde(default)]
+	space_y: u32,
+	/// The amount of space around the edge of the image
+	#[clap(short, long, default_value_t = 0)]
+	#[serde(default)]
+	padding: u32,
+	/// The size of each tile in the spritesheet. Assumed to be square tiles
+	#[clap(short, long, default_value_t = 32)]
+	#[serde(default = "tile_size")]
+	tile_size: u32,
+	/// When set, files will be numbered consecutively, regardless of empty sprites. By default, files
+	/// will be numbered based on their inferred "tile id", based on position in the sheet
+	#[clap(short, long, default_value_t = false)]
+	#[serde(default)]
+	sequential: bool,
+}
+
+fn params_to_columns(
+	dimension: impl AsPrimitive<f32>,
+	tile_size: impl AsPrimitive<f32>,
+	padding: impl AsPrimitive<f32>,
+	spacing: impl AsPrimitive<f32>,
+) -> u32 {
+	let dimension = dimension.as_();
+	let tile_size = tile_size.as_();
+	let padding = padding.as_();
+	let spacing = spacing.as_();
+
+	let base = (dimension - (2.0 * padding) - spacing) / (tile_size + spacing);
+	base.floor() as u32 + if spacing == 0.0 { 0 } else { 1 }
+}
+
+impl Split {
+	pub fn run(&self, image: &RgbaImage) -> anyhow::Result<()> {
+		log::info!(
+			"Image loaded with size {} x {}",
+			image.width(),
+			image.height()
+		);
+
+		let columns = params_to_columns(image.width(), self.tile_size, self.padding, self.space_x);
+		let rows = params_to_columns(image.height(), self.tile_size, self.padding, self.space_y);
+		log::info!("Inferred sheet contains {} columns", columns);
+		log::info!("Inferred sheet contains {} rows", rows);
+
+		std::fs::create_dir_all(&self.output)?;
+		let path_base = PathBuf::from(&self.output);
+
+		let mut file_count = 0;
+		for x in 0..columns {
+			for y in 0..rows {
+				let img_x = self.padding + (x * self.tile_size) + (x * self.space_x);
+				let img_y = self.padding + (y * self.tile_size) + (y * self.space_y);
+				// let img_y = y * self.tile_size;
+
+				let view = image.view(img_x, img_y, self.tile_size, self.tile_size);
+
+				if view
+					.pixels()
+					.any(|(_, _, pixel)| pixel.channels().get(3).map(|c| c > &0).unwrap_or(false))
+				{
+					view.to_image().save_with_format(
+						path_base.join(format!("{}.png", file_count)),
+						ImageFormat::Png,
+					)?;
+
+					if self.sequential {
+						file_count += 1;
+					}
+				}
+
+				if !self.sequential {
+					file_count += 1;
+				}
+			}
+		}
+
+		Ok(())
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	#[test]
+	fn no_pad_space() {
+		let columns = super::params_to_columns(160, 16, 0, 0);
+		let rows = super::params_to_columns(160, 16, 0, 0);
+
+		assert_eq!(columns, 10);
+		assert_eq!(rows, 10);
+
+		let columns = super::params_to_columns(512, 16, 0, 0);
+		let rows = super::params_to_columns(160, 16, 0, 0);
+
+		assert_eq!(columns, 32);
+		assert_eq!(rows, 10);
+	}
+
+	#[test]
+	fn some_pad() {
+		let columns = super::params_to_columns(168, 16, 4, 0);
+		let rows = super::params_to_columns(168, 16, 4, 0);
+
+		assert_eq!(columns, 10);
+		assert_eq!(rows, 10);
+
+		let columns = super::params_to_columns(520, 16, 4, 0);
+		let rows = super::params_to_columns(168, 16, 4, 0);
+
+		assert_eq!(columns, 32);
+		assert_eq!(rows, 10);
+	}
+	#[test]
+	fn some_space() {
+		let columns = super::params_to_columns(168, 16, 0, 0);
+		let rows = super::params_to_columns(168, 16, 0, 0);
+
+		assert_eq!(columns, 10);
+		assert_eq!(rows, 10);
+
+		let columns = super::params_to_columns(520, 16, 0, 0);
+		let rows = super::params_to_columns(168, 16, 0, 0);
+
+		assert_eq!(columns, 32);
+		assert_eq!(rows, 10);
+	}
+	#[test]
+	fn different_spacing() {
+		let space_x = 3;
+		let space_y = 2;
+
+		let columns = super::params_to_columns(189, 13, 0, space_x);
+		let rows = super::params_to_columns(159, 13, 0, space_y);
+
+		assert_eq!(columns, 12);
+		assert_eq!(rows, 9);
+	}
+
+	#[test]
+	fn correct_conversion() {
+		let columns = super::params_to_columns(434, 32, 3, 1);
+		let rows = super::params_to_columns(302, 32, 6, 1);
+
+		assert_eq!(columns, 13);
+		assert_eq!(rows, 9);
+	}
+}