Skip to content

Commit cbf2c72

Browse files
committed
Add Exercise 5.3.1: Lettuce Crop WebAssembly
1 parent bf807c0 commit cbf2c72

File tree

9 files changed

+1254
-0
lines changed

9 files changed

+1254
-0
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"exercises/4-multitasking/3-asynchronous-multitasking/1-async-channels/Cargo.toml",
2525
"exercises/4-multitasking/3-asynchronous-multitasking/2-async-chat/Cargo.toml",
2626
"exercises/5-rust-for-web/1-rust-for-web-servers/1-lettuce-crop/Cargo.toml",
27+
"exercises/5-rust-for-web/3-rust-in-the-browser/1-lettuce-crop-wasm/Cargo.toml",
2728
"exercises/6-rust-for-systems-programming/1-foreign-function-interface/1-crc-in-c/Cargo.toml",
2829
"exercises/6-rust-for-systems-programming/1-foreign-function-interface/2-crc-in-rust/Cargo.toml",
2930
"exercises/6-rust-for-systems-programming/1-foreign-function-interface/3-qoi-bindgen/Cargo.toml",

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- [Rust for Web]()
2323
- [Rust for Web Servers](rust-for-web-servers.md)
2424
- [Rust in the Cloud](rust-in-the-cloud.md)
25+
- [Rust in the Browser](rust-in-the-browser.md)
2526

2627
- [Rust for Systems Programming]()
2728
- [Foreign Function Interface](foreign-function-interface.md)

