iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 7

Day 6|小小的 pipeline 串起來 o(☆Ф∇Ф☆)o

  • 分享至 

  • xImage
  •  

灰階、亮度/對比都能動了,但每次都要「取像素 → 呼叫一支 → 貼回 → 再取像素 → 再呼叫……」很囉嗦。今天把它變成一次丟一串操作,WASM 在 Rust 裡面一口氣做完再回來──這就是最小版的 pipeline

我們這裡改成前端用一個 JS 陣列描述你要做什麼(例如:灰階 → 亮度+對比),然後整包丟給 WASM;Rust 端把這個陣列反序列化成 enum,再依序對 Vec<u8> 走過去。這樣只有一次 bytes 進出邊界,少很多來回與暫存。

Rust 部分

我們用 serde + serde_wasm_bindgen 來把 JS 的陣列(objects)轉成 Rust 的 enum。這個做法比較易讀,也方便之後擴充更多東西。

1) Cargo.toml

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

2) 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。

3) 重新打包

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 是什麼?

  • 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 值」。

enum 在這裡做什麼?

我們拿它來描述「要套用的影像操作」:可能是 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 塊不知道有什麼差,但那個珍珠好難吃...ლ(´•д• ̀ლ


上一篇
Day 5|亮度與對比:把參數塞進去
下一篇
Day 7|模糊 × 銳化一次滿足
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言