使用 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 框架对比

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

相关推荐
alwaysrun8 小时前
Rust编译参数与优化控制
rust·cargo·rustc·profile·strip·lto
Mos_x8 小时前
Python爬虫---中国大学MOOC爬取数据(文中有
java·后端
半夏知半秋9 小时前
mongodb的复制集整理
服务器·开发语言·数据库·后端·学习·mongodb
一点七加一9 小时前
Harmony鸿蒙开发0基础入门到精通Day09--JavaScript篇
开发语言·javascript·ecmascript
nvd119 小时前
python异步编程 -协程的实际意义
开发语言·python
码事漫谈10 小时前
C++环形缓冲区实践与注意事项
后端
码事漫谈10 小时前
不止于Linux:百花齐放的开源世界与社区的力量
后端
沐知全栈开发10 小时前
NumPy 统计函数
开发语言