diff --git a/src/actions/mod.rs b/src/actions/mod.rs
index 1b6859e49fa177307b324957e0b6b2b43766778f..fba26d873d02596c1f00cba5e57ff2802a4ca5b3 100644
--- a/src/actions/mod.rs
+++ b/src/actions/mod.rs
@@ -1,3 +1,5 @@
 mod colours;
+mod sheets;
 
 pub use colours::apply_colour_map;
+pub use sheets::ExtrudeOptions;
diff --git a/src/actions/sheets.rs b/src/actions/sheets.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a1b551e8eeb55af2de1aa5b942688de009cce0d3
--- /dev/null
+++ b/src/actions/sheets.rs
@@ -0,0 +1,120 @@
+use crate::utils::{RgbaOutputFormat, SpriteData};
+use image::math::Rect;
+use image::{DynamicImage, GenericImage, GenericImageView, Rgba, RgbaImage};
+use std::ops::Deref;
+
+#[derive(Default, Copy, Clone, Debug)]
+pub struct ExtrudeOptions {
+	/// The amount of horizontal space to add between each sprite in the image
+	pub space_x: u32,
+	/// The amount of vertical space to add between each sprite in the image
+	pub space_y: u32,
+	/// The amount of padding to add around the edge of the spritesheet
+	pub padding: u32,
+	/// The size of each tile in the spritesheet. Assumed to be square tiles
+	pub tile_size: u32,
+	/// Use nearby pixels for spacing
+	pub extrude: bool,
+}
+
+impl ExtrudeOptions {
+	pub fn extrude(&self, image: &DynamicImage) -> anyhow::Result<RgbaOutputFormat> {
+		log::info!(
+			"Image loaded with size {} x {}",
+			image.width(),
+			image.height()
+		);
+
+		let columns = image.width() / self.tile_size;
+		let rows = image.height() / self.tile_size;
+		log::info!("Inferred sheet contains {} columns", columns);
+		log::info!("Inferred sheet contains {} rows", rows);
+
+		let mut views = Vec::with_capacity((columns * rows) as usize);
+		for x in 0..columns {
+			for y in 0..rows {
+				let img_x = x * self.tile_size;
+				let img_y = y * self.tile_size;
+
+				let payload = SpriteData {
+					data: image.view(img_x, img_y, self.tile_size, self.tile_size),
+					x: img_x,
+					y: img_y,
+					tx: x,
+					ty: y,
+					width: self.tile_size,
+					height: self.tile_size,
+				};
+
+				views.push(payload);
+			}
+		}
+
+		let new_width = (self.padding * 2 + self.space_x * columns) + image.width();
+		let new_height = (self.padding * 2 + self.space_y * rows) + image.height();
+
+		log::info!(
+			"Using new image width {} / height {}",
+			new_width,
+			new_height
+		);
+		let mut new_image = RgbaImage::from_pixel(new_width, new_height, Rgba::from([0, 0, 0, 0]));
+		for sprite in views.iter() {
+			let (width, height) = sprite.data.dimensions();
+			let (img_x, img_y) = sprite.data.offsets();
+			let target_x = self.padding + img_x + (sprite.tx * self.space_x);
+			let target_y = self.padding + img_y + (sprite.ty * self.space_y);
+
+			new_image.copy_from(sprite.data.deref(), target_x, target_y)?;
+
+			if self.extrude {
+				// Left Side
+				new_image.copy_within(
+					Rect {
+						x: target_x,
+						y: target_y,
+						width: 1,
+						height,
+					},
+					target_x.saturating_sub(1),
+					target_y,
+				);
+				// Right Side
+				new_image.copy_within(
+					Rect {
+						x: target_x + width - 1,
+						y: target_y,
+						width: 1,
+						height,
+					},
+					target_x + width,
+					target_y,
+				);
+				// Top Side
+				new_image.copy_within(
+					Rect {
+						x: target_x,
+						y: target_y,
+						width,
+						height: 1,
+					},
+					target_x,
+					target_y.saturating_sub(1),
+				);
+				// Bottom Side
+				new_image.copy_within(
+					Rect {
+						x: target_x,
+						y: target_y + height - 1,
+						width,
+						height: 1,
+					},
+					target_x,
+					target_y + height,
+				);
+			}
+		}
+
+		Ok(new_image)
+	}
+}
diff --git a/src/commands/extrude.rs b/src/commands/extrude.rs
index 959e8cb7ec595fa3823d345682573cfd454843d5..15aa8dcde64a93f17740f70a05be13f5ada0278a 100644
--- a/src/commands/extrude.rs
+++ b/src/commands/extrude.rs
@@ -1,11 +1,10 @@
 use clap::Parser;
 
