欢迎光临
我们一直在努力

使用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电脑,首先执行

curl —proto '=https' —tlsv1.2 –sSf https://sh.rustup.rs | sh

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

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

cargo install wasm–pack

这里我遇到

zsh: command not found: cargo

执行下处理

source $HOME/.cargo/env

然后执行命令

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

然后执行

cargo init

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

请添加图片描述 这里的 Cargo.toml相当于package.json

[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 个关键点:

  • crate-type = ['cdylib'] 这是给 WASM 输出用的 crate 类型,能生成适合被外部(JS)加载的二进制产物。

  • 依赖 wasm-bindgen = "0.2" 它负责把 Rust 导出的函数生成 JS “胶水代码”,并处理 JS/WASM 之间的类型映射(如 &[u8]、Vec<u8> 等)。

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

  • [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。

    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)]:模块初始化入口

    #[wasm_bindgen(start)]
    pub fn main_js() -> Result<(), JsValue> {
    Ok(())
    }

    • 被标记为 start 的函数会在 WASM 模块初始化时调用(对应 JS 侧 init() 完成后的启动阶段)。
    • 这里暂时不做事,只返回 Ok(()),但你可以在这里做全局初始化、设置 panic hook(用于更友好的错误)等。

    4.2 process_image_wasm:图片处理主函数

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

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

    处理链路可以拆成 4 步:

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

    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)

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

    这里固定缩放到 800×600,滤波器用 Lanczos3,质量相对更好,但计算量也更大。 如果你追求更快速度可以考虑 Triangle 或 Nearest(质量下降但更快)。

    Step 3:滤镜链

    let grayscale = scaled.grayscale().blur(3.5).brighten(5);

    • grayscale():转灰度
    • blur(3.5):高斯模糊(参数越大越慢)
    • brighten(5):提亮

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

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

    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)

    • Cursor 把 Vec<u8> 伪装成一个“可写入流”,给 write_to 使用。
    • Jpeg(80) 是质量参数(0-100),80 通常在清晰度与压缩率之间比较平衡。
    • 返回 Vec<u8> 后,wasm-bindgen 会把它转换成 JS 可用的 Uint8Array。

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

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

    <!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 模块

    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

    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性能统计与展示结果

    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/ 目录下执行:

  • 构建 WASM 包
  • wasm–pack build —target web

    看下效果

    请添加图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 使用Webassembly实现图片压缩
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址