Creating a library
In this section of the tutorial, you will create a library to handle image manipulation. This library will have exactly one function. It should:
- Take a field of bytes representing an image
- Take a filter type
It should not:
- Handle any I/O
- Do input parsing (like filter name detection, etc.)
✅ In image-manipulation/Cargo.toml
, add:
[dependencies]
rustagram2 = "2"
log = "0.4"
We use the log
crate to get some visibility into what is happening.
✅ In image-manipulation/src/lib.rs
, add the following imports and function headers
use std::io::Cursor;
use rustagram::image::io::Reader;
use rustagram::image::ImageOutputFormat;
use rustagram::{RustagramFilter, FilterType};
pub fn apply_filter(img: &[u8], filter: FilterType) -> Vec<u8> {
log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);
}
This communicates three interesting things:
- The function is marked
pub
- this makes it a public function, callable from other libraries. The default in Rust is always private. - The data of the image is an immutable reference to binary data owned outside of the function
- The result is a binary vector, but passed out owned - so it cannot be the initial data
Reading the image
Looking at the signature of Reader, we find that it can only be construted using types that implement the Read
trait. &[u8]
does not implement the Read
trait. However, we can use the
standard libraries Cursor type
to fix that.
✅ Read the image by inserting the following code
let read_cursor = Cursor::new(img);
let img = Reader::new(cursor)
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
Now we got the image read and know it works.
✅ Apply the filter
let out = img.to_rgba8().apply_filter(filter);
On the output side, things are the same, but a little bit more complex.
We need to create the output vector. For writing to it, we can again create
a Cursor
around it, but this time, we'll do it by mutably borrowing the
Cursor
. We can then write to it like to any other IO type.
✅ Write to an output buffer
let mut bytes: Vec<u8> = Vec::new();
let mut write_cursor = Cursor::new(&mut bytes);
out.write_to(&mut write_cursor, ImageOutputFormat::Png)
.unwrap();
bytes
The last statement - a bare bytes
- uses Rusts expression-based nature to mark
bytes
as the variable to be returned. If you feel more comfortable, you may use
return bytes
.
The final code of the library should now read:
use std::io::Cursor;
use rustagram::image::io::Reader;
use rustagram::image::ImageOutputFormat;
use rustagram::{FilterType, RustagramFilter};
pub fn apply_filter(img: &[u8], filter: FilterType) -> Vec<u8> {
log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);
let cursor = Cursor::new(img);
let img = Reader::new(cursor)
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
let out = img.to_rgba8().apply_filter(filter);
let mut bytes: Vec<u8> = Vec::new();
let mut write_cursor = Cursor::new(&mut bytes);
out.write_to(&mut write_cursor, ImageOutputFormat::Png)
.unwrap();
bytes
}
Now that we have a library let's use it in our CLI
Some ideas on what to do next:
- Create further utility functions
- Extend the signature and return a
Result