使用Webassembly实现图片压缩

WebAssembly 是一种新的编码方式,可以在现代的 Web 浏览器中运行------它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。

1. Rust安装

这里我是使用Rust语言,将Rust编译成WebAssembly

首先我们安装下 Rust 环境,这部分看MDN官方文档即可:https://developer.mozilla.org/zh-CN/docs/WebAssembly/Guides/Rust_to_Wasm

我的电脑是mac电脑,首先执行

cpp 复制代码
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装好Rust和Rust 的包管理工具 cargo

要构建我们的包,我们需要一个额外工具 wasm-pack。它会帮助我们把我们的代码编译成 WebAssembly 并制造出正确的 npm 包。使用下面的命令可以下载并安装它:

cpp 复制代码
cargo install wasm-pack

这里我遇到

zsh: command not found: cargo

执行下处理

cpp 复制代码
source $HOME/.cargo/env

然后执行命令

cpp 复制代码
rustc --version
cargo --version

看看有没有安装成功

2. 为什么用 Rust + WASM 处理图片?

浏览器端处理图片常见方案是 Canvas/WebGL/WebCodecs 或纯 JS 算法。但当处理链路更复杂(解码、滤镜、编码、批处理)时,Rust + WASM 有几个优势:

  • CPU 密集型逻辑更稳定:Rust 编译成本地级指令(WASM),在大量像素运算时更可控。
  • 复用成熟库生态 :例如 image 提供解码、缩放、颜色变换、编码等能力。
  • 与前端无缝协作 :通过 wasm-bindgen,Rust 函数可以直接在 JS 中调用,入参出参还能自动做类型转换。

3. 构建配置

我们先创建一个文件wasm_image_gray

然后执行

cpp 复制代码
cargo init

这个命令相当于npm init,用于初始化配置

这里的 Cargo.toml相当于package.json

cpp 复制代码
[package]
name = "wasm_image_gray"
version = "0.1.0"
edition = "2024"

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

[dependencies]
wasm-bindgen = "0.2"

[dependencies.image]
version = "0.24"
default-features = false
features = ["png", "jpeg"]

wasm_image_gray/Cargo.toml 里有 3 个关键点:

  1. crate-type = ['cdylib']

    这是给 WASM 输出用的 crate 类型,能生成适合被外部(JS)加载的二进制产物。

  2. 依赖 wasm-bindgen = "0.2"

    它负责把 Rust 导出的函数生成 JS "胶水代码",并处理 JS/WASM 之间的类型映射(如 &[u8]Vec<u8> 等)。

  3. 依赖 image rust内置库,并关闭默认特性:

toml 复制代码
[dependencies.image]
version = "0.24"
default-features = false
features = ["png", "jpeg"]

这样做的目的通常是减小体积、减少不必要编译内容;但启用 png/jpeg 仍会带来相当多编译量(这也是很多人第一次 wasm-pack build 觉得"卡住"的原因之一:首次编译依赖很重)。

4. Rust 侧:导出给 JS 的 WASM API

核心代码在 wasm_image_gray/src/lib.rs。

cpp 复制代码
use std::io::Cursor;

use wasm_bindgen::prelude::*;
use image::ImageOutputFormat;

// 宏定义,来给 web 端暴露 wasm 提供方法
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    Ok(())
}

// 定义一个函数,来给 web 端调用
#[wasm_bindgen]
pub fn greeting(name: &str) -> Result<String, JsValue> {
    Ok(format!("Hello, {}!", name))
}

// 处理图片
#[wasm_bindgen]
pub fn process_image_wasm(input_data:&[u8])  -> Result<Vec<u8>, JsValue>{
    let img = image::load_from_memory(input_data).map_err(|e|JsValue::from_str(&format!("图片加载失败: {}", e)))?;

    let scaled = img.resize(800, 600, image::imageops::FilterType::Lanczos3);

    // 滤镜处理
    let grayscale = scaled.grayscale().blur(3.5).brighten(5);

    let mut  result_buf = Vec::new();

        // 将处理后的图片写入缓冲区,格式为 JPEG,质量默认 (通常是 75)
    grayscale.write_to(&mut Cursor::new(&mut result_buf), ImageOutputFormat::Jpeg(80))
        .map_err(|e| JsValue::from_str(&format!("图片编码失败: {}", e)))?;

    // 4. 返回 Vec<u8>
    // wasm_bindgen 会自动将其转换为 JS 的 Uint8Array
    Ok(result_buf)
}

4.1 #[wasm_bindgen(start)]:模块初始化入口

cpp 复制代码
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    Ok(())
}
  • 被标记为 start 的函数会在 WASM 模块初始化时调用(对应 JS 侧 init() 完成后的启动阶段)。
  • 这里暂时不做事,只返回 Ok(()),但你可以在这里做全局初始化、设置 panic hook(用于更友好的错误)等。

