返回文章列表

Rust WebAssembly 圖片處理效能最佳化

本文示範如何使用 Rust 和 WebAssembly 建立高效能的圖片處理網頁應用程式。透過 `image` crate 處理圖片,並以 `wasm-pack` 編譯成 Wasm 模組,在前端 JavaScript 接收圖片資料,呼叫 Wasm 函式進行縮放,最後將處理結果繪製回

Web 開發 效能最佳化

隨著網頁應用程式日益複雜,前端效能最佳化變得至關重要。WebAssembly 提供了在瀏覽器中執行高效能程式碼的途徑。本文以圖片處理為例,展示如何結合 Rust 和 WebAssembly 開發更流暢的網頁體驗。首先,使用 Rust 的 image crate 進行圖片操作,並透過 wasm-pack 將程式碼編譯成 Wasm 模組。接著,前端 JavaScript 程式碼從 Canvas 元素取得圖片資料,並呼叫 Wasm 模組中的函式進行圖片縮放。最後,將處理後的圖片資料重新繪製到 Canvas 上,展現 WebAssembly 帶來的效能提升。這個方法有效解決了 JavaScript 在處理大量圖片資料時的效能瓶頸,提供更快速、更流暢的使用者經驗。

使用 Rust 與 WebAssembly 進行圖片處理

本章節將介紹如何使用 Rust 與 WebAssembly 進行圖片處理。我們將使用 image crate 來處理圖片,並將 Rust 編譯為 WebAssembly,以便在網頁中使用。

建立 WebAssembly 專案

首先,我們需要建立一個新的 WebAssembly 專案。使用以下命令建立一個新的專案:

wasm-pack new wasm-image-processing

然後,在 Cargo.toml 檔案中新增 image crate:

[dependencies]
wasm-bindgen = "0.2.63"
image = "0.24.5"

確保 edition 設定為 "2021"

定義 JavaScript API

在開始撰寫 Rust 程式碼之前,我們需要思考一下要如何設計 JavaScript API。我們希望能夠調整圖片大小,因此需要定義一個函式來實作此功能。為了簡化範例,我們將圖片縮小一半。

瞭解 image crate 的 resize 函式

image crate 中的 resize 函式定義如下:

pub fn resize<I: GenericImageView>(
    image: &I,
    nwidth: u32,
    nheight: u32,
    filter: FilterType
) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>>
where
    I::Pixel: 'static,
    <I::Pixel as Pixel>::Subpixel: 'static,

該函式接受一個實作 GenericImageView trait 的圖片物件、新的寬度、新的高度和一個 FilterType 列舉值。我們需要將圖片資料轉換為實作 GenericImageView trait 的型別。

建立前端專案

wasm-image-processing 資料夾中建立一個新的前端專案:

npm init wasm-app client

client/index.html 檔案中新增以下 HTML 程式碼:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Cat image processor</title>
</head>
<body>
    <noscript>
        This page contains webassembly and javascript content,
        please enable javascript in your browser.
    </noscript>
    <input type="file" name="image-upload" id="image-upload" value="">
    <br>
    <button id="shrink">Shrink</button>
    <br>
    <canvas id="preview"></canvas>
    <script src="./bootstrap.js"></script>
</body>
</html>

該頁面包含一個檔案選擇器、一個按鈕和一個 <canvas> 元素,用於顯示圖片。

載入圖片檔案到 <canvas>

index.js 檔案中新增以下程式碼:

function setup(event) {
    const fileInput = document.getElementById('image-upload')
    fileInput.addEventListener('change', function(event) {
        const file = event.target.files[0]
        const imageUrl = window.URL.createObjectURL(file)
        const image = new Image()
        image.src = imageUrl
        image.addEventListener('load', (loadEvent) => {
            const canvas = document.getElementById('preview')
            canvas.width = image.naturalWidth
            canvas.height = image.naturalHeight
            canvas.getContext('2d').drawImage(
                image,
                0,
                0,
                canvas.width,
                canvas.height
            )
        })
    })
}

內容解密:

