😇

Reactの環境下でWASM(Rust)使ってグリッチ?な画像を作成してみる

2021/12/17に公開

はじめに

ここ数年フロントエンド界隈では、WASMを聞くことが多くなりました。流行ってますよね、WASM。乗るしかない このビックウエーブに

bigwave.png

今回の記事ではReactでWASMを読み込んで、グリッチ?な画像を作成することをやってみました。

ファイル構造

.
├── README.md
├── react
│   ├── decoration.d.ts
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   ├── App.tsx
│   │   ├── Components
│   │   │   └── Wasm.tsx
│   │   ├── index.html
│   │   └── index.tsx
│   ├── tsconfig.json
│   └── webpack.config.js
├── wasm
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── wasm-build

Reactの環境

詳しくは説明しませんが、webpackのコードは以下です。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
    mode: 'development',
    entry: './src/index.tsx',
    experiments: {
        asyncWebAssembly: true,
    },
    resolve: {
        extensions: ['.js', '.ts','.tsx','.css']
    },
    ignoreWarnings: [
        (warning) =>
          warning.message ===
          "Critical dependency: the request of a dependency is an expression",
    ],
    module: {
        rules: [
            {
                test: /\.(ts|tsx|js)$/,
                exclude: /node_modules/,
                use: [
                  {
                    loader: 'babel-loader',
                  },
                ],
            },
						// 今回はCSSは説明しませんが一応入れています
            {
                test: /\.css$/,
                use: [
                    'css-modules-typescript-loader',
                    {
                      loader: 'css-loader',
                      options: {
                        modules: true
                      }
                    }
                  ]
            },
            { 
                test: /\.wasm$/, 
                include: path.resolve(__dirname, "src"), 
                use: [{ 
                    loader: require.resolve("wasm-loader"), 
                    options: {} 
                 }],
             },
        ]
    },
    output: {
        path: `${__dirname}/dist`,
        filename: "main.js"
    },
    plugins:[
        new HtmlWebpackPlugin({ template: './src/index.html' }),
    ]
}

package.jsonは以下です

{
  "name": "react_rust_wasm",
  "version": "1.0.0",
  "description": "",
  "main": "index.tsx",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.4",
    "@babel/preset-react": "^7.16.0",
    "@babel/preset-typescript": "^7.16.0",
    "babel-loader": "^8.2.3",
    "css-loader": "^6.5.1",
    "css-modules-typescript-loader": "^4.0.1",
    "html-webpack-plugin": "^5.5.0",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.5.2",
    "wasm": "file:../wasm-build",
    "wasm-loader": "^1.3.0",
    "webpack": "^5.64.4",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.6.0"
  },
  "dependencies": {
    "@types/react": "^17.0.37",
    "@types/react-dom": "^17.0.11",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

以下の設定は引っかかりました。

wasm-bindgeで生成されるJavascriptのファイルでwebpack5だとwarningがでている。まだ解決されていないので以下を設定してwarningを無視するようにする。

module.exports = {
  // ...
  ignoreWarnings: [
    (warning) =>
      warning.message ===
      "Critical dependency: the request of a dependency is an expression",
  ],
  // ...
}

babelrcは以下のようにします

{
    "presets": [
        [
			"@babel/preset-env",
			{
				"targets": {
					"node": "current"
				}
			}
		],
        "@babel/preset-react",
        "@babel/preset-typescript"
    ]
}

targetsを指定しないと、babelがReferenceError: regeneratorRuntime is not definedのエラーを出してしまって動きません。(ただ、今回、babelはあってもなくても良いです)

Reactのコードを書く

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
  ,
  document.getElementById('root')
);

App.tsx

import React, { useState, useEffect } from 'react';
import Webasm from './Components/Wasm';
const App = () => {

  return (
    <div id="flexContents">
      <Webasm />
    </div>
  );
}

export default App;

Wasm.tsx

import React, { useState, useEffect } from 'react';
import type * as WASM from "../../../wasm-build/wasm";

type WASM = typeof WASM;
type WasmCreateGlitch = typeof WASM.create_glitch;

