SandwichProblems
SandwichProblems

Reputation: 33

Updating HTML Canvas imagedata using Rust Webassembly

First post, so forgive me if something is not correct. I am trying to update the image data from a HTML canvas with a PNG file as part of an exercise. The function fetch_url_binary(url: String) returns the pixel information from the PNG.

#[wasm_bindgen] 
pub async fn fetch_url_binary(url: String) -> Result<Uint8Array, JsValue> 
{ 
let window = web_sys::window().unwrap(); // Browser window 
let promise = JsFuture::from(window.fetch_with_str(&url)); // File fetch promise 
let result = promise.await?; // Await fulfillment of fetch 
let response: web_sys::Response = result.dyn_into().unwrap(); // Type casting 
let image_data = JsFuture::from(response.array_buffer()?).await?; // Get text
Ok(Uint8Array::new(&image_data))
}

The function unred(url: String, canvas: String) sets the red channel to zero then updates the canvas with the image.

#[wasm_bindgen] 
pub async fn unred(url: String, canvas: String) -> Result<(), JsValue>
{
    let binary = fetch_url_binary(url).await.unwrap();
    let mut altbuf: Vec<u8> = Vec::new();
    for n in 0..binary.length() {
        if n % 4 == 0 {
            binary.set_index(n,0);
        }
        altbuf.push(binary.get_index(n));
    }
    let window = web_sys::window().unwrap();
    let document = window.document().expect("Could not get document");
    let canvas = document.get_element_by_id(&canvas).unwrap().dyn_into::<web_sys::HtmlCanvasElement>()?;
    let context = canvas.get_context("2d")?.unwrap().dyn_into::<web_sys::CanvasRenderingContext2d>()?;
    let image_data_temp = ImageData::new_with_u8_clamped_array(Clamped(&altbuf), altbuf.len().try_into().unwrap());
    context.put_image_data(&image_data_temp.unwrap(), 0.0, 0.0);
    Ok(()) 
}

The associated HTML code is here:

<!doctype html><html><body> 
  <canvas id="myCanvas" width="300" height="200" style="border:1px solid #d3d3d3;">
  <script type="module"> 
  import init, {unred, fetch_url_binary} from './pkg/hi_lib.js'; 
  var result;
  async function run() 
  { 
    await init(); // Initialize module 
    unred("myPng.png", "myCanvas");
  } 
  run(); // Execute async wrapper 
</script> 
</body></html>

I then test the code in a python3 http server where I get the following error:

       Uncaught (in promise) hi_lib_bg.wasm:0x5a79  
    RuntimeError: unreachable
    at hi_lib_bg.wasm:0x5a79
    at hi_lib_bg.wasm:0x663a
    at hi_lib_bg.wasm:0x71ba
    at hi_lib_bg.wasm:0x72c9
    at hi_lib_bg.wasm:0x673c
    at hi_lib_bg.wasm:0x20e7
    at hi_lib_bg.wasm:0x3fea
    at hi_lib_bg.wasm:0x7580
    at __wbg_adapter_16 (hi_lib.js:202)
    at real (hi_lib.js:187)
    $func69 @   hi_lib_bg.wasm:0x5a79
