Offline Image Converter allows you to convert between different image formats directly in the browser. It compiles the Rust image crate to WebAssembly and provides a frontend for converting between the supported image formats. Most of the functionality from the Offline PDF Merger was reused to allow for a quick development period of only 3 days. This post explores the interesting technical challenges of the project.

Image formats

The main purpose of the project is to convert between common types such as PNG, JPEG, WebP and AVIF, but it also exposes most of the other types implemented by the image crate. The internal storage of the image uses a DynamicImage that allows all the formats to be decoded and then encoded to another format. This made it quite easy to convert in Rust with 4 lines of code:

fn convert(image_data: Vec<u8>, output: ImageFormat) -> Result<Vec<u8>, String> {
	let img = image::load_from_memory(&image_data).map_err(map_image_err)?;
	let mut output_data: Vec<u8> = Vec::new();
	img.write_to(&mut Cursor::new(&mut output_data), output).map_err(map_image_err)?;
	Ok(output_data)
}

Both the PNG and JPEG formats allow for additional parameters, which affect the conversion between the types:

  • JPEG allows for a quality field between 0 and 100 that specifies how much of the original quality should be preserved, with the drawback that higher quality results in larger files.
  • PNG allows for setting a compression method and a filter type:
    • The compression method allows for selecting how much time should be spent on compression.
    • The filter type is a PNG standard in the specification that can be either: None, Sub, Up, Average, or Paeth. Each of these options links to a mathematical function that specifies how the image is prepared for compression.

This extended the method to allow for new encoder types:

fn convert(image_data: Vec<u8>, input: ImageFormat, output: ImageFormat, options: EncoderOptions) -> Result<Vec<u8>, String> {
	let img = image::load_from_memory_with_format(&image_data, input).map_err(map_image_err)?;
	let mut output_data: Vec<u8> = Vec::new();
	match options {
		EncoderOptions::Jpeg { quality } => {
			let mut encoder = JpegEncoder::new_with_quality(&mut output_data, quality);
			encoder.encode_image(&img).map_err(map_image_err)?;
		},
		EncoderOptions::Png { compression, filter } => {
			let encoder = PngEncoder::new_with_quality(&mut output_data, compression, filter);
			encoder.write_image(&img.as_bytes(), img.width(), img.height(), img.color().into()).map_err(map_image_err)?;
		},
		EncoderOptions::None => img.write_to(&mut Cursor::new(&mut output_data), output).map_err(map_image_err)?,
	};
    Ok(output_data)
}

Paste functionality

When I want to share pictures on this blog, most of the time they are screenshots from projects or games. In order to make it easier to share them in a more web-friendly format, I decided to add paste functionality to the site. When you use the snipping tool to capture a screenshot, you can simply paste it anywhere on the page and it will allow you to convert it to any type. This can be done without saving the screenshot to a file, allowing you to only save it as the desired format.

This was quite easy to do as it just required an event listener on the paste event:

document.addEventListener("paste", (event) => {
  const items = event.clipboardData.items;
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.startsWith("image/")) {
      const file = items[i].getAsFile();

      filePasteSection._file = file;
      pasted = true;

      showPasteSection();
    }
  }
});

This functionality was used to turn screenshots of the project in the following section from screenshotted and pasted PNGs to AVIF format. This reduced the size from 95-100 KB to 20-25 KB without any noticeable quality differences.

UI

The UI is mostly the same as the previous project, and it provides the same dark and light modes:

Offline Image Converter light-mode Offline Image Converter dark-mode

Additional thoughts about the UI:

  • A line was added between the input section and the header to allow for more differentiation.
  • The advanced options are not shown by default as most users would not need them.
  • The JPEG and PNG options are grouped in the select box at the top as they are expected to be the most used.
  • When a user selects either PNG or JPEG, the output file will automatically be shifted to the other format to allow for easy conversion.
  • The advanced section for JPEG and PNG exposes the optimization options. The filter type is especially not very user-friendly because the method names do not provide a clear explanation of what they do. In order to make it possible to decipher the options, a link to the specification was included.

Conclusion

The project was quite quick to set up because it shares most of the UI and WebAssembly setup with the Offline PDF Merger. It did produce a nice result, and especially the paste functionality will be something that provides quick value in the future for setting up this blog.

Go check it out at: