Reactの環境下でWASM(Rust)使ってグリッチ?な画像を作成してみる
はじめに
ここ数年フロントエンド界隈では、WASMを聞くことが多くなりました。流行ってますよね、WASM。乗るしかない このビックウエーブに
今回の記事では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;
ざっと説明すると以下です。
- ファイル選択するとImageFileUrlにBlobオブジェクトとして保存される
- グリッチ化するを押すとcreateGlitchが作動する
- 画像データをBlobオブジェクトからメモリ上のバッファーを指定してUint8Arrayでバイナリ配列化する
- 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を使わないようにしました。
Rustのコードを書く
グリッチってどうすればいいかなぁと考えて、以下を満たせば良いと勝手に考えました。
- 画像がずれている部分がある
- 色がおかしくなっている
それで上記二つについて以下のように実装しました
- 画像配列の行をランダムな数ずらす
- それぞれのピクセルで色をランダムにする。
正直、この処理だけでグリッチって言えんの?どうなんだい?って感じですが、まあ、初心者なんでね。そこは許してほしいよ。まじで。
それでコードは以下です。
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
それで、動作結果が以下のようになりました。
グリッチになりましたね(強引)。
これは誰がなんと言おうとグリッチですわ。
補足など
今回使ったImageクレートですが、処理が遅えよと言われております。
WebAssembly で画像のリサイズ処理をやってみたら JavaScript + Canvas API より遅かった話
それで、以下のページとかで高速化で書き直してくれているらしいです
Rust(wasm)のimage::load_from_memory遅すぎ問題
こっちのコード使って変換したほうが良いかもしれません。
WASMは早くない場合もあると聞きますが、パッケージ関係の問題でもあるのでしょうか?
まとめ
グリッチをReactとWASMを使用して作ってみました。
今後は音声とか動画とかも試してみたい感あります。
参考文献
Intro to Webassembly in React With Rust
Discussion