const Webasm= () => {
  const [imageFileUrl, setImageFileUrl] = useState(new Blob());
  const [imageType, setImageType] = useState('');
  const [WASM, setWASM] = useState<WASM>();
  const handleClickCreateGlitch = async () => {
        const blob = await createGlitch(imageFileUrl, imageType, WASM.create_glitch);
        console.log('test',blob);
        setImageFileUrl(blob);
  }
  const processImage = async (event:any) => {
    const imageFile = event.target.files[0];
    const imageUrl = URL.createObjectURL(imageFile);
    const resp = await fetch(imageUrl);
    const b = await resp.blob();
    setImageFileUrl(b);
    setImageType(imageFile.type);
  }
  useEffect(() => {
      const getWasm = async () => {
          const wasm = await import("wasm");
          setWASM(wasm);
      }
      getWasm();
  },[]);
  
  return (
    <div>
      {imageFileUrl.size?
      <>
        <img src={URL.createObjectURL(imageFileUrl)} />
        <button onClick={() => handleClickCreateGlitch()}>グリッチ化する</button>
      </>
      :<input type="file" accept="image/*" onChange={processImage}></input>}
    </div>
  );
}

const createGlitch = async (file:Blob,format:string, wasm:WasmCreateGlitch) => {
    const arr = new Uint8Array(await file.arrayBuffer());
    const result =wasm(arr, format);
    const blob = new Blob([result]);
    return blob;
}
export default Webasm;

ざっと説明すると以下です。

  1. ファイル選択するとImageFileUrlにBlobオブジェクトとして保存される
  2. グリッチ化するを押すとcreateGlitchが作動する
  3. 画像データをBlobオブジェクトからメモリ上のバッファーを指定してUint8Arrayでバイナリ配列化する
  4. wasmにバイナリ配列データと画像の種類のデータを渡す

Carge.toml

[package]
name = "wasm"
version = "0.1.0"
edition = "2018"

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

[lib]
crate-type = ["cdylib"]
path = "src/lib.rs"

[dependencies]
wasm-bindgen = "0.2.34"
imageproc = "0.22.0"
rusttype = "0.9.2"
js-sys = "0.3.32"
console_error_panic_hook = "0.1.7"
rand = "0.8.0"
getrandom = { version = "0.2", features = ["js"] } 
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[dependencies.web-sys]
version = "0.3.44"
features = [
  'console',
]

[dependencies.image]
version = "0.23.14"
default-features = false
features = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld"]

dependencies.image

なぜか知りませんが、imageクレートをそのまま使うと、jpgが変換されないエラーが出たので以下のissueを参考にdefault-featuresを使わないようにしました。

The global thread pool has not been initialized.: ThreadPoolBuildError { kind: IOError(Error { kind: Unsupported, message: "operation not supported on this platform" }) }

Rustのコードを書く

グリッチってどうすればいいかなぁと考えて、以下を満たせば良いと勝手に考えました。

  1. 画像がずれている部分がある
  2. 色がおかしくなっている

それで上記二つについて以下のように実装しました

  1. 画像配列の行をランダムな数ずらす
  2. それぞれのピクセルで色をランダムにする。

正直、この処理だけでグリッチって言えんの?どうなんだい?って感じですが、まあ、初心者なんでね。そこは許してほしいよ。まじで。

それでコードは以下です。

extern crate console_error_panic_hook;
use wasm_bindgen::prelude::*;
use image::*;
use js_sys::*;
use rand::seq::SliceRandom;
use rand::Rng;
// use web_sys::console;

#[wasm_bindgen]
extern "C" {
    // Use `js_namespace` here to bind `console.log(..)` instead of just
    // `log(..)`
    #[wasm_bindgen(js_namespace = console, js_name = log)]
    fn log_u32(s: u32);
}

