Final application

You should have this file tree layout:

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── app
│   ├── app.js
│   ├── image_filter.js       <-- generated file
│   ├── image_filter_bg.wasm  <-- generated file
│   └── index.html
└── src
    └── lib.rs

To recap your final Rust code should look something like this:

use std::io::Cursor;
use std::panic;
use wasm_bindgen::prelude::*;

use rustagram::image::io::Reader;
use rustagram::image::ImageOutputFormat;
use rustagram::RustagramFilter;

#[wasm_bindgen(start)]
pub fn main() {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(log::Level::Debug).unwrap();
}

#[wasm_bindgen]
pub fn apply_filter(img: &[u8], filter: &str) -> Vec<u8> {
    log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);

    let img = Reader::new(Cursor::new(img))
        .with_guessed_format()
        .unwrap()
        .decode()
        .unwrap();
    let filter_type = filter.parse().unwrap();
    let out = img.to_rgba8().apply_filter(filter_type);
    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), ImageOutputFormat::Png)
        .unwrap();

    bytes
}

The frontend in HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust Image filter</title>
  </head>
  <body>
    <input type="file" id="files" name="file" accept="image/png, image/jpeg" />
    <select name="filter">
      <option value="None">none</option>
      <option value="1977" selected>1977</option>
      <option value="Aden">Aden</option>
      <option value="Brannan">Brannan</option>
      <option value="Brooklyn">Brooklyn</option>
      <option value="Clarendon">Clarendon</option>
      <option value="Earlybird">Earlybird</option>
      <option value="Gingham">Gingham</option>
      <option value="Hudson">Hudson</option>
      <option value="Inkwell">Inkwell</option>
      <option value="Kelvin">Kelvin</option>
      <option value="Lark">Lark</option>
      <option value="Lofi">Lofi</option>
      <option value="Maven">Maven</option>
      <option value="Mayfair">Mayfair</option>
      <option value="Moon">Moon</option>
      <option value="Nashville">Nashville</option>
      <option value="Reyes">Reyes</option>
      <option value="Rise">Rise</option>
      <option value="Slumber">Slumber</option>
      <option value="Stinson">Stinson</option>
      <option value="Toaster">Toaster</option>
      <option value="Valencia">Valencia</option>
      <option value="Walden">Walden</option>
    </select>
    <span></span>
    <br>
    <img />
    <script type="module" src="app.js"></script>
  </body>
</html>

The JavaScript frontend code:

import init, { apply_filter } from './image_filter.js';

await init();

document.querySelector('input[type=file]').onchange = (evt) => {
  imageFilter();
};
document.querySelector('select').onchange = (evt) => {
  imageFilter();
};

function typedArrayToURL(typedArray) {
  return URL.createObjectURL(
    new Blob([typedArray.buffer], { type: "image/png" })
  );
}

async function imageFilter() {
  let files = document.getElementById('files').files;
  if (!files.length) {
    return;
  }
  let file = files[0];
  let span = document.querySelector('span');
  span.innerText = "working...";

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);
  imgEl.width = "500";

  let filter = document.querySelector("select").value.toLowerCase();
  if (filter == "none") {
    span.innerText = "done.";
    return;
  }

  let img = await file.arrayBuffer();
  let result = apply_filter(new Uint8Array(img), filter);
  let blobUrl = typedArrayToURL(result);
  imgEl.src = blobUrl;
  imgEl.width = "500";
  span.innerText = "done.";
}

A demo deployment is available at:

https://tmp.fnordig.de/wasm/image-filter/


Some ideas on what to do next:

  • The code unwraps a lot. Introduce some error handling. Can you return an error from your wasm module?