此段程式碼用於載入圖片檔案到 <canvas> 元素中。首先,我們取得檔案選擇器的元素,並為其新增一個事件監聽器。當檔案選擇器中的檔案發生變化時,我們取得所選檔案的 URL,並建立一個新的 Image 物件。然後,我們將圖片的 src 屬性設定為所選檔案的 URL,並為其新增一個 load 事件監聽器。當圖片載入完成後,我們取得 <canvas> 元素,並將其寬度和高度設定為圖片的自然寬度和高度。最後,我們使用 drawImage 方法將圖片繪製到 <canvas> 上。

此段程式碼分為以下幾個步驟:

  1. 取得檔案選擇器的元素,並為其新增事件監聽器。
  2. 當檔案選擇器中的檔案發生變化時,取得所選檔案的 URL。
  3. 建立一個新的 Image 物件,並將其 src 屬性設定為所選檔案的 URL。
  4. 當圖片載入完成後,取得 <canvas> 元素,並設定其寬度和高度。
  5. 使用 drawImage 方法將圖片繪製到 <canvas> 上。

此段程式碼的目的是將使用者所選的圖片檔案載入到 <canvas> 元素中,以便後續進行圖片處理。

將圖片傳遞給 WebAssembly

現在已經有了一個基本的 JavaScript 應用程式,可以將圖片載入到 <canvas> 中,但如何將這些資料傳遞給 Wasm 程式碼?如前所述,圖片可以表示為畫素的集合,每個畫素的顏色可以用整數表示。JavaScript 和 Rust 都非常擅長處理整數陣列,因此將使用這種格式在 JavaScript 和 Rust 程式碼之間傳遞資料。

圖片資料的表示方法

在這裡,畫素將被表示為四個顏色資料:紅色、綠色、藍色和 Alpha 通道,分別代表畫素的紅色強度、綠色強度、藍色強度和透明度。Alpha 為 0% 表示完全透明,Alpha 為 100% 表示完全不透明。每個值都由一個 u8 表示,因此範圍在 0 到 255 之間。在 Rust 端,這可以由 Vec<u8> 表示;在 JavaScript 端,可以由 Uint8ClampedArray 表示。

在 Rust 中定義 shrink_by_half 函式

在 Rust 端,可以完成函式定義,更新 lib.rs 檔案如下:

use image::{RgbaImage};
use image::imageops;
use wasm_bindgen::prelude::*;

// 當啟用 'wee_alloc' 功能時,使用 'wee_alloc' 作為全域分配器。
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
pub fn shrink_by_half(
    original_image: Vec<u8>,
    width: u32,
    height: u32
) -> Vec<u8> {
    let image: RgbaImage = image::ImageBuffer::from_vec(width, height, original_image).unwrap();
    let output_image = imageops::resize(&image, width / 2, height / 2, imageops::FilterType::Nearest);
    output_image.into_vec()
}

內容解密:

  1. original_image 引數:代表原始圖片資料的 Vec<u8>,包含每個畫素的 RGBA 資訊。
  2. widthheight 引數:代表原始圖片的寬度和高度,用於從一維陣列重建二維圖片。
  3. image::ImageBuffer::from_vec:將 Vec<u8> 轉換為 RgbaImage,以便進行圖片處理。
  4. imageops::resize:使用指定的縮放演算法(此處為 FilterType::Nearest)將圖片縮放至指定大小。
  5. output_image.into_vec():將處理後的圖片轉換回 Vec<u8>,以便傳回給 JavaScript。

在前端呼叫 shrink_by_half 函式

在前端頁面中,可以為「Shrink」按鈕新增事件監聽器,以觸發對 shrink_by_half() Wasm 函式的呼叫。修改 index.js 檔案如下:

import * as wasm from '../pkg/wasm_image_processing';

// 在 setup 函式中新增事件監聽器
document.getElementById('shrink').addEventListener('click', () => {
    const canvas = document.getElementById('preview');
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const originalImage = imageData.data;

    const width = canvas.width;
    const height = canvas.height;

    const shrunkImage = wasm.shrink_by_half(originalImage, width, height);
    // 將 shrunkImage 資料繪製到 canvas 上
    const shrunkCanvas = document.getElementById('shrunk-preview');
    shrunkCanvas.width = width / 2;
    shrunkCanvas.height = height / 2;
    const shrunkCtx = shrunkCanvas.getContext('2d');
    const shrunkImageData = shrunkCtx.createImageData(width / 2, height / 2);
    shrunkImageData.data.set(shrunkImage);
    shrunkCtx.putImageData(shrunkImageData, 0, 0);
});

