使用 Rust 开发图片切分工具:从零到发布的完整指南

使用 Rust 开发图片切分工具:从零到发布的完整指南

前言

在日常工作中,我们经常需要将图片切分成多个小块,比如制作九宫格发朋友圈、处理游戏精灵图、制作拼图等。市面上虽然有一些在线工具,但往往有文件大小限制、需要上传到服务器、或者功能不够灵活。因此,我决定用 Rust 开发一个本地的图片切分工具。

本文将详细记录这个项目的开发过程,包括功能设计、技术选型、核心实现以及遇到的问题和解决方案。

项目概述

项目名称 :Image Splitter(图片切分工具)
开发语言 :Rust
项目类型 :桌面 GUI 应用
代码规模 :约 320 行核心代码
开源地址https://gitee.com/yang-yuqing521/image-segmentation

核心功能

  • ✅ 图形化界面,支持中文显示
  • ✅ 图片实时预览
  • ✅ 灵活的切分设置(1x1 到 10x10)
  • ✅ 切分效果预览
  • ✅ 批量保存切分图片
  • ✅ 支持多种图片格式(PNG、JPG、BMP、GIF)

技术选型

为什么选择 Rust?

  1. 性能优异:图片处理是计算密集型任务,Rust 的零成本抽象和系统级性能非常适合
  2. 内存安全:无需 GC,编译期保证内存安全
  3. 跨平台:一次编写,可编译到 Windows、macOS、Linux
  4. 丰富的生态:有优秀的图片处理库和 GUI 框架

依赖库选型

toml 复制代码
[dependencies]
image = "0.25"    # 图片处理核心库
eframe = "0.29"   # GUI 应用框架
egui = "0.29"     # Immediate mode GUI 库
rfd = "0.15"      # 跨平台文件对话框
1. egui/eframe - GUI 框架

选择理由

  • Immediate Mode:相比传统 Retained Mode GUI,代码更简洁直观
  • 纯 Rust 实现:无需额外的 C/C++ 依赖
  • 跨平台:支持 Windows、macOS、Linux,甚至 Web(通过 WASM)
  • 现代化界面:开箱即用的美观 UI

替代方案对比

  • iced:也是纯 Rust GUI,但生态相对较小
  • druid:功能强大但学习曲线陡峭
  • tauri:基于 Web 技术,体积较大
2. image - 图片处理库

这是 Rust 生态中最成熟的图片处理库,支持:

  • 多种图片格式的编解码
  • 图片裁剪、缩放、旋转等操作
  • 像素级别的精细控制
3. rfd - 文件对话框

提供原生文件选择对话框,用户体验好,无需手动输入路径。

效果展示

保存结果所见即所得:

核心功能实现

1. 应用状态管理

rust 复制代码
struct ImageSplitterApp {
    // 图片相关
    source_image: Option<DynamicImage>,      // 原始图片
    source_path: Option<PathBuf>,            // 图片路径
    image_texture: Option<egui::TextureHandle>, // GPU 纹理

    // 切分设置
    rows: u32,                                // 行数
    cols: u32,                                // 列数

    // 预览
    preview_tiles: Vec<egui::TextureHandle>,  // 切分预览纹理
    show_preview: bool,                       // 是否显示预览

    // 状态
    status_message: String,                   // 状态栏信息
}

使用 Option<T> 来处理可能为空的状态,这是 Rust 的惯用法,避免了空指针异常。

2. 图片加载流程

rust 复制代码
fn load_image(&mut self, path: PathBuf, ctx: &egui::Context) {
    match image::open(&path) {
        Ok(img) => {
            // 1. 更新状态信息
            self.status_message = format!("已加载图片: {} ({}x{})",
                path.file_name().unwrap().to_string_lossy(),
                img.width(), img.height());

            // 2. 转换为 RGBA8 格式
            let rgba_image = img.to_rgba8();
            let size = [img.width() as _, img.height() as _];
            let pixels = rgba_image.as_flat_samples();

            // 3. 创建 egui 纹理
            let color_image = egui::ColorImage::from_rgba_unmultiplied(
                size,
                pixels.as_slice(),
            );

            let texture = ctx.load_texture(
                "source_image",
                color_image,
                egui::TextureOptions::LINEAR,
            );

            // 4. 保存状态
            self.source_image = Some(img);
            self.image_texture = Some(texture);
        }
        Err(e) => {
            self.status_message = format!("加载图片失败: {}", e);
        }
    }
}

关键点

  • 使用 match 进行错误处理,符合 Rust 风格
  • 图片需要转换为 GPU 纹理才能在 egui 中显示
  • to_rgba8() 确保所有格式统一为 RGBA

3. 图片切分算法