book/src/rust-in-the-browser.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Unit 5.3 - Rust in the Browser
2+
3+
## Exercise 5.3.1: Lettuce Crop WebAssembly
4+
In exercise 5.1.1, we build a web server that hosts an image cropping service. But do we really need to do this cropping on our server? Wouldn't it be much more privacy-friendly if we could do the image cropping in the user's browser instead of uploading images to our external server?
5+
6+
In this exercise, we will create a new version of our Lettuce Crop website that crops images with [WebAssembly](https://webassembly.org/). WebAssembly allows you to run compiled code in a safe sandboxed environment in the browser. This means we will not need a dedicated server anymore, as the website will only consist of static files which we can be hosted using any HTTP server. You could even host it for free using [GitHub pages](https://pages.github.com/)!
7+
8+
### 3.2.1.A Building with Wasm Pack
9+
In `exercises/5-rust-for-web/3-rust-in-the-browser/1-lettuce-crop-wasm` we have set up a basic WebAssembly project. As you can see in the `Cargo.toml`, the project has been configured as a dynamic library (`"cdylib"`). We've also added the `wasm-bindgen` crate as a dependency, which is used to generate WebAssembly bindings.
10+
11+
To build the project, we will use `wasm-pack`. First, install `wasm-pack` with:
12+
```
13+
cargo install wasm-pack
14+
```
15+
Then, build the project with `wasm-pack`. Since we want to use it in the browser, we set the [wasm-pack target](https://rustwasm.github.io/docs/wasm-pack/commands/build.html#target) to `web`, and we tell it to put the generate files in the `assets/pkg` folder:
16+
```
17+
wasm-pack build --target web --out-dir assets/pkg
18+
```
19+
20+
Now, a bunch of files should appear in the `assets/pkg` folder:
21+
- A `.wasm` file, which contains the compiled WebAssembly code
22+
- Some `.d.ts` files, which describe the TypeScript types of the generated bindings
23+
- A `.js` files, which contains the JavaScript bindings for our WebAssembly binary
24+
25+
### 3.2.1.B Interacting with JavaScript
26+
So what functionality does the compiled WebAssembly currently include? In `lib.rs` you can see two functions: an extern `alert()` function, and a `hello()` function. Both of these functions have been annotated with `#[wasm_bindgen]` to indicate that we want to bind them with WebAssembly. Extern functions will be bound to existing JavaScript methods, in this case the [window's `alert()` function](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) which shows a popup dialog.
27+
28+
Let's add the WebAssembly to our website. Add the following JavaScript in the `<body>` of the `index.html` to load the WebAssembly binary and call our `hello()` function when we press the submit button:
29+
```html
30+
<script type="module">
31+
import init, { hello } from "./pkg/lettuce_crop_wasm.js";
32+
init().then(() => {
33+
const submit_button = document.querySelector('input[type="submit"]');
34+
submit_button.onclick = () => {
35+
hello("WebAssembly");
36+
}
37+
});
38+
</script>
39+
```
40+
41+
To try out the website, you can use any HTTP server that is able to serve local files. You could use `axum` to host the files like we did in exercise 5.1.1, but you can also use for example `npx http-server` if you have `npm` installed.
42+
43+
### 3.2.1.C Cropping images
44+
Let's add a `crop_image(bytes: Vec<u8>, max_size: u32) -> Vec<u8>` function to our Rust library that will crop our images. You can use the same logic as in exercise 5.1.1 (part D and E) to create a `DynamicImage` from the input bytes, crop it, and export it as WebP. Mark the function with `#[wasm_bindgen]` and rebuild the library to generate WebAssembly bindings for it.
45+
46+
If you look at the generated JavaScript bindings, you will see that the `Vec<u8>`s for the `crop_image` function have been turned into `Uint8Array`s. We will need to write some JavaScript to read the user's selected image and give it to our `crop_image` as a `Uint8Array`.
47+
48+
First, let's grab our other two input elements:
49+
```js
50+
const max_size = document.querySelector('input[name="max_size"]');
51+
const image = document.querySelector('input[name="image"]');
52+
```
53+
Then, in the `onclick` of the submit button, you can grab the selected file using `image.files[0]`. To get the contents of the file, we will use a [`FileReader`](https://developer.mozilla.org/en-US/docs/Web/API/FileReader):
54+
```js
55+
const file = image.files[0];
56+
const reader = new FileReader();
57+
reader.onload = (evt) => {
58+
const bytes = new Uint8Array(evt.target.result);
59+
const cropped_bytes = crop_image(bytes, max_size.value); // call our function
60+
// TODO: do something with the cropped_bytes
61+
};
62+
reader.readAsArrayBuffer(file);
63+
```
64+
Finally, to display the resulting cropped image to the user, we will construct a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) from the `Uint8Array`, and turn this `Blob` into a URL to which we will redirect the user:
65+
```js
66+
window.location.href = URL.createObjectURL(new Blob([cropped_bytes]));
67+
```
68+
If you select an invalid file, you will get an error in the browser console. Feel free to add some better error handling by using a [try-catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch), and by validating whether `image.files[0]` exists before reading it. It would also be nice to verify that `max_size` has a sensible value.
69+
70+
### 3.2.1.D Using the web-sys crate (bonus)
71+
Instead of using JavaScript to interact with the HTML document or manually binding extern JavaScript functions using `#[wasm_bindgen]` like we saw with `alert()`, we can also use the [`web-sys`](https://crates.io/crates/web-sys) crate. This crate provides bindings for the JavaScript web APIs available in the browser. However, most of these APIs have to be manually enabled with individual features.
72+
73+
Add the `web-sys` crate to your project with all the needed features enabled:
74+
```
75+
cargo add web-sys --features "Window,Document,HtmlElement,HtmlImageElement,Blob,Url"
76+
```
77+
78+
Now, instead having the `crop_image` function return an array of bytes, let's have it instead append an image to HTML document:
79+
- First, get the HTML body element:
80+
```rust
81+
let window = web_sys::window().unwrap();
82+
let document = window.document().unwrap();
83+
let body = document.body().unwrap();
84+
```
85+
- Then, we can create an HTML image element:
86+
```rust
87+
let img = document.create_element("img").unwrap();
88+
let img: web_sys::HtmlImageElement = img.dyn_into().unwrap();
89+
```
90+
- To set the source of the image, we will again need to create a `Blob` to get a temporary data URL. For this, we first create a JavaScript array:
91+
```rust
92+
let bytes = web_sys::js_sys::Array::new();
93+
bytes.push(&web_sys::js_sys::Uint8Array::from(&buffer[..]));
94+
```
95+
- And then we can create a Blob and create a URL:
96+
```rust
97+
let blob = web_sys::Blob::new_with_u8_array_sequence(&bytes).unwrap();
98+
let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap();
99+
```
100+
- And finally, we can set the image's source and append the image to the document's body:
101+
```rust
102+
img.set_src(&url);
103+
body.append_child(&img).unwrap();
104+
```
105+
- Remember to also update the JavaScript code in the HTML document accordingly.

0 commit comments

Comments
 (0)