fn random_color_choice(red:u8, green:u8, blue:u8, alpha:u8) -> image::Rgba<u8>{
    let mut rng = rand::thread_rng();
    if [true, false].choose(&mut  rng).unwrap() == &true {
        Rgba([rand::thread_rng().gen_range(0..255), rand::thread_rng().gen_range(0..255), rand::thread_rng().gen_range(0..255), alpha])
    }else{
        Rgba([red, green, blue, alpha])
    }
}
#[wasm_bindgen]
pub fn create_glitch(arr: Uint8Array, fmt: &str) -> Uint8Array{
    console_error_panic_hook::set_once();
    // glitchの間隔をランダムで選択するための初期値
    let glitch_list_x:Vec<u32> = vec![1,5,7,10,15,20];
    let glitch_list_y:Vec<u32> = vec![5,10,15,20,15,30];
    let buffer: Vec<u8> = arr.to_vec();
    let mut img = load_from_memory(&buffer).expect("Error occurs at load image from buffer.");
    let (width, height) = img.dimensions();
		// ランダムのためのシード
    let mut rng = rand::thread_rng();
		// 初期値を取得する
    let mut choice = glitch_list_y.choose(&mut rng).unwrap();
    let mut boolean_glitch = [true, false].choose(&mut rng).unwrap();
    let mut n = 0;
		// 画像の全てのピクセルで処理をする。boolean_glitchがtrueの場合は行をランダムにズラす。
    while n < height{
        let mut i = 0;
        if boolean_glitch == &true {i = *glitch_list_x.choose(&mut rng).unwrap();}
        let mut t = 0;
        while &t < choice && n+t < height {
            // console::log_1(&JsValue::from(n+t));
            for x in 0..width{
                let mut to_pixel = img.get_pixel(i, n+t);
                to_pixel = random_color_choice(to_pixel[0], to_pixel[1], to_pixel[2], to_pixel[3]);
                img.put_pixel(x, n+t, to_pixel);
                i = if i >= width -1 { 0 } else { i+1 };
            }
            t += 1;
        }
        n += t;
        choice = glitch_list_y.choose(&mut rng).unwrap();
        boolean_glitch = [true, false].choose(&mut rng).unwrap();
    }
    let result = saveto_buffer(img, fmt);

    Uint8Array::new(&unsafe { Uint8Array::view(&result)}.into())
}

fn saveto_buffer(img: DynamicImage, fmt_str:&str) -> Vec<u8> {
    console_error_panic_hook::set_once();

    let fmt = match fmt_str {
        "image/png" => ImageOutputFormat::Png,
        "image/gif" => ImageOutputFormat::Gif,
        "image/bmp" => ImageOutputFormat::Bmp,
        "image/jpg" => ImageOutputFormat::Jpeg(80),
        "image/jpeg" => ImageOutputFormat::Jpeg(80),
        unsupport => ImageOutputFormat::Unsupported(String::from(unsupport)),
    };

    let mut result: Vec<u8> = Vec::new();
    img.write_to(&mut result, fmt).expect("Error occurs at save image from buffer.");

    result
}

画像編集用のimageクレートが存在するのでそれを使うと簡単にできます。

使った関数は以下になります。

  • get_pixel(x,y)
    • 画像のピクセルデータを参照
    • Rgba型として取得されるので、[red,green,blue,alpha]の形で色などが変更可能
  • dimensions()
    • 画像のpixelのheightとwidthを取得する
  • put_pixel(x,y,Rgba<u8>)
    • 画像のピクセルデータを上書きする
  • load_from_memory()
    • jsから渡されたバイナリの値をイメージとして読み取る
  • write_to()
    • imageをresult変数に書き込む

実行

RustをWASMでビルドしてwasm-buildファイルを作成します。ここはwebpackの設定変更すればどんなファイル名でも良いです

wasm-pack build --out-dir ../wasm-build

Reactを立ち上げましょう

npx webpack serve --config webpack.config.js

それで、動作結果が以下のようになりました。

glitch_gif.gif

グリッチになりましたね(強引)。

これは誰がなんと言おうとグリッチですわ。

補足など

今回使ったImageクレートですが、処理が遅えよと言われております。

WebAssembly で画像のリサイズ処理をやってみたら JavaScript + Canvas API より遅かった話

それで、以下のページとかで高速化で書き直してくれているらしいです

Rust(wasm)のimage::load_from_memory遅すぎ問題

こっちのコード使って変換したほうが良いかもしれません。

WASMは早くない場合もあると聞きますが、パッケージ関係の問題でもあるのでしょうか?

まとめ

グリッチをReactとWASMを使用して作ってみました。

今後は音声とか動画とかも試してみたい感あります。

参考文献

Intro to Webassembly in React With Rust

Rustで画像に文字を描画する

Rust で画像処理?

WebAssembly (Rust) によるブラウザ上の画像処理

Rustで画像をネガポジ変換

Rust - image

Discussion