Converting png/jpeg image to WebP

Hello

I want a function to convert PNG and JPEG images to WebP. This by simply giving the path of the image to function as parameter.

To achieve this I am using the Image and WebP crates.
The WebP-crate is poorly documented.

My code:

// Module to convert images to webp

use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat};     // Using image crate: https://github.com/image-rs/image
use webp::{Encoder, PixelLayout, WebPImage, WebPMemory};                    // Using webp crate: https://github.com/jaredforth/webp

/*
    Function which converts an image in PNG or JPEG format to WEBP.
    :param file_path: &String with the path to the image to convert.
    :return Option<String>: Return the path of the WEBP-image as String when succesfull, returns None if function fails.
*/
pub async fn image_to_webp(file_path: &String) -> Option<String> {
    // Open path as DynamicImage
    let image: DynamicImage = ImageReader::open(file_path).unwrap().decode().unwrap();

    // Guess image format (JPEG or PNG)
    let image_format: Option<ImageFormat> = match image::guess_format(image.as_bytes()).unwrap() {
        ImageFormat::Png => Some(ImageFormat::Png),
        ImageFormat::Jpeg => Some(ImageFormat::Jpeg),
        _ => None,
    };

    // Make webp::Encoder from DynamicImage
    let encoder: Encoder = Encoder::from_image(&image).unwrap();

    // Encode image into WebPMemory
    let encoded_webp: WebPMemory = encoder.encode(65f32);

    // Set pixellayout
    let pixel_layout: PixelLayout = match image_format {
        Some(ImageFormat::Png) => PixelLayout::Rgba,
        Some(ImageFormat::Jpeg) => PixelLayout::Rgb,
        None => {
            return None;
        }
        _ => {
            return None;
        }
    };

    // WebPMemory to WebpImage
    let webp_image: WebPImage =
        WebPImage::new(encoded_webp, pixel_layout, image.width(), image.height());

    unimplemented!()
}

Error:

associated function `new` is private private associated function	(42:44)

I am able to encode my image into webp::WebPMemory, see the variable encoded_webp. The problem is that webp::WebPMemory does not contain a function to save the file or to convert to image::DynamicImage... image::DynamicImage does contain a function like pub fn save<Q>(&self, path: Q) -> ImageResult<()> which would allow me to save the image to a path.

So I want to convert encoded_webp of type webp::WebPMemory to a variable with type webp::WebPImage. Once I have converted it to webp::WebPImage I can convert it to image::DynamicImage and save the file.

The problem is that I have no clue on how to convert webp::WebPMemory to webp::WebPImage! The documentation does not explain this at all!!

I took a look at the source code of the WebP-crate and saw that webp::WebPImage has new() function taking a webp::WebPMemory as parameter, but that new() function is private...

So how would I continue?

Never mind. Think I got it:

// Module to convert images to webp

use image::io::Reader as ImageReader;
use image::{DynamicImage, EncodableLayout, GenericImage, GenericImageView, ImageFormat}; // Using image crate: https://github.com/image-rs/image
use webp::{Encoder, PixelLayout, WebPImage, WebPMemory}; // Using webp crate: https://github.com/jaredforth/webp

use std::fs::File;
use std::io::Write;

/*
    Function which converts an image in PNG or JPEG format to WEBP.
    :param file_path: &String with the path to the image to convert.
    :return Option<String>: Return the path of the WEBP-image as String when succesfull, returns None if function fails.
*/
pub async fn image_to_webp(file_path: &String) -> Option<String> {
    // Open path as DynamicImage
    let image: DynamicImage = ImageReader::open(file_path).unwrap().decode().unwrap();

    // Guess image format (JPEG or PNG)
    let image_format: Option<ImageFormat> = match image::guess_format(image.as_bytes()).unwrap() {
        ImageFormat::Png => Some(ImageFormat::Png),
        ImageFormat::Jpeg => Some(ImageFormat::Jpeg),
        _ => None,
    };

    // Make webp::Encoder from DynamicImage
    let encoder: Encoder = Encoder::from_image(&image).unwrap();

    // Encode image into WebPMemory
    let encoded_webp: WebPMemory = encoder.encode(65f32);

    // Make File-stream for WebP-result and write bytes into it, and save to path "output.webp"
    let mut webp_image = File::create("output.webp").unwrap();
    webp_image.write_all(encoded_webp.as_bytes()).unwrap();

    unimplemented!()
}

Issues in the Git-repository of WebP contained an example.

Fully working module to convert PNG/JPEG to WebP!

