Final application

You should have this file tree layout:

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── fastly.toml
└── src
    ├── app.js
    ├── index.html
    └── main.rs

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

use std::io::Cursor;

use fastly::http::{Method, StatusCode};
use fastly::{mime, Error, Request, Response};
use rustagram::image;
use rustagram::image::io::Reader;
use rustagram::RustagramFilter;

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    // Pattern match on the path...
    match (req.get_method(), req.get_path()) {
        // If request is to the `/` path...
        (&Method::GET, "/") => Ok(Response::from_status(StatusCode::OK)
            .with_content_type(mime::TEXT_HTML_UTF_8)
            .with_body(include_str!("index.html"))),
        (&Method::GET, "/app.js") => Ok(Response::from_status(StatusCode::OK)
            .with_content_type(mime::APPLICATION_JAVASCRIPT)
            .with_body(include_str!("app.js"))),

        (&Method::POST, "/image") => convert_image(req),

        // Catch all other requests and return a 404.
        _ => Ok(Response::from_status(StatusCode::NOT_FOUND)
            .with_body_text_plain("The page you requested could not be found\n")),
    }
}

pub fn convert_image(mut req: Request) -> Result<Response, Error> {
    let filter_str = req.get_query_parameter("filter").unwrap();
    let filter = filter_str.parse().unwrap();

    if !req.has_body() {
        return Ok(
            Response::from_status(StatusCode::BAD_REQUEST)
                .with_body_text_plain("missing image")
        );
    }

    let body = req.take_body();
    let body = body.into_bytes();

    let img = Reader::new(Cursor::new(body))
        .with_guessed_format()
        .unwrap();

    let img = img.decode().unwrap();

    let img = img.thumbnail(500, 500);
    let out = img.to_rgba8().apply_filter(filter);
    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Png)?;

    Ok(Response::from_status(StatusCode::OK)
        .with_body(bytes)
        .with_content_type(mime::IMAGE_PNG))
}

The frontend in HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust WASM Demo</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 src="app.js"></script>
  </body>
</html>

And the JavaScript frontend code:

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

async function postImage() {
  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 url = `/image?filter=${filter}`;
  let response = await fetch(url, {
    method: "POST",
    body: img,
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  let blob = await response.blob();
  imgEl.src = URL.createObjectURL(blob);
  span.innerText = "done.";
}

You can build and serve your application locally like this:

fastly compute serve

Some ideas on what to do next:

  • Did you even notice that this was compiled to WebAssembly?
  • What happens if you compile it natively?
  • Can you return different image formats? Different sizes?
  • What other task could be suitable for edge computing?