rust 复制代码
fn generate_preview(&mut self, ctx: &egui::Context) {
    if let Some(img) = &self.source_image {
        let (width, height) = img.dimensions();
        let tile_width = width / self.cols;
        let tile_height = height / self.rows;

        for row in 0..self.rows {
            for col in 0..self.cols {
                // 计算切分位置
                let x = col * tile_width;
                let y = row * tile_height;

                // 裁剪图片
                let tile = img.crop_imm(x, y, tile_width, tile_height);

                // 转换为纹理用于预览
                let texture = self.create_texture(tile, ctx, row, col);
                self.preview_tiles.push(texture);
            }
        }
    }
}

算法说明

  • 使用整数除法计算每块的大小
  • crop_imm() 是不可变裁剪,不会修改原图
  • 从左到右、从上到下依次切分

边界处理

如果图片尺寸不能被行列数整除,余数部分会被丢弃。例如:

  • 1920x1080 切 3x3 → 每块 640x360(丢弃右侧和底部的像素)

4. 预览界面布局

rust 复制代码
fn show_preview_grid(&self, ui: &mut egui::Ui) {
    let tiles_per_row = self.cols as usize;
    let available_width = ui.available_width();
    let spacing = 10.0;

    // 计算每块预览图的显示大小
    let tile_display_size = ((available_width - spacing * (tiles_per_row as f32 + 1.0))
        / tiles_per_row as f32).min(200.0);

    // 网格布局
    for row in 0..self.rows as usize {
        ui.horizontal(|ui| {
            for col in 0..self.cols as usize {
                let idx = row * tiles_per_row + col;
                if let Some(texture) = self.preview_tiles.get(idx) {
                    ui.vertical(|ui| {
                        ui.image((texture.id(), egui::vec2(tile_display_size, tile_display_size)));
                        ui.label(format!("({}, {}) 第{}块", row, col, idx + 1));
                    });
                }
            }
        });
    }
}

亮点

  • 自适应布局:根据窗口宽度动态调整预览图大小
  • 限制最大尺寸(200px)避免预览图过大
  • 显示位置信息,方便用户确认

5. 批量保存

rust 复制代码
fn save_tiles(&mut self, output_dir: PathBuf) {
    if let Some(img) = &self.source_image {
        let base_name = self.source_path
            .as_ref()
            .and_then(|p| p.file_stem())
            .unwrap_or("image");

        let extension = self.source_path
            .as_ref()
            .and_then(|p| p.extension())
            .unwrap_or("png");

        for row in 0..self.rows {
            for col in 0..self.cols {
                let tile = img.crop_imm(/* ... */);

                // 文件命名:原名_行_列_序号.扩展名
                let filename = format!("{}_{}_{}_{}.{}",
                    base_name, row, col, count + 1, extension);
                let output_path = output_dir.join(&filename);

                tile.save(&output_path)?;
            }
        }
    }
}

文件命名策略

  • photo_0_0_1.jpg - 第一行第一列,序号1
  • photo_0_1_2.jpg - 第一行第二列,序号2
  • 既包含位置信息,又有全局序号,方便后续使用

开发过程中遇到的问题

问题 1:中文显示为方框

现象

初次运行时,所有中文字符显示为空白方框。

原因

egui 默认只包含 ASCII 字符集的字体,不支持中文。

解决方案

手动加载系统中文字体(微软雅黑、黑体或宋体)

rust 复制代码
fn setup_chinese_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();

    let font_paths = vec![
        "C:\\Windows\\Fonts\\msyh.ttc",      // 微软雅黑
        "C:\\Windows\\Fonts\\simhei.ttf",    // 黑体
        "C:\\Windows\\Fonts\\simsun.ttc",    // 宋体
    ];

    for font_path in font_paths {
        if let Ok(font_data) = std::fs::read(font_path) {
            fonts.font_data.insert(
                "chinese_font".to_owned(),
                egui::FontData::from_owned(font_data),
            );

            // 设置为默认字体
            fonts.families
                .entry(egui::FontFamily::Proportional)
                .or_default()
                .insert(0, "chinese_font".to_owned());

            ctx.set_fonts(fonts);
            return;
        }
    }
}

要点

  • 按优先级尝试加载字体,找到第一个可用的就停止
  • Windows 字体路径固定为 C:\Windows\Fonts\
  • macOS 和 Linux 需要不同的路径

跨平台改进

rust 复制代码
#[cfg(target_os = "windows")]
const FONT_PATHS: &[&str] = &[
    "C:\\Windows\\Fonts\\msyh.ttc",
];

#[cfg(target_os = "macos")]
const FONT_PATHS: &[&str] = &[
    "/System/Library/Fonts/PingFang.ttc",
];

#[cfg(target_os = "linux")]
const FONT_PATHS: &[&str] = &[
    "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
];

问题 2:编译错误 - dlltool.exe not found