4.2 process_image_wasm:图片处理主函数

cpp 复制代码
#[wasm_bindgen]
pub fn process_image_wasm(input_data: &[u8]) -> Result<Vec<u8>, JsValue> { ... }

这个函数是整个案例的核心:输入是图片文件的字节数组,输出是处理后的 JPEG 字节数组。

处理链路可以拆成 4 步:

Step 1:从内存字节解码图片

cpp 复制代码
let img = image::load_from_memory(input_data)
  .map_err(|e| JsValue::from_str(&format!("图片加载失败: {}", e)))?;
  • load_from_memory 会根据图片格式自动识别解码(这里启用了 png 和 jpeg)。
  • 错误通过 JsValue 返回给 JS,便于前端 catch 并提示。

Step 2:缩放(Resize)

cpp 复制代码
let scaled = img.resize(800, 600, image::imageops::FilterType::Lanczos3);

这里固定缩放到 800x600,滤波器用 Lanczos3,质量相对更好,但计算量也更大。

如果你追求更快速度可以考虑 Triangle 或 Nearest(质量下降但更快)。

Step 3:滤镜链

cpp 复制代码
let grayscale = scaled.grayscale().blur(3.5).brighten(5);
  • grayscale():转灰度
  • blur(3.5):高斯模糊(参数越大越慢)
  • brighten(5):提亮

这段代码很"链式",可读性强,也清晰表达图像处理 pipeline。

Step 4:编码为 JPEG 并返回字节

cpp 复制代码
let mut result_buf = Vec::new();
grayscale
  .write_to(&mut Cursor::new(&mut result_buf), ImageOutputFormat::Jpeg(80))
  .map_err(|e| JsValue::from_str(&format!("图片编码失败: {}", e)))?;
Ok(result_buf)
  • CursorVec<u8> 伪装成一个"可写入流",给 write_to 使用。
  • Jpeg(80) 是质量参数(0-100)80 通常在清晰度与压缩率之间比较平衡。
  • 返回 Vec<u8> 后,wasm-bindgen 会把它转换成 JS 可用的 Uint8Array

5. JS/HTML 侧:如何把文件字节送进 WASM,再把结果展示出来

入口页面在 wasm_image_gray/index.html。核心逻辑分 3 段:

cpp 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Rust + WASM 图片极速处理实战</title>
    <style>
      body {
        font-family: sans-serif;
        padding: 20px;
        max-width: 800px;
        margin: 0 auto;
        background: #f5f5f5;
      }
      .container {
        background: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      }
      .btn-group {
        margin: 20px 0;
      }
      button {
        padding: 10px 20px;
        cursor: pointer;
        background: #007bff;
        color: white;
        border: none;
        border-radius: 4px;
        font-size: 16px;
      }
      button:disabled {
        background: #ccc;
        cursor: not-allowed;
      }

      .preview-area {
        display: flex;
        gap: 20px;
        margin-top: 20px;
      }
      .img-box {
        flex: 1;
        border: 1px dashed #ddd;
        padding: 10px;
        text-align: center;
      }
      img {
        max-width: 100%;
        height: auto;
        display: block;
        margin: 0 auto;
        max-height: 300px;
      }
      .stats {
        margin-top: 10px;
        font-size: 14px;
        color: #666;
        font-family: monospace;
      }
      h3 {
        margin: 0 0 10px 0;
        font-size: 16px;
        color: #333;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>⚡ WASM 图片处理器 (Rust Powered)</h2>
      <p>处理流程:Resize(800px) -> Grayscale -> JPEG Encode</p>

      <!-- 文件选择 -->
      <input type="file" id="upload" accept="image/*" />

      <!-- 预览区域 -->
      <div class="preview-area">
        <div class="img-box">
          <h3>原始图片 (JS)</h3>
          <img id="img-origin" />
          <div id="stats-origin" class="stats"></div>
        </div>
        <div class="img-box">
          <h3>WASM 处理结果</h3>
          <img id="img-result" />
          <div id="stats-result" class="stats">等待处理...</div>
        </div>
      </div>
    </div>

    <!-- 引入 WASM 胶水代码 -->
    <script type="module">
      // 注意:需先运行 wasm-pack build --target web,打包生成 wasm 文件,输出至 pkg 文件夹中
      // 并且 index.html 与 pkg 文件夹在同一级目录
      import init, { process_image_wasm } from "./pkg/wasm_image_gray.js";

      async function main() {
        // 1. 初始化 WASM 实例 (加载 .wasm 文件)
        await init();
        console.log("✅ WASM 模块加载完成");

        const uploadInput = document.getElementById("upload");
        const imgOrigin = document.getElementById("img-origin");
        const imgResult = document.getElementById("img-result");
        const statsOrigin = document.getElementById("stats-origin");
        const statsResult = document.getElementById("stats-result");

        uploadInput.addEventListener("change", async (e) => {
          const file = e.target.files[0];
          console.log("🚀 ~ main ~ file:", file);
          if (!file) return;

          // 2. 显示原图信息
          statsOrigin.innerHTML = `大小: ${(file.size / 1024).toFixed(2)} KB`;
          imgOrigin.src = URL.createObjectURL(file);

          // 3. 读取文件为 ArrayBuffer (JS -> WASM 内存交互的第一步)
          const reader = new FileReader();
          reader.onload = async (event) => {
            const arrayBuffer = event.target.result;
            // 转换为 Uint8Array,这是 Rust 能够识别的 &[u8]
            const uint8Array = new Uint8Array(arrayBuffer);

            try {
              statsResult.innerHTML = "处理中...";

              // --- ⏱️ 开始计时 ---
              const start = performance.now();

              // 4. 调用 Rust 函数
              // 这一步发生了数据穿越:JS内存 -> WASM线性内存 -> 计算 -> 写入WASM线性内存 -> 拷贝回JS
              const resultData = process_image_wasm(uint8Array);

              const end = performance.now();
              // --- ⏱️ 结束计时 ---

              // 5. 将结果 Uint8Array 转回 Blob 显示
              const blob = new Blob([resultData], { type: "image/jpeg" });
              imgResult.src = URL.createObjectURL(blob);

              statsResult.innerHTML = `
                        耗时: <b style="color:red">${(end - start).toFixed(
                          2
                        )} ms</b><br>
                        大小: ${(blob.size / 1024).toFixed(2)} KB<br>
                        压缩率: -${((1 - blob.size / file.size) * 100).toFixed(
                          1
                        )}%
                    `;
            } catch (err) {
              console.error(err);
              statsResult.innerHTML = "处理失败,请看控制台";
            }
          };
          reader.readAsArrayBuffer(file);
        });
      }

      main();
    </script>
  </body>
</html>

5.1 初始化 WASM 模块

cpp 复制代码
import init, { process_image_wasm } from "./pkg/wasm_image_gray.js";

await init();
console.log("✅ WASM 模块加载完成");
  • wasm-pack build --target web 会在 pkg/ 下生成 wasm_image_gray.js .wasm 文件。
  • init() 负责加载并实例化 wasm

5.2 读取用户上传的文件为 Uint8Array

cpp 复制代码
const reader = new FileReader();
reader.onload = async (event) => {
  const arrayBuffer = event.target.result;
  const uint8Array = new Uint8Array(arrayBuffer);
  const resultData = process_image_wasm(uint8Array);
};
reader.readAsArrayBuffer(file);
  • FileReader 把图片读取为 ArrayBuffer
  • 再用 Uint8Array 包装,这样才能以字节序列形式传给 Rust 的 &[u8]

5.3性能统计与展示结果

cpp 复制代码
const start = performance.now();
const resultData = process_image_wasm(uint8Array);
const end = performance.now();

const blob = new Blob([resultData], { type: "image/jpeg" });
imgResult.src = URL.createObjectURL(blob);
  • performance.now() 统计一次处理耗时
  • 返回的 resultData 作为 Blob 生成 URL,展示到 <img> 标签
  • 另外也计算了输出大小和压缩率

6.构建与运行

wasm_image_gray/ 目录下执行:

  1. 构建 WASM 包
cpp 复制代码
wasm-pack build --target web

看下效果

相关推荐
愚坤6 分钟前
前端真有意思,又干了一年图片编辑器
前端·javascript·产品
文心快码BaiduComate10 分钟前
用Comate开发我的第一个MCP——让Vibe Coding长长脑子
前端·后端·程序员
OpenTiny社区36 分钟前
这是OpenTiny与开发者一起写下的2025答卷!
前端·javascript·vue.js
龙在天1 小时前
复刻网页彩虹🌈镭射效果
前端
孟祥_成都1 小时前
让 AI 自动写 SQL、读文档,前端也能玩转 Agent! langchain chains 模块解析
前端·人工智能
天蓝色的鱼鱼2 小时前
别再瞎转Base64了!一文打通前端二进制任督二脉
前端
哟哟耶耶2 小时前
Plugin-安装Vue.js devtools6.6.3扩展(组件层级可视化)
前端·javascript·vue.js
梦6502 小时前
【前端实战】图片元素精准定位:无论缩放,元素始终钉在指定位置
前端·html·css3
烟袅2 小时前
一文搞懂 useRef:它到底在“存”什么?
前端·react.js
Knight_AL2 小时前
Vue + Spring Boot 项目统一添加 `/wvp` 访问前缀实践
前端·vue.js·spring boot