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

前言
在日常工作中,我们经常需要将图片切分成多个小块,比如制作九宫格发朋友圈、处理游戏精灵图、制作拼图等。市面上虽然有一些在线工具,但往往有文件大小限制、需要上传到服务器、或者功能不够灵活。因此,我决定用 Rust 开发一个本地的图片切分工具。
本文将详细记录这个项目的开发过程,包括功能设计、技术选型、核心实现以及遇到的问题和解决方案。
项目概述
项目名称:Image Splitter(图片切分工具)
开发语言:Rust
项目类型:桌面 GUI 应用
代码规模:约 320 行核心代码
开源地址:https://gitee.com/yang-yuqing521/image-segmentation
核心功能
- ✅ 图形化界面,支持中文显示
- ✅ 图片实时预览
- ✅ 灵活的切分设置(1x1 到 10x10)
- ✅ 切分效果预览
- ✅ 批量保存切分图片
- ✅ 支持多种图片格式(PNG、JPG、BMP、GIF)
技术选型
为什么选择 Rust?
- 性能优异:图片处理是计算密集型任务,Rust 的零成本抽象和系统级性能非常适合
- 内存安全:无需 GC,编译期保证内存安全
- 跨平台:一次编写,可编译到 Windows、macOS、Linux
- 丰富的生态:有优秀的图片处理库和 GUI 框架
依赖库选型
[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. 应用状态管理
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. 图片加载流程
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. 图片切分算法
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. 预览界面布局
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. 批量保存
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- 第一行第一列,序号1photo_0_1_2.jpg- 第一行第二列,序号2- 既包含位置信息,又有全局序号,方便后续使用
开发过程中遇到的问题
问题 1:中文显示为方框
现象:
初次运行时,所有中文字符显示为空白方框。
原因:
egui 默认只包含 ASCII 字符集的字体,不支持中文。
解决方案:
手动加载系统中文字体(微软雅黑、黑体或宋体)
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 需要不同的路径
跨平台改进:
#[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:
$ rustup show
Default host: x86_64-pc-windows-msvc
active toolchain: stable-x86_64-pc-windows-gnu
gnu 工具链依赖 MinGW 环境,需要 dlltool.exe 等工具,但系统中未安装 MinGW。
解决方案:
切换到 MSVC 工具链(Windows 推荐)
# 切换默认工具链
$ 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:
[toolchain]
channel = "stable"
targets = ["x86_64-pc-windows-msvc"]
这样团队成员克隆项目后会自动使用正确的工具链。
问题 3:大图片处理性能问题
现象:
切分 8K 图片(7680x4320)为 10x10 时,预览生成耗时超过 5 秒,界面卡顿。
原因:
- 需要裁剪 100 个子图
- 每个子图都要转换为 GPU 纹理
- 同步操作阻塞 UI 线程
优化方案:
方案 1:异步处理(推荐)
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:图片降采样
对于预览,无需完整分辨率:
fn create_preview_texture(&self, tile: DynamicImage) -> egui::TextureHandle {// 预览图限制最大尺寸为 400x400let resized = tile.resize(400, 400, image::imageops::FilterType::Lanczos3);// ...
}
方案 3:懒加载
只预览可见区域的图片:
// 使用 ScrollArea,只渲染视口内的纹理
egui::ScrollArea::vertical().show(ui, |ui| {for (idx, texture) in visible_tiles.iter().enumerate() {ui.image(texture);}
});
问题 4:Cargo.toml 配置错误
现象:
Cargo.toml 中有一行:
edition = "2024"
编译警告:
warning: unknown edition `2024`
原因:
Rust Edition 只有 2015、2018、2021 版本,2024 还未发布。
修复:
[package]
name = "image-splitter"
version = "0.1.0"
edition = "2021" # 使用最新的稳定版
问题 5:内存泄漏问题
现象:
连续加载多张大图后,内存占用持续增长不释放。
原因:
preview_tiles 中的纹理一直保存在 GPU 内存中。
解决方案:
加载新图片时清理旧纹理:
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;// 加载新图片// ...
}
编译和打包
开发模式编译
cargo build
cargo run
发布版本编译
cargo build --release
生成的可执行文件在 target/release/image-splitter.exe
优化编译体积
在 Cargo.toml 中添加:
[profile.release]
opt-level = "z" # 优化体积
lto = true # 链接时优化
codegen-units = 1 # 更好的优化
strip = true # 移除符号信息
panic = "abort" # 减少 panic 处理代码
效果对比:
- 优化前:12.5 MB
- 优化后:4.8 MB(减少 61%)
使用 UPX 压缩
# 下载 UPX: https://upx.github.io/
upx --best --lzma target/release/image-splitter.exe
最终体积:约 2 MB
使用体验优化
1. 添加图标
创建 build.rs:
#[cfg(windows)]
fn main() {let mut res = winres::WindowsResource::new();res.set_icon("icon.ico");res.compile().unwrap();
}#[cfg(not(windows))]
fn main() {}
添加依赖:
[build-dependencies]
winres = "0.1"
2. 拖放文件支持
// 检测拖放事件
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. 键盘快捷键
// 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) {// 保存切分图片
}
项目总结
技术亮点
- 纯 Rust 实现:无需外部依赖,一键编译
- Immediate Mode GUI:代码简洁,易于维护
- 类型安全:编译期捕获大部分错误
- 跨平台:理论上支持 Windows、macOS、Linux
性能数据
测试环境:Windows 11, i7-12700, 32GB RAM
| 图片尺寸 | 切分设置 | 加载时间 | 预览时间 | 保存时间 |
|---|---|---|---|---|
| 1920x1080 | 3x3 | 50ms | 80ms | 120ms |
| 3840x2160 | 5x5 | 180ms | 350ms | 600ms |
| 7680x4320 | 10x10 | 650ms | 4200ms | 8500ms |
后续优化方向
-
功能扩展
- 图片合并(将多个小图合并为一张)
- 不均匀切分(自定义每块的像素大小)
- 批量处理(一次处理多张图片)
- 自定义输出命名规则
-
性能优化
- 多线程并行处理
- GPU 加速(使用 compute shader)
- 渐进式预览(边切分边显示)
-
用户体验
- 拖放文件支持
- 撤销/重做功能
- 最近使用的文件列表
- 保存配置(记住上次的切分设置)
-
跨平台适配
- macOS 构建和测试
- Linux 构建和测试
- 自动化 CI/CD 构建
开发感悟
为什么选择 Rust 是正确的
- 编译期错误检查:很多 bug 在编译阶段就被发现了
- 无 GC 暂停:图片处理过程流畅,无卡顿
- 优秀的包管理:Cargo 比 npm、pip 好用太多
- 活跃的社区:遇到问题能快速找到解决方案
遇到的挑战
- 学习曲线:所有权、生命周期概念需要时间理解
- 编译时间:首次编译依赖较慢(约 3 分钟)
- 生态不如 Python:有些库还不够成熟
给初学者的建议
- 从小项目开始:先做个命令行工具,再做 GUI
- 多看官方文档:The Rust Book 写得非常好
- 善用编译器提示:rustc 的错误信息非常详细
- 不要畏惧报错:编译器是你的朋友,不是敌人
相关链接地址
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!
参考资料
- The Rust Programming Language
- egui 官方文档
- image crate 文档
- Rust GUI 框架对比
如果这篇文章对你有帮助,欢迎分享给更多人!