-use image::math::Rect;
-use image::{GenericImage, GenericImageView, Rgba, RgbaImage};
+use image::{ColorType, DynamicImage, GenericImage, Rgba};
 
-use crunch_cli::utils::{RgbaOutputFormat, SpriteData};
+use crunch_cli::actions::ExtrudeOptions;
+use crunch_cli::utils::RgbaOutputFormat;
 use serde::{Deserialize, Serialize};
-use std::ops::Deref;
 
 #[inline(always)]
 fn tile_size() -> u32 {
@@ -51,102 +50,16 @@ impl Extrude {
 		&self,
 		image: &impl GenericImage<Pixel = Rgba<u8>>,
 	) -> anyhow::Result<RgbaOutputFormat> {
-		log::info!(
-			"Image loaded with size {} x {}",
-			image.width(),
-			image.height()
-		);
+		let opts = ExtrudeOptions {
+			extrude: self.extrude,
+			space_x: self.space_x,
+			space_y: self.space_y,
+			padding: self.padding,
+			tile_size: self.tile_size,
+		};
 
-		let columns = image.width() / self.tile_size;
-		let rows = image.height() / self.tile_size;
-		log::info!("Inferred sheet contains {} columns", columns);
-		log::info!("Inferred sheet contains {} rows", rows);
-
-		let mut views = Vec::with_capacity((columns * rows) as usize);
-		for x in 0..columns {
-			for y in 0..rows {
-				let img_x = x * self.tile_size;
-				let img_y = y * self.tile_size;
-
-				let payload = SpriteData {
-					data: image.view(img_x, img_y, self.tile_size, self.tile_size),
-					x: img_x,
-					y: img_y,
-					tx: x,
-					ty: y,
-					width: self.tile_size,
-					height: self.tile_size,
-				};
-
-				views.push(payload);
-			}
-		}
-
-		let new_width = (self.padding * 2 + self.space_x * columns) + image.width();
-		let new_height = (self.padding * 2 + self.space_y * rows) + image.height();
-
-		log::info!(
-			"Using new image width {} / height {}",
-			new_width,
-			new_height
-		);
-		let mut new_image = RgbaImage::from_pixel(new_width, new_height, Rgba::from([0, 0, 0, 0]));
-		for sprite in views.iter() {
-			let (width, height) = sprite.data.dimensions();
-			let (img_x, img_y) = sprite.data.offsets();
-			let target_x = self.padding + img_x + (sprite.tx * self.space_x);
-			let target_y = self.padding + img_y + (sprite.ty * self.space_y);
-
-			new_image.copy_from(sprite.data.deref(), target_x, target_y)?;
-
-			if self.extrude {
-				// Left Side
-				new_image.copy_within(
-					Rect {
-						x: target_x,
-						y: target_y,
-						width: 1,
-						height,
-					},
-					target_x.saturating_sub(1),
-					target_y,
-				);
-				// Right Side
-				new_image.copy_within(
-					Rect {
-						x: target_x + width - 1,
-						y: target_y,
-						width: 1,
-						height,
-					},
-					target_x + width,
-					target_y,
-				);
-				// Top Side
-				new_image.copy_within(
-					Rect {
-						x: target_x,
-						y: target_y,
-						width,
-						height: 1,
-					},
-					target_x,
-					target_y.saturating_sub(1),
-				);
-				// Bottom Side
-				new_image.copy_within(
-					Rect {
-						x: target_x,
-						y: target_y + height - 1,
-						width,
-						height: 1,
-					},
-					target_x,
-					target_y + height,
-				);
-			}
-		}
-
-		Ok(new_image)
+		let mut inner_image = DynamicImage::new(image.width(), image.height(), ColorType::Rgba8);
+		inner_image.copy_from(image, 0, 0)?;
+		opts.extrude(&inner_image)
 	}
 }