使用 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 框架
依赖库选型
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- 第一行第一列,序号1photo_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) {
// 保存切分图片
}
项目总结
技术亮点
- 纯 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!
参考资料
如果这篇文章对你有帮助,欢迎分享给更多人!