$func86 @   hi_lib_bg.wasm:0x663a
$func120    @   hi_lib_bg.wasm:0x71ba
$func126    @   hi_lib_bg.wasm:0x72c9
$func88 @   hi_lib_bg.wasm:0x673c
$func37 @   hi_lib_bg.wasm:0x20e7
$func48 @   hi_lib_bg.wasm:0x3fea
$_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h51eff7df35816d6c   @   hi_lib_bg.wasm:0x7580
__wbg_adapter_16    @   hi_lib.js:202
real    @   hi_lib.js:187
Promise.then (async)        
imports.wbg.__wbg_then_2fcac196782070cc @   hi_lib.js:440
$func67 @   hi_lib_bg.wasm:0x584d
$func66 @   hi_lib_bg.wasm:0x5709
$func61 @   hi_lib_bg.wasm:0x52af
$func103    @   hi_lib_bg.wasm:0x6cbc
$_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h51eff7df35816d6c   @   hi_lib_bg.wasm:0x7580
__wbg_adapter_16    @   hi_lib.js:202
real    @   hi_lib.js:187
Promise.then (async)        
imports.wbg.__wbg_then_8c2d62e8ae5978f7 @   hi_lib.js:444
$func44 @   hi_lib_bg.wasm:0x3723
$func45 @   hi_lib_bg.wasm:0x399b
$func37 @   hi_lib_bg.wasm:0x1ad6
$func48 @   hi_lib_bg.wasm:0x3fea
$_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h51eff7df35816d6c   @   hi_lib_bg.wasm:0x7580
__wbg_adapter_16    @   hi_lib.js:202
real    @   hi_lib.js:187
Promise.then (async)        
imports.wbg.__wbg_then_2fcac196782070cc @   hi_lib.js:440
$func67 @   hi_lib_bg.wasm:0x584d
$func66 @   hi_lib_bg.wasm:0x5709
$func61 @   hi_lib_bg.wasm:0x52af
$func103    @   hi_lib_bg.wasm:0x6cbc
$_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h51eff7df35816d6c   @   hi_lib_bg.wasm:0x7580
__wbg_adapter_16    @   hi_lib.js:202
real    @   hi_lib.js:187
Promise.then (async)        
imports.wbg.__wbg_then_8c2d62e8ae5978f7 @   hi_lib.js:444
$func44 @   hi_lib_bg.wasm:0x3723
$func45 @   hi_lib_bg.wasm:0x3913
$func37 @   hi_lib_bg.wasm:0x1ad6
$func48 @   hi_lib_bg.wasm:0x3fea
$_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h51eff7df35816d6c   @   hi_lib_bg.wasm:0x7580
__wbg_adapter_16    @   hi_lib.js:202
real    @   hi_lib.js:187
Promise.then (async)        
imports.wbg.__wbg_then_2fcac196782070cc @   hi_lib.js:440
$func67 @   hi_lib_bg.wasm:0x584d
$func64 @   hi_lib_bg.wasm:0x5503
$func79 @   hi_lib_bg.wasm:0x6202
$wasm_bindgen__convert__closures__invoke2_mut__hc0a39dba83c8fc65    @   hi_lib_bg.wasm:0x7540
__wbg_adapter_55    @   hi_lib.js:296
cb0 @   hi_lib.js:424
imports.wbg.__wbg_new_b1d61b5687f5e73a  @   hi_lib.js:429
$func183    @   hi_lib_bg.wasm:0x77e4
$unred  @   hi_lib_bg.wasm:0x66a2
unred   @   hi_lib.js:226
run @   rust_html.html:16
await in run (async)        
(anonymous) @   rust_html.html:20

I assume it has something to do with the asynchronous code, but I do not have any idea on how to solve it. I am also trying to avoid altering the javascript directly as the exercise should be completable purely in Rust.

EDIT (1) After doing some review, I think the issue is that I convert the Uint8Array into a vector and then Clamp it. It seems like the function expects an clamped array instead.

EDIT (2) I noticed another error, this time with fetch_url_binary(). The Uint8Array does not match the PNG file. There are 44750 with 4 bytes of information (RGBA) associated with each one. The Uint8Array is sized at 6384.

EDIT (3) Request for the Cargo.toml file.

[package]
name = "hi_lib"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.78" 
wasm-bindgen-futures = "0.4.28" 
image = "0.23.14"

[dependencies.js-sys]
version = "0.3.4"
 
[dependencies.web-sys] 
version = "0.3.55" 
features = ['Response','Window', 'HtmlElement', 'HtmlCanvasElement', 'HtmlImageElement','CanvasRenderingContext2d','Document', 'Element', 'Attr', 'ImageData'] 

