image
crateThe safety and performance guarantees of Rust are extremely useful for developers doing all kinds of work, making it ideal for a lot of scenarios. However, Rust’s package ecosystem is extremely large and examples can be terse, oftentimes making Rust underutilized.
Rust specifically carries a lot of promise in its image manipulation abilities, but many people don’t use these features because of how daunting they seem. That’s why in this article, we’ll cover Rust’s image
crate and the various methods it gives us to decode, manipulate, and encode images.
While working with images, we want to operate on them as two-dimensional arrays. Ideally, we’d iterate over an image using a code API that looks like this:
for pixel in img.pixels() { // modify RBGA pixel }
Unfortunately, most image formats are written to optimize compression and performance rather than ease of use. The PPM image format is essentially a series of ASCII RGB values, but even then it has extra data, such as a header containing height and width, that we need to account for.
From there, it gets successively more complex with formats like PNG and JPEG that add layers of compression and lossy encoding. To make matters worse, we often deal with various types of image encoding in the same program.
Attempting to operate on images using Rust’s inbuilt functionality for binary files will quickly become a headache. Here’s where the image
crate comes to our rescue and gives us a very clean API to work with.
image
crateThe image
crate has DynamicImage
and ImageBuffer
enums that allow us to operate over images with an API, like in our example from the previous section.
ImageBuffer
represents a specific type of image that has essentially been converted to a 2D array, while DynamicImage
allows us to move between types of images and still provides some general image processing abilities.
To create either one of these, the crate gives us a few utilities for opening images and decoding them. Let’s test them out on our mascot, Ferris the crab.
image::open(path)
If we just want something quick and easy, we can use image::open(path)
. This is a top-level method in the crate that takes in an argument satisfying AsRef<Path>
and returns a Result
containing a DynamicImage
. Since String
and str
implement AsRef<Path>
, we can use it like this:
use image; use image::GenericImageView; // to allow calling .pixels() // ... let img = image::open("ferris.png").expect("File not found!"); for pixel in img.pixels() { // modify RGBA pixel }
The function will assume encoding from the file extension that we give it, and that’s good enough for most purposes.
image::io::Reader
The image
crate also allows us to have more fine-grained control over how we load images. For example, what if we are loading an image over a network connection or from the GPU and receive it as a buffer rather than dealing with a file on disk? Or, what if we don’t trust the file extension that the user has given us?
For more detailed cases like these, we can use image::io::Reader
. This method gives us control over the loading process.
For example, given a PPM image in memory, we can load it in as such:
use image::io::Reader; use std::io::Cursor; let data = Cursor::new(b"\ P3 3 2 255 255 0 0 0 255 0 0 0 255 255 255 0 255 255 255 0 0 0"); let reader = Reader::new(data) .with_guessed_format() .expect("This will never fail using Cursor"); let img = reader.decode().expect("Failed to read image");
Let’s step through this example piece by piece. At the very beginning, we create a Cursor
object to represent a binary stream of data directly from a string. In our case, this is just a simple test image, but in reality, this may come from a network socket, the GPU, or some other form of communication.
Once we’ve constructed a reader object, we need to figure out the image format of our stream.
with_guessed_format
The with_guessed_format
method tries to read from the stream and use the header to determine its data type, which, in our example, is P3 for the PPM format. Since our Cursor
will always be readable, this method will never fail and we can safely call expect
. If we give it some other type that implements Read
and fails, it will return an error type.
decode
So far, we have provided the reader with information about the image we want to read, such as what type of image it is. To get an image out of the reader, we need to call decode
, which returns a Result
containing our DynamicImage
from earlier. Decode can break in two ways:
We will trigger an error if we fail to determine the image format when calling with_guessed_format
. The format of the reader object will become an error type.
We can specifically check for this by either pattern matching the return of decode
against an ImageError::Unsupported
, or by checking that reader.format()
returns a Some
value before we call decode
.
We can also trigger an error if the image does not line up with the format we have specified. For example, the PPM file format indicates each pixel as chunks of three ASCII values representing RGB. If, for some reason, we only had two values per pixel, decode
would return an ImageError::DecodingError
.
Reader::open
If we wanted to take advantage of the more advanced formatting of Reader
but still open files as we were doing before, we can call Reader::open
. This method assumes the image format from the file extension, but we can call with_guessed_format()
and have it read the header to infer the actual image format.
image
crateNow we’ll discuss encoding images. The image
crate makes this simple, but there are a few caveats to be aware of. We’ll go over operating on images as well as saving them.
Modifying the images we load is as easy as reading and writing images, with a few caveats.
For a lot of common operations, like blurs or filters, the crate provides us with a set of inbuilt operations. A full list can be found here, but what if we wanted to modify an image ourselves?
We need to be careful about how we are operating on the image in memory. Although it is possible to modify the image as we iterate over it, we will more often than not run into issues with the borrow checker.
If we try writing to the image while iterating over it, we might end up modifying something we’ll iterate over later and open a whole can of worms. Instead, it’s generally simpler to make a copy of the image that we write to instead.
As an example, suppose that we want to iterate over our friendly ferris.png
image and make every pixel closer to black. The image
crate provides a utility to do this, but let’s write a simple version ourselves:
use image::{GenericImageView, ImageBuffer, Pixel}; fn main() { let img = image::open("ferris.png").expect("File not found!"); let (w, h) = img.dimensions(); let mut output = ImageBuffer::new(w, h); // create a new buffer for our output for (x, y, pixel) in img.pixels() { output.put_pixel(x, y, // pixel.map will iterate over the r, g, b, a values of the pixel pixel.map(|p| p.saturating_sub(65)) ); } // What do we do with output now? }
Now, Ferris will look like this:
In just a few lines of code, we can read in a PNG, iterate over its internal pixels, and create a new version of it. This basic structure should be everything we need to write any image processing algorithm in Rust.
Now that we have created a new image, how do we get it out of memory and send it somewhere? That’s where writing an image comes in.
Once we’ve loaded and modified an image or created a completely new image from scratch, we can use the image
crate to encode and save it.
Saving to a file is extremely easy. Both DynamicImage
and ImageBuffer
have a save(path)
method. They’ll infer the format that we wish to encode the image in using the file extension.
After making all of our desired modifications, we can do something like img.save("output.png")
and the crate will handle all of the hard work for us.
For most use cases, saving to a file is sufficient. Sometimes, however, we want to send the encoded image over a network connection or send an image to some hardware component. In cases like these, we’d prefer to write our encoded image instead.
For this use case, write_to
is our friend. write_to
is available both for DynamicImage
and any type of ImageBuffer
. It will take a writer object that implements Write + Seek
, as well as an ImageOutputFormat
object, which comes as part of the crate.
The image
crate is an extremely powerful tool that gives us a lot of fine-grained control over our images and how to manipulate them. It also provides tons of utilities and features that make image processing simple.
If you’ve found that the image
crate is slow to process your images, make sure that you are running your code in release mode using cargo run --release
.
A lot of the methods we’ve used will be running multiple loops over your images, and Rust maintains a lot of extra unnecessary debugging information when it’s run without the release flag. If you’re creating a tool and want to test it with debugging information available, I would recommend working with smaller images.
We’ve covered a lot of information about decoding and encoding using the image
crate! You should now have the understanding needed to go forward and work with images in Rust.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.