现象

运行 cargo build --release 时报错:

复制代码
error: error calling dlltool 'dlltool.exe': program not found
error: could not compile `parking_lot_core` (lib) due to 1 previous error

原因分析

检查 Rust 工具链发现使用的是 x86_64-pc-windows-gnu

bash 复制代码
$ rustup show
Default host: x86_64-pc-windows-msvc
active toolchain: stable-x86_64-pc-windows-gnu

gnu 工具链依赖 MinGW 环境,需要 dlltool.exe 等工具,但系统中未安装 MinGW。

解决方案

切换到 MSVC 工具链(Windows 推荐)

bash 复制代码
# 切换默认工具链
$ rustup default stable-x86_64-pc-windows-msvc

# 清理之前的编译产物
$ cargo clean

# 重新编译
$ cargo build --release

知识点

Windows 上 Rust 有两种工具链:

工具链 依赖 优点 缺点
msvc Visual Studio Build Tools 官方推荐,兼容性好 需要安装 VS
gnu MinGW-w64 开源,无需 VS 需要额外安装 MinGW

最佳实践

  • Windows:使用 msvc
  • Linux:使用 gnu
  • macOS:使用默认工具链

预防措施

在项目根目录创建 rust-toolchain.toml

toml 复制代码
[toolchain]
channel = "stable"
targets = ["x86_64-pc-windows-msvc"]

这样团队成员克隆项目后会自动使用正确的工具链。

问题 3:大图片处理性能问题

现象

切分 8K 图片(7680x4320)为 10x10 时,预览生成耗时超过 5 秒,界面卡顿。

原因

  • 需要裁剪 100 个子图
  • 每个子图都要转换为 GPU 纹理
  • 同步操作阻塞 UI 线程

优化方案

方案 1:异步处理(推荐)
rust 复制代码
use std::sync::mpsc;
use std::thread;

fn generate_preview_async(&mut self, ctx: &egui::Context) {
    let img = self.source_image.clone().unwrap();
    let (sender, receiver) = mpsc::channel();

    thread::spawn(move || {
        // 在后台线程处理图片
        for row in 0..rows {
            for col in 0..cols {
                let tile = img.crop_imm(/* ... */);
                sender.send((row, col, tile)).unwrap();
            }
        }
    });

    // 在主线程创建纹理
    ctx.request_repaint();
}
方案 2:图片降采样

对于预览,无需完整分辨率:

rust 复制代码
fn create_preview_texture(&self, tile: DynamicImage) -> egui::TextureHandle {
    // 预览图限制最大尺寸为 400x400
    let resized = tile.resize(400, 400, image::imageops::FilterType::Lanczos3);
    // ...
}
方案 3:懒加载

只预览可见区域的图片:

rust 复制代码
// 使用 ScrollArea,只渲染视口内的纹理
egui::ScrollArea::vertical().show(ui, |ui| {
    for (idx, texture) in visible_tiles.iter().enumerate() {
        ui.image(texture);
    }
});

问题 4:Cargo.toml 配置错误

现象
Cargo.toml 中有一行:

toml 复制代码
edition = "2024"

编译警告:

复制代码
warning: unknown edition `2024`

原因

Rust Edition 只有 2015、2018、2021 版本,2024 还未发布。

修复

toml 复制代码
[package]
name = "image-splitter"
version = "0.1.0"
edition = "2021"  # 使用最新的稳定版

问题 5:内存泄漏问题

现象

连续加载多张大图后,内存占用持续增长不释放。

原因
preview_tiles 中的纹理一直保存在 GPU 内存中。

解决方案

加载新图片时清理旧纹理:

rust 复制代码
fn load_image(&mut self, path: PathBuf, ctx: &egui::Context) {
    // 清理旧的预览纹理
    self.preview_tiles.clear();
    self.show_preview = false;

    // egui 的纹理在 TextureHandle drop 时会自动释放
    self.image_texture = None;

    // 加载新图片
    // ...
}

编译和打包

开发模式编译

bash 复制代码
cargo build
cargo run

发布版本编译

bash 复制代码
cargo build --release

生成的可执行文件在 target/release/image-splitter.exe

优化编译体积

Cargo.toml 中添加:

toml 复制代码
[profile.release]
opt-level = "z"       # 优化体积
lto = true           # 链接时优化
codegen-units = 1    # 更好的优化
strip = true         # 移除符号信息
panic = "abort"      # 减少 panic 处理代码

效果对比

  • 优化前:12.5 MB
  • 优化后:4.8 MB(减少 61%)

使用 UPX 压缩

bash 复制代码
# 下载 UPX: https://upx.github.io/
upx --best --lzma target/release/image-splitter.exe

最终体积:约 2 MB

使用体验优化

1. 添加图标

创建 build.rs

