灰階、亮度/對比都能動了,但每次都要「取像素 → 呼叫一支 → 貼回 → 再取像素 → 再呼叫……」很囉嗦。今天把它變成一次丟一串操作,WASM 在 Rust 裡面一口氣做完再回來──這就是最小版的 pipeline。
我們這裡改成前端用一個 JS 陣列描述你要做什麼(例如:灰階 → 亮度+對比),然後整包丟給 WASM;Rust 端把這個陣列反序列化成 enum,再依序對 Vec<u8>
走過去。這樣只有一次 bytes 進出邊界,少很多來回與暫存。
我們用 serde
+ serde_wasm_bindgen
來把 JS 的陣列(objects)轉成 Rust 的 enum。這個做法比較易讀,也方便之後擴充更多東西。
Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
src/lib.rs
保留已有的函式,再加下面這段即可:
use serde::Deserialize; // 最上方呼叫 serde
#[derive(Deserialize)]
#[serde(tag = "kind")] // 以欄位 kind 決定變體
enum Op {
#[serde(rename = "grayscale")]
Grayscale,
#[serde(rename = "bc")]
BrightnessContrast { b: f64, c: f64 },
}
/// 一次套用多個操作:ops 是 JS 陣列,例如:
/// [ { kind: "grayscale" }, { kind: "bc", b: 40, c: 60 } ]
#[wasm_bindgen]
pub fn apply_pipeline(
input: &[u8],
w: u32,
h: u32,
ops: &JsValue,
) -> Result<Vec<u8>, JsValue> {
let expected = (w as usize) * (h as usize) * 4;
if input.len() != expected {
return Err(JsValue::from_str("input length mismatch"));
}
// 反序列化 JS 陣列成 Vec<Op>
let ops: Vec<Op> = serde_wasm_bindgen::from_value(ops.clone())
.map_err(|e| JsValue::from_str(&format!("bad ops: {e}")))?;
let mut buf = input.to_vec();
for op in ops {
match op {
Op::Grayscale => {
buf = grayscale(&buf, w, h);
}
Op::BrightnessContrast { b, c } => {
buf = brightness_contrast(&buf, w, h, b, c);
}
}
}
Ok(buf)
}
這裡我直接重用前兩天做好的兩支函式,就是把 buf 依序餵進去、吃回結果繼續往下個 op。
rm -rf pkg
wasm-pack build --target web --out-dir pkg --out-name rustwasm_test
最上面改成引用 apply_pipeline
import init, { apply_pipeline } from 'rustwasm-test'
import wasmUrl from 'rustwasm-test/rustwasm_test_bg.wasm?url'
UI 與載圖不變,canvas/ctx/w/h/original 都保持。到截圖後的程式碼整格替換成以下程式碼:
// 共用:跑一串操作(灰階、亮度/對比…)
const runPipeline = (ops: unknown[]) => {
if (!w || !h) return
const imgData = ctx.getImageData(0, 0, w, h)
const input = new Uint8Array(imgData.data.buffer)
try {
const out = apply_pipeline(input, w, h, ops) as Uint8Array // Vec<u8> 會映射成 Uint8Array
imgData.data.set(out)
ctx.putImageData(imgData, 0, 0)
} catch (e) {
console.error('apply_pipeline failed:', e)
}
}
// 轉灰階(pipeline 單步)
go.onclick = () => {
runPipeline([{ kind: 'grayscale' }])
original = ctx.getImageData(0, 0, w, h) // 把結果當成新原圖(可選)
}
// 一次完成「灰階 → 亮度/對比」
applyBtn.onclick = () => {
const b = Number(bEl.value)
const c = Number(cEl.value)
runPipeline([
{ kind: 'grayscale' },
{ kind: 'bc', b, c },
])
}
// 還原原圖
resetBtn.onclick = () => {
if (!w || !h || !original) return
ctx.putImageData(original, 0, 0)
bEl.value = '0'; cEl.value = '0'; syncLabel()
}
啟動:
cd demo
pnpm remove rustwasm-test
pnpm add file:../pkg
pnpm dev
serde = serialization / deserialization 的縮寫。
它是一套 Rust 的通用框架,負責把資料 轉成可傳輸的格式(序列化),或從那些格式 還原成 Rust 型別(反序列化)。
常見搭配:
serde_json
:把 Rust 型別 ⇄ JSON 字串。serde-wasm-bindgen
:把 Rust 型別 ⇄ JS 的值(JsValue
,不是字串!)——這就是我們在 WASM 場景用的橋樑。在 WASM 專案裡,你從前端傳來的是 JS 物件/陣列,不是 JSON 字串。
所以要用
serde-wasm-bindgen::from_value(js_value)
來「直接吃 JS 值」。
我們拿它來描述「要套用的影像操作」:可能是 Grayscale
,也可能是 BrightnessContrast { b, c }
。
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(tag = "kind")] // 讓 serde 用欄位 "kind" 來判斷是哪一種變體
enum Op {
#[serde(rename = "grayscale")]
Grayscale,
#[serde(rename = "bc")]
BrightnessContrast { b: f64, c: f64 },
}
#[derive(Deserialize)]
:讓 serde 自動幫你實作「從外部資料還原」的邏輯。#[serde(tag = "kind")]
:告訴 serde:「外部物件會有個 kind
欄位,用它來決定是哪個變體」。#[serde(rename = "...")]
:把變體名稱改成你想吃的字串(對齊前端傳來的值)。因為用了 tag = "kind"
,前端只要傳這樣的「物件陣列」:
// JS/TS
const ops = [
{ kind: 'grayscale' },
{ kind: 'bc', b: 40, c: 60 },
]
Rust 端就能把它吃成 Vec<Op>
:
use serde_wasm_bindgen as swb;
#[wasm_bindgen]
pub fn apply_pipeline(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
let ops: Vec<Op> = swb::from_value(ops.clone())
.map_err(|e| JsValue::from_str(&format!("bad ops: {e}")))?;
// 然後 match 每個變體去處理
let mut buf = input.to_vec();
for op in ops {
match op {
Op::Grayscale => { buf = grayscale(&buf, w, h); }
Op::BrightnessContrast { b, c } => { buf = brightness_contrast(&buf, w, h, b, c); }
}
}
Ok(buf)
}
參考:
https://magiclen.org/rust-serde/
https://github.com/serde-rs/json
又來打日記了:今天用文化幣看了 96 分鐘看到哭,跟朋友吃了石二鍋。回宿舍買了青蛙撞奶,菜單上還有黑糖珍珠鮮奶差 20 塊不知道有什麼差,但那個珍珠好難吃...ლ(´•д• ̀ლ