Rust Image Combiner Project
The image combiner is a command line tool built with the image crate. The tool weaves their pixels into one new picture.
Step 1: keep the three paths in a struct
Three file paths belong together. So give them one struct. A new function fills the struct from the command line:
#[derive(Debug)]
struct Args {
image_1: String,
image_2: String,
output: String,
}
impl Args {
fn new() -> Args {
Args {
image_1: nth_arg(1),
image_2: nth_arg(2),
output: nth_arg(3),
}
}
}Step 2: open an image and learn its format
ImageReader opens the file. format() reports PNG, JPEG and friends before you decode. And decode() loads the pixels:
fn open_image(path: &str) -> (DynamicImage, ImageFormat) {
let reader = image::ImageReader::open(path).expect("cannot open file");
let format = reader.format().expect("unknown image format");
let image = reader.decode().expect("cannot decode image");
(image, format)
}Step 3: refuse to mix formats
A custom error enum names the one failure main can return. Different formats stop the program with that error:
#[derive(Debug)]
enum CombineError {
DifferentFormats,
}
if format_1 != format_2 {
return Err(CombineError::DifferentFormats);
}Step 4: match the sizes first
Count the pixels of each picture. Resize the larger one down to the smaller one's dimensions. Then both byte lists end up the same length:
fn standardise_size(image_1: DynamicImage, image_2: DynamicImage) -> (DynamicImage, DynamicImage) {
let (w1, h1) = image_1.dimensions();
let (w2, h2) = image_2.dimensions();
if w1 * h1 <= w2 * h2 {
(image_1, image_2.resize_exact(w1, h1, image::imageops::FilterType::Triangle))
} else {
(image_1.resize_exact(w2, h2, image::imageops::FilterType::Triangle), image_2)
}
}Step 5: alternate the pixels
A pixel is four bytes. So walk both byte lists in chunks of four. Take the chunks in turn. First picture. Second picture. First. Second:
let pixels_1 = vec![255, 0, 0, 255, 255, 0, 0, 255];
let pixels_2 = vec![0, 0, 255, 255, 0, 0, 255, 255];
let mut combined = Vec::with_capacity(pixels_1.len());
let mut take_first = true;
for (chunk_1, chunk_2) in pixels_1.chunks_exact(4).zip(pixels_2.chunks_exact(4)) {
if take_first {
combined.extend_from_slice(chunk_1);
} else {
combined.extend_from_slice(chunk_2);
}
take_first = !take_first;
}
println!("{:?}", combined);Step 6: save the bytes as an image
save_buffer_with_format writes raw bytes back out as a picture file. It needs the path, the bytes, the size, the colour kind, and the format:
image::save_buffer_with_format(
&args.output,
&data,
width,
height,
image::ColorType::Rgba8,
format_1,
)Example
use image::{DynamicImage, GenericImageView, ImageFormat};
use std::env;
use std::process;
#[derive(Debug)]
struct Args {
image_1: String,
image_2: String,
output: String,
}
impl Args {
fn new() -> Args {
Args {
image_1: nth_arg(1),
image_2: nth_arg(2),
output: nth_arg(3),
}
}
}
fn nth_arg(n: usize) -> String {
env::args().nth(n).unwrap_or_else(|| {
eprintln!("usage: combiner <image1> <image2> <output>");
process::exit(2);
})
}
#[derive(Debug)]
enum CombineError {
DifferentFormats,
}
fn open_image(path: &str) -> (DynamicImage, ImageFormat) {
let reader = image::ImageReader::open(path).unwrap_or_else(|_| {
eprintln!("error: cannot open {}", path);
process::exit(2);
});
let format = reader.format().unwrap_or_else(|| {
eprintln!("error: unknown image format for {}", path);
process::exit(2);
});
let image = reader.decode().unwrap_or_else(|_| {
eprintln!("error: cannot decode {}", path);
process::exit(2);
});
(image, format)
}
fn main() -> Result<(), CombineError> {
let args = Args::new();
println!("{:?}", args);
let (image_1, format_1) = open_image(&args.image_1);
let (image_2, format_2) = open_image(&args.image_2);
if format_1 != format_2 {
return Err(CombineError::DifferentFormats);
}
let (w1, h1) = image_1.dimensions();
let (w2, h2) = image_2.dimensions();
println!("first image: {}x{}", w1, h1);
println!("second image: {}x{}", w2, h2);
let (image_1, image_2) = standardise_size(image_1, image_2);
let (width, height) = image_1.dimensions();
let data = alternate_pixels(image_1, image_2);
image::save_buffer_with_format(
&args.output,
&data,
width,
height,
image::ColorType::Rgba8,
format_1,
)
.unwrap_or_else(|_| {
eprintln!("error: cannot save {}", args.output);
process::exit(2);
});
println!("combined {}x{} image saved to {}", width, height, args.output);
Ok(())
}
fn standardise_size(
image_1: DynamicImage,
image_2: DynamicImage,
) -> (DynamicImage, DynamicImage) {
let (w1, h1) = image_1.dimensions();
let (w2, h2) = image_2.dimensions();
if w1 * h1 <= w2 * h2 {
(
image_1,
image_2.resize_exact(w1, h1, image::imageops::FilterType::Triangle),
)
} else {
(
image_1.resize_exact(w2, h2, image::imageops::FilterType::Triangle),
image_2,
)
}
}
fn alternate_pixels(image_1: DynamicImage, image_2: DynamicImage) -> Vec<u8> {
let pixels_1 = image_1.to_rgba8().into_vec();
let pixels_2 = image_2.to_rgba8().into_vec();
let mut combined = Vec::with_capacity(pixels_1.len());
let mut take_first = true;
for (chunk_1, chunk_2) in pixels_1.chunks_exact(4).zip(pixels_2.chunks_exact(4)) {
if take_first {
combined.extend_from_slice(chunk_1);
} else {
combined.extend_from_slice(chunk_2);
}
take_first = !take_first;
}
combined
}Key points
- Add the crate first: image = "0.25" under [dependencies] in Cargo.toml — Cargo fetches it the next time you build
- An image in memory is just numbers.
- Each pixel is four bytes.
- They are red, green, blue, and alpha.
- Alpha says how solid the pixel is.
- to_rgba8().into_vec() hands you all of them as one long Vec<u8>
- Two pictures can only be woven when they share a size and a format.
- So the tool checks the format first.
- Then it shrinks the bigger picture down to the smaller one's size.
- The program is split the way real tools are.
- An Args struct owns the three paths.
- Small functions each do one job.
- And main only calls them in order.
- In a full Cargo project, this Args struct would live in its own file, src/args.rs.
- A mod args; line at the top of main.rs would declare it.
- The image combiner builds one new picture by weaving pixels from two input pictures.
In this example, the tool is run as combiner sunset.png portrait.png merged.png. The Args debug line shows the three paths it picked up. sunset.png is 4x2 and portrait.png is 2x2, and both are PNG, so the format check passes. standardise_size shrinks sunset.png down to 2x2. alternate_pixels then weaves the two byte lists four bytes at a time. It takes one pixel from the first picture. Then one from the second. And save_buffer_with_format writes the woven bytes to merged.png. The verifier pass for this lesson opened merged.png back up. It is 2x2. Its four pixels alternate. They switch between the first image's colour and the second's. Exactly as planned. The Module Files lesson shows that split.
Note
The image crate does the hard parts — reading PNG and JPEG bytes, resizing smoothly.
Your code is the planning around it: which pixels, in which order, saved where.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why must the two images be resized to the same size before their pixels are alternated?