I would recommmend going with the Cargo.toml in the answer below. There is definitely a lot of bloat with my version.

Upvotes: 3

Views: 1855

Answers (1)

frankenapps
frankenapps

Reputation: 8241

Your current code produces an IndexSizeError because ImageData expects an array of pixel colors (4 byte per pixel, using one of the allowed pixel representations), but you are supplying a whole PNG, which for example also includes the png header, but then compresses some pixel values for memory efficency. You will need to decode your png first, for example using image-rs and then supply the pixel values only.

Here is a basic example how to do this using the image crate:

lib.rs:

use image::GenericImageView;
use js_sys::Uint8Array;
use wasm_bindgen::{prelude::*, JsCast, Clamped};
use wasm_bindgen_futures::JsFuture;
use web_sys::ImageData;

#[wasm_bindgen]
pub async fn fetch_url_binary(url: String) -> Result<Uint8Array, JsValue> {
    let window = web_sys::window().unwrap(); // Browser window
    let promise = JsFuture::from(window.fetch_with_str(&url)); // File fetch promise
    let result = promise.await?; // Await fulfillment of fetch
    let response: web_sys::Response = result.dyn_into().unwrap(); // Type casting
    let image_data = JsFuture::from(response.array_buffer()?).await?; // Get text
    Ok(Uint8Array::new(&image_data))
}

#[wasm_bindgen]
pub async fn unred(url: String, canvas: String) -> Result<(), JsValue> {
    let binary = fetch_url_binary(url).await?;
    let altbuf = binary.to_vec();

    // Convert the png encoded bytes to an rgba pixel buffer (given the PNG is actually in 8byte RGBA format).
    let image = image::load_from_memory_with_format(&altbuf, image::ImageFormat::Png).unwrap();
    let mut rgba_image = image.to_rgba8();

    // I suppose this is what you tried to do in your original loop
    // judging by the function name:
    for (_, _, pixel) in rgba_image.enumerate_pixels_mut() {
        if pixel[0] > 0 {
            *pixel = image::Rgba([0, pixel[1], pixel[2], pixel[3]]);
        }
    }

    let window = web_sys::window().unwrap();
    let document = window.document().expect("Could not get document");
    let canvas = document
        .get_element_by_id(&canvas)
        .unwrap()
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    let context = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()?;
    let clamped_buf: Clamped<&[u8]> = Clamped(rgba_image.as_raw());
    let image_data_temp = 
        ImageData::new_with_u8_clamped_array_and_sh(clamped_buf, image.width(), image.height())?;
    context.put_image_data(&image_data_temp, 0.0, 0.0)?;
    Ok(())
}

Cargo.toml:

[package]
name = "hi_lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
image = "0.23.14"
js-sys = "0.3.55"
wasm-bindgen = "0.2.78"
wasm-bindgen-futures = "0.4.28"
web-sys = {version = "0.3.55", features=[
    "CanvasRenderingContext2d",
    "Document",
    "HtmlCanvasElement",
    "ImageData",
    "Response",
    "Window"
]}

It would have been good if you would have provided the list of dependencies, that way you save people who try to test your code quite some time (especially here with all these web-sys features).

For completeness sake (I changed nothing substantial) index.html:

<!doctype html>
<html lang="en">

<head>
    <title>Unred image</title>
</head>

<body>
    <canvas id="myCanvas" width="1024" height="1024" style="border:1px solid #d3d3d3;">
        <script type="module">
            import init, { unred, fetch_url_binary } from './pkg/hi_lib.js';
            var result;
            async function run() {
                await init(); // Initialize module 
                unred("myPng.png", "myCanvas");
            }
            run(); // Execute async wrapper 
        </script>
</body>

</html>

and the image I used for testing (the PNG format matters here, so be sure to use 8 bit RGBA format when encoding, or convert the image accordingly): test image

Now when building with wasm-pack build --target web and then serving the crates' root directory with a http server, this is the output: enter image description here

Upvotes: 5

Related Questions