Because there is not a lot of documentation about this problem I decided to share the module I am using to solve this. This will save some time for the next sucker Googling this. The function image_to_webp(file_path: &String) -> Option<String> allows you to give the path of a png or jpeg image as parameter, the function will make a subdirectory in the parent-directory of the file you are compressing to put the final WebP-image. So /some/folder/path/test.png will result in /some/folder/path/webp/test.webp.

I am using this module in a web-project so I can automatically convert images uploaded by users to WebP. This way I can also serve the WebP-version of 'non-static' images. Resulting in drastically increasing loading speed and efficiency.

How to use.

// Cargo.toml
[dependencies]
webp = "0.2"
image = "0.23.14"
// main.rs
mod webp;

fn main() {
    let file_name: String = "./some/folder/path/example.png".to_string();
    webp::image_to_webp(&file_name).await;

    // Returns an Option<String>. None on failure, or path of the final WebP-image as String on success. Use match to handle errors.
    // https://doc.rust-lang.org/std/option/
    //
    // New WebP-image: "./some/folder/path/webp/example.webp"
}

Things to consider.

  • I'm not fluent yet in Rust. Don't blindly trust this code.
  • My code needs to be asynchronous. If yours not remove .await and async keywords.
  • Module is well documented. You can improve and edit it. Add error-handling. Or you could make an extra parameter to set the quality of the WebP compression. Now it's standard 65f32.
  • Check that you have the correct chmod access to the file/folders you are trying to manipulate if the code fails.

Module (webp.rs)

// Module to convert images to webp

use image::io::Reader as ImageReader;
use image::{DynamicImage, EncodableLayout}; // Using image crate: https://github.com/image-rs/image
use webp::{Encoder, WebPMemory}; // Using webp crate: https://github.com/jaredforth/webp

use std::fs::File;
use std::io::Write;
use std::path::Path;

/*
    Function which converts an image in PNG or JPEG format to WEBP.
    :param file_path: &String with the path to the image to convert.
    :return Option<String>: Return the path of the WEBP-image as String when succesfull, returns None if function fails.
*/
pub async fn image_to_webp(file_path: &String) -> Option<String> {
    // Open path as DynamicImage
    //let image: DynamicImage = ImageReader::open(file_path).unwrap().decode().unwrap();
    let image = ImageReader::open(file_path);
    let image: DynamicImage = match image {
        Ok(img) => img.with_guessed_format().unwrap().decode().unwrap(), //ImageReader::with_guessed_format() function guesses if image needs to be opened in JPEG or PNG format.
        Err(e) => {
            println!("Error: {}", e);
            return None;
        }
    };

    // Make webp::Encoder from DynamicImage.
    let encoder: Encoder = Encoder::from_image(&image).unwrap();

    // Encode image into WebPMemory.
    let encoded_webp: WebPMemory = encoder.encode(65f32);

    // Put webp-image in a separate webp-folder in the location of the original image.
    let path: &Path = Path::new(file_path);
    let parent_directory: &Path = path.parent().unwrap();
    let webp_folder_path = format!("{}/webp", parent_directory.to_str().unwrap());
    match std::fs::create_dir_all(webp_folder_path.to_string()) {
        Ok(_) => {}
        Err(e) => {
            println!("Error {}", e);
            return None;
        }
    }

    // Get filename of original image.
    let filename_original_image = path.file_stem().unwrap().to_str().unwrap();

    // Make full output path for webp-image.
    let webp_image_path = format!(
        "{}/{}.webp",
        webp_folder_path.to_string(),
        filename_original_image
    );

    // Make File-stream for WebP-result and write bytes into it, and save to path "output.webp".
    let mut webp_image = File::create(webp_image_path.to_string()).unwrap();
    match webp_image.write_all(encoded_webp.as_bytes()) {
        Err(e) => {
            println!("Error: {}", e);
            return None;
        }
        Ok(_) => return Some(webp_image_path),
    }
}

Just copy this code in a file called webp.rs in your /src folder. That way it automatically becomes a module.

Good luck!

1 Like

Here's a version rewritten to return (opaque) errors and to use Path and PathBuf instead of Strings. Feel free to take what you like, no credit needed.

Some specific notes:

  • I didn't see any reason for this to be async
  • You almost always want to take a &str instead of a &String
    • But here you want a &Path
    • Or the generic version I used, which is less typing for users of the function, at the cost of more monomorphizing. Many libraries, including std, use this convention
  • Even if you're only ever going to unwrap returned errors, you can see how much the ability to use ? can clean up your code
    • You could always have a wrapper that unwraps if it's a bother
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.