rust 复制代码
#[cfg(windows)]
fn main() {
    let mut res = winres::WindowsResource::new();
    res.set_icon("icon.ico");
    res.compile().unwrap();
}

#[cfg(not(windows))]
fn main() {}

添加依赖:

toml 复制代码
[build-dependencies]
winres = "0.1"

2. 拖放文件支持

rust 复制代码
// 检测拖放事件
if !ctx.input(|i| i.raw.dropped_files.is_empty()) {
    let files = ctx.input(|i| i.raw.dropped_files.clone());
    if let Some(file) = files.first() {
        if let Some(path) = &file.path {
            self.load_image(path.clone(), ctx);
        }
    }
}

3. 键盘快捷键

rust 复制代码
// Ctrl+O 打开文件
if ctx.input(|i| i.key_pressed(egui::Key::O) && i.modifiers.ctrl) {
    // 打开文件对话框
}

// Ctrl+S 保存
if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
    // 保存切分图片
}

项目总结

技术亮点

  1. 纯 Rust 实现:无需外部依赖,一键编译
  2. Immediate Mode GUI:代码简洁,易于维护
  3. 类型安全:编译期捕获大部分错误
  4. 跨平台:理论上支持 Windows、macOS、Linux

性能数据

测试环境:Windows 11, i7-12700, 32GB RAM

图片尺寸 切分设置 加载时间 预览时间 保存时间
1920x1080 3x3 50ms 80ms 120ms
3840x2160 5x5 180ms 350ms 600ms
7680x4320 10x10 650ms 4200ms 8500ms

后续优化方向

  1. 功能扩展

    • 图片合并(将多个小图合并为一张)
    • 不均匀切分(自定义每块的像素大小)
    • 批量处理(一次处理多张图片)
    • 自定义输出命名规则
  2. 性能优化

    • 多线程并行处理
    • GPU 加速(使用 compute shader)
    • 渐进式预览(边切分边显示)
  3. 用户体验

    • 拖放文件支持
    • 撤销/重做功能
    • 最近使用的文件列表
    • 保存配置(记住上次的切分设置)
  4. 跨平台适配

    • macOS 构建和测试
    • Linux 构建和测试
    • 自动化 CI/CD 构建

开发感悟

为什么选择 Rust 是正确的

  1. 编译期错误检查:很多 bug 在编译阶段就被发现了
  2. 无 GC 暂停:图片处理过程流畅,无卡顿
  3. 优秀的包管理:Cargo 比 npm、pip 好用太多
  4. 活跃的社区:遇到问题能快速找到解决方案

遇到的挑战

  1. 学习曲线:所有权、生命周期概念需要时间理解
  2. 编译时间:首次编译依赖较慢(约 3 分钟)
  3. 生态不如 Python:有些库还不够成熟

给初学者的建议

  1. 从小项目开始:先做个命令行工具,再做 GUI
  2. 多看官方文档:The Rust Book 写得非常好
  3. 善用编译器提示:rustc 的错误信息非常详细
  4. 不要畏惧报错:编译器是你的朋友,不是敌人

相关链接地址

Gitee 仓库https://gitee.com/yang-yuqing521/image-segmentation
问题反馈https://gitee.com/yang-yuqing521/image-segmentation/issues
发布版本https://gitee.com/yang-yuqing521/image-segmentation/releases

欢迎 Star ⭐ 和提 Issue!

参考资料

  1. The Rust Programming Language
  2. egui 官方文档
  3. image crate 文档
  4. Rust GUI 框架对比

如果这篇文章对你有帮助,欢迎分享给更多人!

相关推荐
0xDevNull9 分钟前
MySQL数据冷热分离详解
后端·mysql
AI袋鼠帝16 分钟前
OpenClaw(龙虾)最强开源对手!Github 40K Star了,又一个爆火的Agent..
后端
lly20240625 分钟前
C 标准库 - `<stdio.h>`
开发语言
沫璃染墨27 分钟前
C++ string 从入门到精通:构造、迭代器、容量接口全解析
c语言·开发语言·c++
jwn99927 分钟前
Laravel6.x核心特性全解析
开发语言·php·laravel
迷藏49430 分钟前
**发散创新:基于Rust实现的开源合规权限管理框架设计与实践**在现代软件架构中,**权限控制(RBAC)** 已成为保障
java·开发语言·python·rust·开源
功德+n1 小时前
Linux下安装与配置Docker完整详细步骤
linux·运维·服务器·开发语言·docker·centos
明日清晨1 小时前
python扫码登录dy
开发语言·python
我是唐青枫1 小时前
C#.NET gRPC 深入解析:Proto 定义、流式调用与服务间通信取舍
开发语言·c#·.net
JJay.1 小时前
Kotlin 高阶函数学习指南
android·开发语言·kotlin