diff --git a/Cargo.lock b/Cargo.lock
index df714a9ca3bba969392086af86662b36ab358804..d6e742141295881728784c923e70ab6d3126372f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "adler"
@@ -233,7 +233,7 @@ dependencies = [
 
 [[package]]
 name = "crunch-cli"
-version = "0.8.2"
+version = "0.9.0"
 dependencies = [
  "anyhow",
  "clap",
diff --git a/Cargo.toml b/Cargo.toml
index 2eabda7129cb693edfe94e9db7caa9d3f617efc8..a23223dc4e29026187d60e296f067cc62c44c873 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "crunch-cli"
-version = "0.8.2"
+version = "0.9.0"
 edition = "2021"
 
 homepage = "https://microhacks.lcr.app/crunch/"
diff --git a/src/cli_args.rs b/src/cli_args.rs
index ba1cc538e3375af0b03ce475a9eac7ac22dce4c4..8e8090dbd8970405105dcb0aa3dcd3a807743aaa 100644
--- a/src/cli_args.rs
+++ b/src/cli_args.rs
@@ -2,9 +2,7 @@ use clap::Parser;
 use image::ImageFormat;
 use serde::{Deserialize, Serialize};
 
-use crate::commands::{
-	Atlas, Extract, Extrude, Flip, Info, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split,
-};
+use crate::commands::{Atlas, Extract, Extrude, Flip, Info, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split, Swap};
 
 use crate::load_image;
 
@@ -53,6 +51,9 @@ pub enum Args {
 	#[clap(name = "info")]
 	#[serde(alias = "info")]
 	Info(Info),
+	#[clap(name = "info")]
+	#[serde(alias = "info")]
+	Swap(Swap),
 }
 
 impl Args {
@@ -104,6 +105,15 @@ impl Args {
 					.save_with_format(&remap.output, ImageFormat::Png)
 					.map_err(anyhow::Error::from)
 			}
+			Args::Swap(swap) => {
+				let image_data = load_image(&swap.input, None)?;
+				let palette_data = load_image(&swap.palette, None)?;
+				let palette_swap = Palette::from_columns(&palette_data)?;
+				let output = Remap::remap_image(image_data, palette_swap)?;
+				output
+					.save_with_format(&swap.output, ImageFormat::Png)
+					.map_err(anyhow::Error::from)
+			},
 			Args::Reduce(reduce) => {
 				if let Some(amount) = reduce.colours {
 					log::info!("Num cols {}", amount);
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index a3c215c687b8ede2018ceb59bf620ed70e682967..7a2d578e2c22fd1fbd6cbfc2fc5bf84a3a819168 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -10,6 +10,7 @@ mod remap;
 mod rotate;
 mod scale;
 mod split;
+mod swap;
 
 pub use atlas::Atlas;
 pub use extract::Extract;
@@ -23,3 +24,4 @@ pub use remap::Remap;
 pub use rotate::Rotate;
 pub use scale::Scale;
 pub use split::Split;
+pub use swap::Swap;
\ No newline at end of file
diff --git a/src/commands/palette.rs b/src/commands/palette.rs
index bc6040f21eaeeefa95ea37eb1ce115d69d7458e4..2778712b38c7587968175a77b20e3357637efa66 100644
--- a/src/commands/palette.rs
+++ b/src/commands/palette.rs
@@ -4,7 +4,7 @@ use std::collections::hash_map::RandomState;
 use std::collections::{HashMap, HashSet};
 
 use std::io::Write;
-
+use anyhow::Error;
 use deltae::{Delta, LabValue, DE2000};
 use image::{GenericImage, Pixel, Rgba};
 use num_traits::ToPrimitive;
@@ -110,6 +110,37 @@ impl Palette {
 		Ok(())
 	}
 
+	pub fn from_columns(image: &impl GenericImage) -> anyhow::Result<ColourMapping> {
+		if image.width() != 2 {
+			return Err(Error::msg("Image must have a pixel width of 2"))?;
+		}
+
+		let mut mapping = ColourMapping::with_capacity(image.height() as usize);
+		for y in 0..image.height() {
+			let left = image.get_pixel(0, y);
+			let right = image.get_pixel(1, y);
+			let left = left.to_rgba();
+			let right = right.to_rgba();
+			let data = Rgba::from([
+				left.0[0].to_u8().unwrap(),
+				left.0[1].to_u8().unwrap(),
+				left.0[2].to_u8().unwrap(),
+				left.0[3].to_u8().unwrap(),
+			]);
+			let left_pixel = BasicRgba::from(data);
+			let data = Rgba::from([
+				left.0[0].to_u8().unwrap(),
+				left.0[1].to_u8().unwrap(),
+				left.0[2].to_u8().unwrap(),
+				left.0[3].to_u8().unwrap(),
+			]);
+			let right_pixel = BasicRgba::from(data);
+			mapping.insert(left_pixel, right_pixel);
+		}
+
+		Ok(mapping)
+	}
+
 	pub fn calculate_mapping(from: &PixelPalette, to: &PixelPalette) -> ColourMapping {
 		let colour_labs = Vec::from_iter(to.iter().map(LabValue::from));
 
diff --git a/src/commands/swap.rs b/src/commands/swap.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c8e772e1e99ad05e4d71be268a764c7969a0d1f4
--- /dev/null
+++ b/src/commands/swap.rs
@@ -0,0 +1,62 @@
+use crate::commands::palette::ColourMapping;
+use clap::Parser;
+use image::{GenericImage, Pixel, Rgba};
+use num_traits::ToPrimitive;
+use serde::{Deserialize, Serialize};
+
+use crate::utils::{new_image, BasicRgba, OutputFormat};
+
+/// Use an existing colour mapping file to swap colours in the input sprite
+#[derive(Debug, Clone, Parser, Serialize, Deserialize)]
+#[clap(author, version = "0.9.0")]
+pub struct Swap {
+	/// The path to the image file
+	#[serde(default)]
+	pub input: String,
+	/// The path to write the recoloured image
+	#[serde(default)]
+	pub output: String,
+
+	/// The path to the palette file containing the output colours
+	#[clap(short, long)]
+	pub palette: String,
+}
+
+impl Swap {
+	pub fn swap_pixels(
+		image: impl GenericImage,
+		mappings: ColourMapping,
+	) -> anyhow::Result<OutputFormat> {
+		let mut output = new_image(image.width(), image.height());
+		for (x, y, pixel) in image.pixels() {
+			let pixel = pixel.to_rgba();
+
+			if pixel.0[3].to_u8().unwrap() == 0 {
+				output.put_pixel(x, y, Rgba::from(BasicRgba::transparent()));
+				continue;
+			}
+
+			let data = Rgba::from([
+				pixel.0[0].to_u8().unwrap(),
+				pixel.0[1].to_u8().unwrap(),
+				pixel.0[2].to_u8().unwrap(),
+				pixel.0[3].to_u8().unwrap(),
+			]);
+			let basic = BasicRgba::from(data);
+
+			match mappings.get(&basic) {
+				Some(mapped_data) => output.put_pixel(
+					x,
+					y,
+					Rgba::from(BasicRgba {
+						a: basic.a,
+						..*mapped_data
+					}),
+				),
+				None => output.put_pixel(x, y, data),
+			};
+		}
+
+		Ok(output)
+	}
+}