內容解密:

  1. 取得 Canvas 資料:從 <canvas> 中取得圖片資料,包括寬度、高度和畫素資料。
  2. 呼叫 shrink_by_half:將原始圖片資料、寬度和高度傳遞給 Wasm 中的 shrink_by_half 函式,獲得縮放後的圖片資料。
  3. 繪製縮放後的圖片:將縮放後的圖片資料繪製到另一個 <canvas> 上,以顯示處理結果。

使用WebAssembly提升網頁前端效能:以圖片處理為例

隨著網頁應用程式變得越來越複雜,提升前端效能成為了一個重要的課題。WebAssembly(Wasm)是一種二進位制指令格式,可以在現代網頁瀏覽器中執行,提供了一種提升網頁效能的方法。本文將介紹如何使用Rust和WebAssembly來建立一個高效能的圖片處理網頁應用程式。

建立Rust與WebAssembly的開發環境

首先,我們需要建立一個Rust專案,並使用wasm-pack工具將Rust程式碼編譯成WebAssembly。wasm-pack是一個用於建立和封裝WebAssembly模組的工具,可以簡化Rust與JavaScript之間的互動。

步驟1:建立Rust專案並新增wasm-image-processing crate

cargo new wasm-image-processing --lib

Cargo.toml中新增以下設定:

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

[dependencies]
wasm-bindgen = "0.2.63"

步驟2:實作圖片縮小功能

src/lib.rs中實作一個簡單的圖片縮小功能:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn shrink_by_half(input: &[u8], width: u32, height: u32) -> Vec<u8> {
    // 省略實作細節...
    let mut output = Vec::new();
    // 將輸入圖片縮小一半並存入output
    output
}

步驟3:使用wasm-pack編譯Rust程式碼成WebAssembly

wasm-pack build

在JavaScript中使用WebAssembly模組

接下來,我們需要在JavaScript中使用編譯好的WebAssembly模組。首先,建立一個新的JavaScript專案,並安裝必要的依賴項。

步驟1:安裝依賴項

{
  "dependencies": {
    "wasm-image-processing": "file:../pkg"
  },
  "devDependencies": {
    "webpack": "^4.29.3",
    "webpack-cli": "^3.1.0",
    "webpack-dev-server": "^3.1.5",
    "copy-webpack-plugin": "^5.0.0"
  }
}

步驟2:在JavaScript中匯入和使用WebAssembly模組

import * as wasmImage from "wasm-image-processing";

function setup(event) {
  // ...
  const shrinkButton = document.getElementById('shrink');
  shrinkButton.addEventListener('click', function(event) {
    const canvas = document.getElementById('preview');
    const canvasContext = canvas.getContext('2d');
    const imageBuffer = canvasContext.getImageData(0, 0, canvas.width, canvas.height).data;
    const outputBuffer = wasmImage.shrink_by_half(imageBuffer, canvas.width, canvas.height);
    const u8OutputBuffer = new ImageData(new Uint8ClampedArray(outputBuffer), canvas.width / 2);
    canvasContext.clearRect(0, 0, canvas.width, canvas.height);
    canvas.width = canvas.width / 2;
    canvas.height = canvas.height / 2;
    canvasContext.putImageData(u8OutputBuffer, 0, 0);
  });
}

程式碼解密:

  1. 首先,我們從canvas元素中取得2D繪圖上下文。
  2. 使用getImageData方法取得canvas中的圖片資料。
  3. 將圖片資料傳遞給wasmImage.shrink_by_half函式,該函式是由Rust編譯成WebAssembly的模組提供的。
  4. 將縮小後的圖片資料轉換成ImageData物件,並繪製到canvas上。

執行與測試

  1. wasm-image-processing目錄下執行wasm-pack build
  2. 切換到client目錄,執行npm install && npm run start
  3. 開啟瀏覽器,存取http://localhost:8080