├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.toml ├── README.md ├── font └── MiSans-Normal.ttf ├── img ├── image-20250214173052178.png ├── image-20250214173053745.png ├── image-20250214173152024.png └── image-20250214173208188.png └── src ├── combine.rs ├── extractanlysis.rs ├── fileanalysis.rs ├── framebrowser.rs ├── main.rs ├── stereo.rs └── transform.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | include: 19 | - os: macos-latest 20 | arch: x86_64 21 | target: macos 22 | - os: ubuntu-latest 23 | arch: x86_64 24 | target: linux 25 | - os: ubuntu-latest # GitHub 现已提供 ARM 原生运行器 26 | arch: arm64 27 | target: linux-arm 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Install Rust toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | profile: minimal 37 | override: true 38 | target: ${{ contains(matrix.target, 'arm') && 'aarch64-unknown-linux-gnu' || '' }} 39 | 40 | - name: Cache Cargo dependencies 41 | uses: actions/cache@v3 42 | with: 43 | path: | 44 | ~/.cargo/bin/ 45 | ~/.cargo/registry/index/ 46 | ~/.cargo/registry/cache/ 47 | ~/.cargo/git/db/ 48 | target/ 49 | key: ${{ runner.os }}-${{ matrix.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} 50 | 51 | # macOS 专属步骤 52 | - name: Install macOS dependencies 53 | if: matrix.target == 'macos' 54 | run: | 55 | brew install pkg-config 56 | rustup target add x86_64-pc-windows-gnu 57 | brew install mingw-w64 58 | 59 | # Linux x86_64 专属步骤 60 | - name: Install Linux x86_64 dependencies 61 | if: matrix.target == 'linux' 62 | run: | 63 | sudo apt-get update 64 | sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 65 | 66 | # ARM 专属步骤 67 | - name: Install ARM dependencies 68 | if: matrix.target == 'linux-arm' 69 | run: | 70 | sudo apt-get update 71 | sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 72 | 73 | # 构建命令组 74 | - name: Build macOS binary 75 | if: matrix.target == 'macos' 76 | run: cargo build --release 77 | 78 | - name: Build Windows binary (macOS cross-compile) 79 | if: matrix.target == 'macos' 80 | run: cargo build --release --target x86_64-pc-windows-gnu 81 | 82 | - name: Build Linux x86_64 83 | if: matrix.target == 'linux' 84 | run: cargo build --release 85 | 86 | - name: Build Linux ARM64 (Native) 87 | if: matrix.target == 'linux-arm' 88 | run: cargo build --release 89 | 90 | # 统一打包逻辑 91 | - name: Package artifacts 92 | run: | 93 | mkdir -p release 94 | case "${{ matrix.target }}" in 95 | macos) 96 | cp target/release/stegsolve-rs release/stegsolve-rs-macos 97 | cp target/x86_64-pc-windows-gnu/release/stegsolve-rs.exe release/stegsolve-rs-windows.exe 98 | ;; 99 | linux) 100 | cp target/release/stegsolve-rs release/stegsolve-rs-linux-x86_64 101 | ;; 102 | linux-arm) 103 | cp target/release/stegsolve-rs release/stegsolve-rs-linux-aarch64 104 | ;; 105 | esac 106 | 107 | # 生成压缩包 108 | cd release 109 | tar -czvf binaries-${{ matrix.target }}.tar.gz ./* 110 | 111 | - name: Upload artifacts 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: binaries-${{ matrix.target }} 115 | path: release/binaries-${{ matrix.target }}.tar.gz 116 | 117 | release: 118 | needs: build 119 | if: startsWith(github.ref, 'refs/tags/v') 120 | runs-on: ubuntu-latest 121 | 122 | steps: 123 | - name: Download all artifacts 124 | uses: actions/download-artifact@v4 125 | with: 126 | path: ./artifacts 127 | 128 | - name: Prepare release assets 129 | run: | 130 | mkdir -p release-assets 131 | find ./artifacts -name '*.tar.gz' -exec tar -xzvf {} -C release-assets \; 132 | 133 | - name: Publish Release 134 | uses: softprops/action-gh-release@v1 135 | with: 136 | files: | 137 | release-assets/stegsolve-rs-macos 138 | release-assets/stegsolve-rs-windows.exe 139 | release-assets/stegsolve-rs-linux-x86_64 140 | release-assets/stegsolve-rs-linux-aarch64 141 | env: 142 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | 4 | 5 | .DS_Store 6 | 7 | Cargo.lock -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'confirm_exit'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=confirm_exit", 15 | "--package=confirm_exit" 16 | ], 17 | "filter": { 18 | "name": "confirm_exit", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'confirm_exit'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=confirm_exit", 34 | "--package=confirm_exit" 35 | ], 36 | "filter": { 37 | "name": "confirm_exit", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stegsolve-rs" 3 | version = "0.2.2" 4 | authors = ["Emil Ernerfeldt "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | rust-version = "1.81" 8 | publish = false 9 | 10 | 11 | [dependencies] 12 | crc32fast = "1.4.2" 13 | eframe = "0.31.0" 14 | env_logger = "0.11.6" 15 | image = "0.25.5" 16 | rand = "0.9.0" 17 | rfd = "0.15.2" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StegSolve-rs 2 | StegSolve-rs 是一个基于 Rust + egui 重构的图像隐写分析工具,复刻重构了StegSolve 3 | 4 | > 由于跨平台原因0.2版本全面重构为egui进行开发。 5 | > 6 | > 主分支为egui、副分支为上个版本的gtk gui存档, 7 | 8 | ## 主要功能 9 | java原版全功能重构 10 | 11 | ![image-20250214173053745](/img/image-20250214173053745.png) 12 | 13 | 14 | ## 功能截图 15 | 16 | ![image-20250214173152024](/img/image-20250214173152024.png) 17 | 18 | ![image-20250214173208188](/img/image-20250214173208188.png) 19 | 20 | ## 如何运行 21 | `git clone https://github.com/jiayuqi7813/Stegsolve-rs.git` 22 | 23 | `cargo run` 24 | 25 | 你也可以选择直接下载release版本运行,目前支持windows-x64、linux-x64、macos-arm64、macos-x64。需要更多平台可以提交issue。 26 | 27 | linux需要如下依赖 28 | ```shell 29 | sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 30 | ``` 31 | 32 | ## Why Rust+egui? 33 | egui确实文明,gtk坑太多了( 34 | 35 | ## 已知问题 36 | 37 | 目前可能会存在一些问题,欢迎提交issue进行反馈。 38 | 39 | - [x] 目前子ui页面大小计算有问题,导致部分页面显示不全,需要手动调整窗口大小 40 | - ps:目前应该都修复了,除了有些界面会突然很大,但不会出现显示不全的问题 41 | 42 | 43 | ## todo 44 | 45 | - [x] 自动流水线打包全平台 46 | - [x] 全平台预编译打包支持 47 | - [ ] 多语言支持 48 | - [ ] 项目结构重构 49 | 50 | ## 贡献 51 | 感谢以下贡献者的贡献 52 | 53 | [@b3nguang](https://github.com/b3nguang) 54 | 55 | ## 许可证 56 | [MIT License](LICENSE) 57 | -------------------------------------------------------------------------------- /font/MiSans-Normal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiayuqi7813/Stegsolve-rs/24bb6f8657816f1955446503c90a0b084e3696a7/font/MiSans-Normal.ttf -------------------------------------------------------------------------------- /img/image-20250214173052178.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiayuqi7813/Stegsolve-rs/24bb6f8657816f1955446503c90a0b084e3696a7/img/image-20250214173052178.png -------------------------------------------------------------------------------- /img/image-20250214173053745.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiayuqi7813/Stegsolve-rs/24bb6f8657816f1955446503c90a0b084e3696a7/img/image-20250214173053745.png -------------------------------------------------------------------------------- /img/image-20250214173152024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiayuqi7813/Stegsolve-rs/24bb6f8657816f1955446503c90a0b084e3696a7/img/image-20250214173152024.png -------------------------------------------------------------------------------- /img/image-20250214173208188.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiayuqi7813/Stegsolve-rs/24bb6f8657816f1955446503c90a0b084e3696a7/img/image-20250214173208188.png -------------------------------------------------------------------------------- /src/combine.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, TextureOptions, TextureHandle}; 2 | use egui::{Context, Slider}; 3 | use image::{RgbaImage, ImageBuffer}; 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | use rfd; 7 | 8 | /// 常量,用于选择合成模式 9 | const NUM_TRANSFORMS: i32 = 13; 10 | 11 | pub struct ImageCombiner { 12 | img1: RgbaImage, 13 | img2: Rc>>, 14 | transform_num: Rc>, 15 | texture: Option, // 存储合成图像的纹理 16 | } 17 | 18 | impl ImageCombiner { 19 | pub fn new(img1: RgbaImage) -> Self { 20 | Self { 21 | img1, 22 | img2: Rc::new(RefCell::new(None)), 23 | transform_num: Rc::new(RefCell::new(0)), 24 | texture: None, 25 | } 26 | } 27 | 28 | pub fn update(&mut self, ui: &mut egui::Ui) { 29 | // 检查拖放 30 | if !ui.ctx().input(|i| i.raw.dropped_files.is_empty()) { 31 | if let Some(dropped_file) = ui.ctx().input(|i| i.raw.dropped_files.first().cloned()) { 32 | if let Some(path) = dropped_file.path { 33 | if let Ok(img) = image::open(&path) { 34 | *self.img2.borrow_mut() = Some(img.to_rgba8()); 35 | self.update_image_with_context(ui.ctx()); 36 | } 37 | } 38 | } 39 | } 40 | 41 | // 检查键盘输入 42 | let left_key_pressed = ui.ctx().input(|i| i.key_pressed(egui::Key::ArrowLeft)); 43 | let right_key_pressed = ui.ctx().input(|i| i.key_pressed(egui::Key::ArrowRight)); 44 | 45 | if left_key_pressed { 46 | self.backward(ui.ctx()); 47 | } 48 | if right_key_pressed { 49 | self.forward(ui.ctx()); 50 | } 51 | 52 | ui.vertical(|ui| { 53 | ui.label("请选择第二张图片(可以直接拖入图片)"); 54 | 55 | if let Some(texture) = &self.texture { 56 | ui.image(texture); 57 | } 58 | 59 | ui.separator(); 60 | 61 | // 显示当前合成模式的文本 62 | ui.label(format!("当前合成模式: {}", self.get_transform_text())); 63 | 64 | ui.separator(); 65 | 66 | // Transform Selector 67 | ui.label("选择合成模式"); 68 | let mut current_transform = *self.transform_num.borrow(); 69 | if ui.add(Slider::new(&mut current_transform, 0..=NUM_TRANSFORMS - 1).text("变换模式")).changed() { 70 | *self.transform_num.borrow_mut() = current_transform; 71 | } 72 | ui.separator(); 73 | 74 | // Buttons 75 | ui.horizontal(|ui| { 76 | let left_button = egui::Button::new("<") 77 | .fill(if left_key_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 78 | if ui.add(left_button).clicked() { 79 | self.backward(ui.ctx()); 80 | } 81 | 82 | if ui.button("打开图片").clicked() { 83 | self.open_second_image(ui.ctx()); 84 | } 85 | 86 | if ui.button("保存").clicked() { 87 | self.save_image(ui.ctx()); 88 | } 89 | 90 | if ui.button("重置").clicked() { 91 | self.reset(); 92 | } 93 | 94 | let right_button = egui::Button::new(">") 95 | .fill(if right_key_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 96 | if ui.add(right_button).clicked() { 97 | self.forward(ui.ctx()); 98 | } 99 | }); 100 | }); 101 | } 102 | 103 | fn backward(&mut self, ctx: &Context) { 104 | if self.img2.borrow().is_none() { return; } 105 | { 106 | let mut num = self.transform_num.borrow_mut(); 107 | *num = if *num <= 0 { 108 | NUM_TRANSFORMS - 1 109 | } else { 110 | *num - 1 111 | }; 112 | } 113 | self.update_image_with_context(ctx); 114 | } 115 | 116 | fn forward(&mut self, ctx: &Context) { 117 | if self.img2.borrow().is_none() { return; } 118 | { 119 | let mut num = self.transform_num.borrow_mut(); 120 | *num = (*num + 1) % NUM_TRANSFORMS; 121 | } 122 | self.update_image_with_context(ctx); 123 | } 124 | 125 | fn open_second_image(&mut self, ctx: &Context) { 126 | let dialog = rfd::FileDialog::new().pick_file(); 127 | if let Some(path) = dialog { 128 | if let Ok(img) = image::open(path) { 129 | *self.img2.borrow_mut() = Some(img.to_rgba8()); 130 | self.update_image_with_context(ctx); 131 | } else { 132 | println!("无法打开图片"); 133 | } 134 | } 135 | } 136 | 137 | fn save_image(&self, _ctx: &Context) { 138 | if self.img2.borrow().is_none() { return; } 139 | 140 | let dialog = rfd::FileDialog::new().save_file(); 141 | if let Some(path) = dialog { 142 | if let Some(combined) = self.get_combined_image() { 143 | if let Err(e) = combined.save(path) { 144 | println!("保存失败: {}", e); 145 | } 146 | } 147 | } 148 | } 149 | 150 | fn get_combined_image(&self) -> Option { 151 | let img2 = self.img2.borrow(); 152 | let img2 = img2.as_ref()?; 153 | 154 | let transform_num = *self.transform_num.borrow(); 155 | 156 | match transform_num { 157 | 11 => self.horizontal_interlace(img2), 158 | 12 => self.vertical_interlace(img2), 159 | _ => self.combine_pixels(img2, transform_num), 160 | } 161 | } 162 | 163 | fn combine_pixels(&self, img2: &RgbaImage, transform_num: i32) -> Option { 164 | let width = self.img1.width().max(img2.width()); 165 | let height = self.img1.height().max(img2.height()); 166 | 167 | let mut result = ImageBuffer::new(width, height); 168 | 169 | for y in 0..height { 170 | for x in 0..width { 171 | let p1 = if x < self.img1.width() && y < self.img1.height() { 172 | self.img1.get_pixel(x, y) 173 | } else { 174 | &image::Rgba([0, 0, 0, 0]) 175 | }; 176 | 177 | let p2 = if x < img2.width() && y < img2.height() { 178 | img2.get_pixel(x, y) 179 | } else { 180 | &image::Rgba([0, 0, 0, 0]) 181 | }; 182 | 183 | let combined = match transform_num { 184 | 0 => [p1[0]^p2[0], p1[1]^p2[1], p1[2]^p2[2], 255], // XOR 185 | 1 => [p1[0]|p2[0], p1[1]|p2[1], p1[2]|p2[2], 255], // OR 186 | 2 => [p1[0]&p2[0], p1[1]&p2[1], p1[2]&p2[2], 255], // AND 187 | 3 => [ // ADD 188 | p1[0].saturating_add(p2[0]), 189 | p1[1].saturating_add(p2[1]), 190 | p1[2].saturating_add(p2[2]), 191 | 255 192 | ], 193 | 4 => [ // ADD separate 194 | ((p1[0] as u16 + p2[0] as u16) % 256) as u8, 195 | ((p1[1] as u16 + p2[1] as u16) % 256) as u8, 196 | ((p1[2] as u16 + p2[2] as u16) % 256) as u8, 197 | 255 198 | ], 199 | _ => [p1[0], p1[1], p1[2], 255] 200 | }; 201 | 202 | result.put_pixel(x, y, image::Rgba(combined)); 203 | } 204 | } 205 | 206 | Some(result) 207 | } 208 | 209 | fn horizontal_interlace(&self, img2: &RgbaImage) -> Option { 210 | let width = self.img1.width().min(img2.width()); 211 | let height = self.img1.height().min(img2.height()); 212 | 213 | let mut result = ImageBuffer::new(width, height * 2); 214 | 215 | for y in 0..height { 216 | for x in 0..width { 217 | let p1 = self.img1.get_pixel(x, y); 218 | let p2 = img2.get_pixel(x, y); 219 | 220 | result.put_pixel(x, y*2, *p1); 221 | result.put_pixel(x, y*2+1, *p2); 222 | } 223 | } 224 | 225 | Some(result) 226 | } 227 | 228 | fn vertical_interlace(&self, img2: &RgbaImage) -> Option { 229 | let width = self.img1.width().min(img2.width()); 230 | let height = self.img1.height().min(img2.height()); 231 | 232 | let mut result = ImageBuffer::new(width * 2, height); 233 | 234 | for y in 0..height { 235 | for x in 0..width { 236 | let p1 = self.img1.get_pixel(x, y); 237 | let p2 = img2.get_pixel(x, y); 238 | 239 | result.put_pixel(x*2, y, *p1); 240 | result.put_pixel(x*2+1, y, *p2); 241 | } 242 | } 243 | 244 | Some(result) 245 | } 246 | 247 | fn update_image_with_context(&mut self, ctx: &Context) { 248 | if let Some(combined) = self.get_combined_image() { 249 | let size = [combined.width() as usize, combined.height() as usize]; 250 | let image_data = egui::ColorImage::from_rgba_unmultiplied(size, combined.as_raw()); 251 | self.texture = Some(ctx.load_texture( 252 | "combined_image", 253 | image_data, 254 | TextureOptions::default(), 255 | )); 256 | } 257 | } 258 | 259 | fn get_transform_text(&self) -> String { 260 | match *self.transform_num.borrow() { 261 | 0 => "XOR", 262 | 1 => "OR", 263 | 2 => "AND", 264 | 3 => "ADD", 265 | 4 => "ADD (R,G,B separate)", 266 | 5 => "SUB", 267 | 6 => "SUB (R,G,B separate)", 268 | 7 => "MUL", 269 | 8 => "MUL (R,G,B separate)", 270 | 9 => "Lightest (R,G,B separate)", 271 | 10 => "Darkest (R,G,B separate)", 272 | 11 => "Horizontal Interlace", 273 | 12 => "Vertical Interlace", 274 | _ => "???" 275 | }.to_string() 276 | } 277 | 278 | /// 重置状态到初始值 279 | pub fn reset(&mut self) { 280 | *self.img2.borrow_mut() = None; 281 | *self.transform_num.borrow_mut() = 0; 282 | self.texture = None; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/extractanlysis.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use egui::{Align, Layout, ScrollArea, Ui}; 3 | use image::RgbaImage; 4 | use std::fs::File; 5 | use std::io::Write; 6 | 7 | // 用于文件对话框的库(需要在 Cargo.toml 中添加 rfd 依赖) 8 | // rfd 文档:https://github.com/emilk/rfd 9 | 10 | // ────────────────────────────── 11 | // 定义提取选项的枚举 12 | 13 | #[derive(PartialEq)] 14 | pub enum ExtractDirection { 15 | Row, 16 | Column, 17 | } 18 | 19 | #[derive(PartialEq)] 20 | pub enum BitOrder { 21 | MSBFirst, 22 | LSBFirst, 23 | } 24 | 25 | #[derive(PartialEq)] 26 | pub enum RgbOrder { 27 | RGB, 28 | RBG, 29 | GRB, 30 | GBR, 31 | BRG, 32 | BGR, 33 | } 34 | 35 | // ────────────────────────────── 36 | // 每个通道的位选择状态,数组顺序约定:索引0对应通道最高位(7),索引7对应最低位(0) 37 | pub struct ChannelSelection { 38 | pub name: &'static str, 39 | pub bits: [bool; 8], 40 | } 41 | 42 | impl ChannelSelection { 43 | pub fn new(name: &'static str) -> Self { 44 | Self { 45 | name, 46 | bits: [false; 8], 47 | } 48 | } 49 | } 50 | 51 | // ────────────────────────────── 52 | // ExtractDialog 保存了所有的 UI 状态和提取数据 53 | pub struct ExtractDialog { 54 | /// 是否显示此对话框,调用者可根据该值决定是否移除此对话框 55 | pub open: bool, 56 | /// 通道选择(Red, Green, Blue, Alpha) 57 | pub channel_selections: Vec, 58 | /// 提取方向:按行 / 按列 59 | pub extract_direction: ExtractDirection, 60 | /// 位顺序:MSB优先 / LSB优先 61 | pub bit_order: BitOrder, 62 | /// RGB 通道的顺序 63 | pub rgb_order: RgbOrder, 64 | /// 预览中是否包含十六进制转储 65 | pub preview_hex_dump: bool, 66 | /// 预览文本(只读) 67 | pub preview_text: String, 68 | /// 提取后的二进制数据 69 | pub extract_data: Vec, 70 | } 71 | 72 | impl Default for ExtractDialog { 73 | fn default() -> Self { 74 | Self { 75 | open: true, 76 | channel_selections: vec![ 77 | ChannelSelection::new("Red"), 78 | ChannelSelection::new("Green"), 79 | ChannelSelection::new("Blue"), 80 | ChannelSelection::new("Alpha"), 81 | ], 82 | extract_direction: ExtractDirection::Row, 83 | bit_order: BitOrder::MSBFirst, 84 | rgb_order: RgbOrder::RGB, 85 | preview_hex_dump: true, 86 | preview_text: String::new(), 87 | extract_data: Vec::new(), 88 | } 89 | } 90 | } 91 | 92 | impl ExtractDialog { 93 | /// 在 egui 的 UI 内绘制对话框,image 为待提取数据的图像 94 | /// 返回值:true 表示对话框应该关闭 95 | pub fn ui(&mut self, ui: &mut Ui, image: &RgbaImage) -> bool { 96 | let mut should_close = false; 97 | // 外层采用垂直布局 98 | ui.vertical(|ui| { 99 | // ── 预览设置 ───────────────────────────── 100 | ui.group(|ui| { 101 | ui.label("预览设置"); 102 | ui.horizontal(|ui| { 103 | ui.label("在预览中包含十六进制转储"); 104 | ui.checkbox(&mut self.preview_hex_dump, ""); 105 | }); 106 | }); 107 | 108 | ui.separator(); 109 | 110 | // ── 选项区域:左侧为位平面选择,右侧为提取选项 ───────────────────────────── 111 | ui.horizontal(|ui| { 112 | // 位平面选择· 113 | ui.group(|ui| { 114 | ui.vertical(|ui| { 115 | ui.label("位平面选择"); 116 | ui.add_space(4.0); // 标题和选项之间添加间距 117 | 118 | egui::Grid::new("channel_selection_grid") 119 | .spacing([8.0, 4.0]) // 设置水平和垂直间距 120 | .show(ui, |ui| { 121 | for channel in self.channel_selections.iter_mut() { 122 | // 通道名称(固定宽度) 123 | ui.add_sized([30.0, 20.0], egui::Label::new(channel.name)); 124 | 125 | // "全选"复选框(固定宽度) 126 | let all_selected = channel.bits.iter().all(|&b| b); 127 | let mut all_sel = all_selected; 128 | if ui 129 | .add_sized( 130 | [40.0, 20.0], 131 | egui::Checkbox::new(&mut all_sel, "全选"), 132 | ) 133 | .changed() 134 | { 135 | for b in channel.bits.iter_mut() { 136 | *b = all_sel; 137 | } 138 | } 139 | 140 | // 显示位复选框(从高位到低位,固定宽度) 141 | for (i, bit) in channel.bits.iter_mut().enumerate() { 142 | ui.add_sized( 143 | [24.0, 20.0], 144 | egui::Checkbox::new(bit, (7 - i).to_string()), 145 | ); 146 | } 147 | 148 | ui.end_row(); // 结束当前行,开始新行 149 | } 150 | }); 151 | }); 152 | }); 153 | 154 | // 提取选项 155 | ui.group(|ui| { 156 | ui.vertical(|ui| { 157 | ui.label("提取选项"); 158 | ui.add_space(4.0); // 只保留一个小间距 159 | 160 | egui::Grid::new("extract_options_grid") 161 | .spacing([8.0, 2.0]) // 减小垂直间距 162 | .show(ui, |ui| { 163 | // 提取方向:按行 / 按列 164 | ui.label("提取方向:"); 165 | ui.radio_value( 166 | &mut self.extract_direction, 167 | ExtractDirection::Row, 168 | "按行", 169 | ); 170 | ui.radio_value( 171 | &mut self.extract_direction, 172 | ExtractDirection::Column, 173 | "按列", 174 | ); 175 | ui.end_row(); 176 | 177 | // 位顺序:MSB优先 / LSB优先 178 | ui.label("位顺序:"); 179 | ui.radio_value(&mut self.bit_order, BitOrder::MSBFirst, "MSB优先"); 180 | ui.radio_value(&mut self.bit_order, BitOrder::LSBFirst, "LSB优先"); 181 | ui.end_row(); 182 | 183 | // RGB 顺序(分成两行显示) 184 | ui.label("RGB顺序:"); 185 | egui::Grid::new("rgb_order_grid").show(ui, |ui| { 186 | ui.radio_value(&mut self.rgb_order, RgbOrder::RGB, "RGB"); 187 | ui.radio_value(&mut self.rgb_order, RgbOrder::RBG, "RBG"); 188 | ui.radio_value(&mut self.rgb_order, RgbOrder::GRB, "GRB"); 189 | ui.end_row(); 190 | ui.radio_value(&mut self.rgb_order, RgbOrder::GBR, "GBR"); 191 | ui.radio_value(&mut self.rgb_order, RgbOrder::BRG, "BRG"); 192 | ui.radio_value(&mut self.rgb_order, RgbOrder::BGR, "BGR"); 193 | }); 194 | ui.end_row(); 195 | }); 196 | }); 197 | }); 198 | }); 199 | 200 | ui.separator(); 201 | 202 | // ── 预览区域 ───────────────────────────── 203 | ui.group(|ui| { 204 | ui.label("预览"); 205 | ScrollArea::vertical() 206 | .max_height(120.0) // 进一步减小预览区域高度 207 | .show(ui, |ui| { 208 | ui.add( 209 | egui::TextEdit::multiline(&mut self.preview_text) 210 | .font(egui::TextStyle::Monospace) 211 | .code_editor() 212 | .desired_rows(6) // 减小默认行数 213 | .desired_width(f32::INFINITY), 214 | ); 215 | }); 216 | }); 217 | 218 | ui.separator(); 219 | 220 | // ── 按钮区域 ───────────────────────────── 221 | ui.allocate_space(egui::vec2(0.0, 10.0)); // 添加固定间距 222 | 223 | // 创建一个固定大小的按钮区域 224 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 225 | let button_size = egui::vec2(60.0, 24.0); // 设置固定的按钮大小 226 | 227 | if ui.add_sized(button_size, egui::Button::new("取消")).clicked() { 228 | self.open = false; 229 | should_close = true; 230 | } 231 | ui.add_space(5.0); 232 | 233 | if ui.add_sized(button_size, egui::Button::new("保存二进制")).clicked() { 234 | self.generate_extract(image); 235 | self.save_binary(); 236 | } 237 | ui.add_space(5.0); 238 | 239 | if ui.add_sized(button_size, egui::Button::new("保存文本")).clicked() { 240 | self.generate_extract(image); 241 | self.generate_preview(); 242 | self.save_preview(); 243 | } 244 | ui.add_space(5.0); 245 | 246 | if ui.add_sized(button_size, egui::Button::new("预览")).clicked() { 247 | self.generate_extract(image); 248 | self.generate_preview(); 249 | } 250 | ui.add_space(5.0); 251 | 252 | if ui.add_sized(button_size, egui::Button::new("关闭")).clicked() { 253 | should_close = true; 254 | } 255 | }); 256 | 257 | ui.allocate_space(egui::vec2(0.0, 10.0)); // 底部添加固定间距 258 | }); 259 | should_close 260 | } 261 | 262 | // ───────────────────────────── 263 | // 内部方法:根据通道选择生成掩码,返回 (mask, maskbits) 264 | fn get_mask(&self) -> (u32, u32) { 265 | let mut mask = 0u32; 266 | let mut maskbits = 0u32; 267 | // 按通道顺序(Red, Green, Blue, Alpha),每个通道内按顺序(数组索引0对应位7) 268 | for (channel_index, channel) in self.channel_selections.iter().enumerate() { 269 | for (bit_index, &selected) in channel.bits.iter().enumerate() { 270 | let flat_index = channel_index * 8 + bit_index; // 0..32 271 | if selected { 272 | let shift = 31 - flat_index; 273 | mask |= 1 << shift; 274 | maskbits += 1; 275 | } 276 | } 277 | } 278 | (mask, maskbits) 279 | } 280 | 281 | // 获取提取顺序选项:返回 (row_first, lsb_first, rgb_order) 282 | fn get_bit_order_options(&self) -> (bool, bool, u8) { 283 | let row_first = self.extract_direction == ExtractDirection::Row; 284 | let lsb_first = self.bit_order == BitOrder::LSBFirst; 285 | let rgb_order = match self.rgb_order { 286 | RgbOrder::RGB => 1, 287 | RgbOrder::RBG => 2, 288 | RgbOrder::GRB => 3, 289 | RgbOrder::GBR => 4, 290 | RgbOrder::BRG => 5, 291 | RgbOrder::BGR => 6, 292 | }; 293 | (row_first, lsb_first, rgb_order) 294 | } 295 | 296 | // 向提取缓冲区添加一个位 297 | fn add_bit(extract: &mut Vec, bit_pos: &mut u8, byte_pos: &mut usize, num: u8) { 298 | if num != 0 { 299 | if let Some(byte) = extract.get_mut(*byte_pos) { 300 | *byte += *bit_pos; 301 | } 302 | } 303 | *bit_pos >>= 1; 304 | if *bit_pos >= 1 { 305 | return; 306 | } 307 | *bit_pos = 128; 308 | *byte_pos += 1; 309 | if *byte_pos < extract.len() { 310 | extract[*byte_pos] = 0; 311 | } 312 | } 313 | 314 | // 提取 8 位 315 | fn extract_8bits( 316 | extract: &mut Vec, 317 | bit_pos: &mut u8, 318 | byte_pos: &mut usize, 319 | next_byte: u32, 320 | mut current_mask: u32, 321 | mask: u32, 322 | lsb_first: bool, 323 | ) { 324 | for _ in 0..8 { 325 | if mask & current_mask != 0 { 326 | let bit_val = if next_byte & current_mask != 0 { 1 } else { 0 }; 327 | Self::add_bit(extract, bit_pos, byte_pos, bit_val); 328 | } 329 | if lsb_first { 330 | current_mask <<= 1; 331 | } else { 332 | current_mask >>= 1; 333 | } 334 | } 335 | } 336 | 337 | // 根据当前选项,从单个像素中提取位 338 | fn extract_bits( 339 | extract: &mut Vec, 340 | bit_pos: &mut u8, 341 | byte_pos: &mut usize, 342 | next_byte: u32, 343 | mask: u32, 344 | lsb_first: bool, 345 | rgb_order: u8, 346 | ) { 347 | if lsb_first { 348 | // LSB 优先:Alpha 通道(从最低位开始) 349 | Self::extract_8bits( 350 | extract, 351 | bit_pos, 352 | byte_pos, 353 | next_byte, 354 | 1 << 0, 355 | mask, 356 | lsb_first, 357 | ); 358 | // RGB 通道,按选定顺序 359 | let channels = match rgb_order { 360 | 1 => [1 << 8, 1 << 16, 1 << 24], // RGB 361 | 2 => [1 << 8, 1 << 24, 1 << 16], // RBG 362 | 3 => [1 << 16, 1 << 8, 1 << 24], // GRB 363 | 4 => [1 << 16, 1 << 24, 1 << 8], // GBR 364 | 5 => [1 << 24, 1 << 8, 1 << 16], // BRG 365 | _ => [1 << 24, 1 << 16, 1 << 8], // BGR 366 | }; 367 | for &shift in channels.iter() { 368 | Self::extract_8bits( 369 | extract, bit_pos, byte_pos, next_byte, shift, mask, lsb_first, 370 | ); 371 | } 372 | } else { 373 | // MSB 优先:Alpha 通道(从最高位开始) 374 | Self::extract_8bits( 375 | extract, 376 | bit_pos, 377 | byte_pos, 378 | next_byte, 379 | 1 << 7, 380 | mask, 381 | lsb_first, 382 | ); 383 | let channels = match rgb_order { 384 | 1 => [1 << 31, 1 << 23, 1 << 15], // RGB 385 | 2 => [1 << 31, 1 << 15, 1 << 23], // RBG 386 | 3 => [1 << 23, 1 << 31, 1 << 15], // GRB 387 | 4 => [1 << 23, 1 << 15, 1 << 31], // GBR 388 | 5 => [1 << 15, 1 << 31, 1 << 23], // BRG 389 | _ => [1 << 15, 1 << 23, 1 << 31], // BGR 390 | }; 391 | for &shift in channels.iter() { 392 | Self::extract_8bits( 393 | extract, bit_pos, byte_pos, next_byte, shift, mask, lsb_first, 394 | ); 395 | } 396 | } 397 | } 398 | 399 | /// 根据当前设置和图像生成提取数据 400 | pub fn generate_extract(&mut self, image: &RgbaImage) { 401 | let (mask, maskbits) = self.get_mask(); 402 | let (row_first, lsb_first, rgb_order) = self.get_bit_order_options(); 403 | 404 | let total_bits = (image.width() * image.height()) as u32 * maskbits; 405 | let len = ((total_bits + 7) / 8) as usize; 406 | self.extract_data = vec![0u8; len]; 407 | let mut bit_pos = 128u8; 408 | let mut byte_pos = 0usize; 409 | 410 | if row_first { 411 | for y in 0..image.height() { 412 | for x in 0..image.width() { 413 | let pixel = image.get_pixel(x, y); 414 | // 将 [r, g, b, a] 按大端顺序转换为 u32 415 | let rgba = u32::from_be_bytes([pixel[0], pixel[1], pixel[2], pixel[3]]); 416 | Self::extract_bits( 417 | &mut self.extract_data, 418 | &mut bit_pos, 419 | &mut byte_pos, 420 | rgba, 421 | mask, 422 | lsb_first, 423 | rgb_order, 424 | ); 425 | } 426 | } 427 | } else { 428 | for x in 0..image.width() { 429 | for y in 0..image.height() { 430 | let pixel = image.get_pixel(x, y); 431 | let rgba = u32::from_be_bytes([pixel[0], pixel[1], pixel[2], pixel[3]]); 432 | Self::extract_bits( 433 | &mut self.extract_data, 434 | &mut bit_pos, 435 | &mut byte_pos, 436 | rgba, 437 | mask, 438 | lsb_first, 439 | rgb_order, 440 | ); 441 | } 442 | } 443 | } 444 | } 445 | 446 | /// 生成预览文本,并更新内部的 preview_text 字段 447 | pub fn generate_preview(&mut self) { 448 | let extract = &self.extract_data; 449 | let mut preview = String::new(); 450 | let hex_dump = self.preview_hex_dump; 451 | // 每 16 字节一行 452 | for chunk_start in (0..extract.len()).step_by(16) { 453 | if hex_dump { 454 | for j in 0..16 { 455 | if chunk_start + j < extract.len() { 456 | preview.push_str(&format!("{:02x}", extract[chunk_start + j])); 457 | if j == 7 { 458 | preview.push(' '); 459 | } 460 | } 461 | } 462 | preview.push_str(" "); 463 | } 464 | for j in 0..16 { 465 | if chunk_start + j < extract.len() { 466 | let c = extract[chunk_start + j] as char; 467 | if c.is_ascii_graphic() || c.is_ascii_whitespace() { 468 | preview.push(c); 469 | } else { 470 | preview.push('.'); 471 | } 472 | if j == 7 { 473 | preview.push(' '); 474 | } 475 | } 476 | } 477 | preview.push('\n'); 478 | } 479 | self.preview_text = preview; 480 | } 481 | 482 | /// 调用文件对话框保存预览文本(保存为文本文件) 483 | pub fn save_preview(&self) { 484 | if let Some(path) = rfd::FileDialog::new().set_title("保存预览文本").save_file() { 485 | if let Ok(mut file) = File::create(path) { 486 | if let Err(e) = file.write_all(self.preview_text.as_bytes()) { 487 | eprintln!("保存文件失败: {}", e); 488 | } 489 | } 490 | } 491 | } 492 | 493 | /// 调用文件对话框保存提取数据(保存为二进制文件) 494 | pub fn save_binary(&self) { 495 | if let Some(path) = rfd::FileDialog::new() 496 | .set_title("保存二进制数据") 497 | .save_file() 498 | { 499 | if let Ok(mut file) = File::create(path) { 500 | if let Err(e) = file.write_all(&self.extract_data) { 501 | eprintln!("保存文件失败: {}", e); 502 | } 503 | } 504 | } 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/fileanalysis.rs: -------------------------------------------------------------------------------- 1 | use crc32fast::Hasher; 2 | use eframe::egui::{Align, CentralPanel, Frame, Layout, ScrollArea, TopBottomPanel, Ui}; 3 | use rfd::FileDialog; 4 | use std::fs::File; 5 | use std::io::Read; 6 | pub struct FileAnalysis { 7 | report: Vec, 8 | scroll_to_bottom: bool, 9 | } 10 | 11 | 12 | 13 | impl FileAnalysis { 14 | pub fn new(file_path: &str) -> Self { 15 | Self { 16 | report: analyse_file_format(file_path), 17 | scroll_to_bottom: false, 18 | } 19 | } 20 | 21 | pub fn ui(&mut self, ui: &mut Ui) { 22 | // 底部按钮面板(始终固定显示) 23 | TopBottomPanel::bottom("bottom_panel") 24 | .show(ui.ctx(), |ui| { 25 | Frame::NONE 26 | .fill(ui.style().visuals.window_fill) 27 | .show(ui, |ui| { 28 | ui.with_layout( 29 | Layout::left_to_right(Align::Center) 30 | .with_cross_justify(true), 31 | |ui| { 32 | if ui.button("复制到剪贴板").clicked() { 33 | ui.ctx().copy_text(self.report.join("\n")); 34 | } 35 | if ui.button("导出报告").clicked() { 36 | if let Some(path) = FileDialog::new() 37 | .add_filter("文本文件", &["txt"]) 38 | .save_file() 39 | { 40 | if let Err(e) = std::fs::write(&path, self.report.join("\n")) { 41 | eprintln!("保存文件失败: {}", e); 42 | } 43 | } 44 | } 45 | } 46 | ); 47 | }); 48 | }); 49 | 50 | // 中央内容区域(可滚动) 51 | CentralPanel::default().show(ui.ctx(), |ui| { 52 | ScrollArea::vertical() 53 | .auto_shrink([false, false]) 54 | .stick_to_bottom(self.scroll_to_bottom) 55 | .show(ui, |ui| { 56 | ui.set_width(ui.available_width()); 57 | 58 | // 显示报告内容 59 | for line in &self.report { 60 | ui.label(line); 61 | } 62 | 63 | // 自动滚动处理 64 | if self.scroll_to_bottom { 65 | ui.scroll_to_cursor(Some(Align::BOTTOM)); 66 | self.scroll_to_bottom = false; 67 | } 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | 74 | // 工具函数 75 | fn uf(data: &[u8], offset: usize) -> u8 { 76 | if offset >= data.len() { 77 | 0 78 | } else { 79 | data[offset] 80 | } 81 | } 82 | 83 | fn get_word_le(data: &[u8], offset: usize) -> u16 { 84 | if offset + 1 >= data.len() { 85 | 0 86 | } else { 87 | u16::from_le_bytes([data[offset], data[offset + 1]]) 88 | } 89 | } 90 | 91 | fn get_dword_le(data: &[u8], offset: usize) -> u32 { 92 | if offset + 3 >= data.len() { 93 | 0 94 | } else { 95 | u32::from_le_bytes([ 96 | data[offset], 97 | data[offset + 1], 98 | data[offset + 2], 99 | data[offset + 3], 100 | ]) 101 | } 102 | } 103 | 104 | fn get_dword_be(data: &[u8], offset: usize) -> u32 { 105 | if offset + 3 >= data.len() { 106 | 0 107 | } else { 108 | u32::from_be_bytes([ 109 | data[offset], 110 | data[offset + 1], 111 | data[offset + 2], 112 | data[offset + 3], 113 | ]) 114 | } 115 | } 116 | 117 | // 十六进制转储 118 | fn hex_dump(data: &[u8], from: usize, to: usize, report: &mut Vec) { 119 | if from >= data.len() { 120 | return; 121 | } 122 | 123 | report.push("十六进制:".to_string()); 124 | for i in (from..=to.min(data.len() - 1)).step_by(16) { 125 | let mut line = String::new(); 126 | for j in 0..16 { 127 | if i + j <= to && i + j < data.len() { 128 | line.push_str(&format!("{:02X} ", data[i + j])); 129 | if j == 7 { 130 | line.push(' '); 131 | } 132 | } 133 | } 134 | report.push(line); 135 | } 136 | 137 | report.push("ASCII:".to_string()); 138 | for i in (from..=to.min(data.len() - 1)).step_by(16) { 139 | let mut line = String::new(); 140 | for j in 0..16 { 141 | if i + j <= to && i + j < data.len() { 142 | let c = data[i + j] as char; 143 | if c.is_ascii_graphic() { 144 | line.push(c); 145 | } else { 146 | line.push('.'); 147 | } 148 | if j == 7 { 149 | line.push(' '); 150 | } 151 | } 152 | } 153 | report.push(line); 154 | } 155 | } 156 | 157 | /// 分析 BMP 文件 158 | 159 | // 改进BMP分析 160 | fn analyse_bmp(data: &[u8], report: &mut Vec) { 161 | if data.len() < 54 { 162 | report.push("文件太短,无法解析BMP头".to_string()); 163 | return; 164 | } 165 | 166 | let file_size = get_dword_le(data, 2); 167 | let data_offset = get_dword_le(data, 10); 168 | let header_size = get_dword_le(data, 14); 169 | let width = get_dword_le(data, 18); 170 | let height = get_dword_le(data, 22); 171 | let planes = get_word_le(data, 26); 172 | let bit_count = get_word_le(data, 28); 173 | let compression = get_dword_le(data, 30); 174 | 175 | report.push("文件头信息:".to_string()); 176 | report.push(format!("文件大小: {:X} ({}) 字节", file_size, file_size)); 177 | report.push(format!("数据偏移: {:X} 字节", data_offset)); 178 | report.push(format!("信息头大小: {:X} 字节", header_size)); 179 | report.push(format!("宽度: {} 像素", width)); 180 | report.push(format!("高度: {} 像素", height)); 181 | report.push(format!("色彩平面数: {}", planes)); 182 | report.push(format!("位深度: {} 位", bit_count)); 183 | 184 | // 压缩方式 185 | let compression_type = match compression { 186 | 0 => "无压缩", 187 | 1 => "RLE 8位压缩", 188 | 2 => "RLE 4位压缩", 189 | 3 => "Bitfields", 190 | _ => "未知压缩方式", 191 | }; 192 | report.push(format!("压缩方式: {} ({})", compression, compression_type)); 193 | 194 | // 检查颜色表 195 | if bit_count <= 8 { 196 | let color_count = if get_dword_le(data, 46) == 0 { 197 | 1 << bit_count 198 | } else { 199 | get_dword_le(data, 46) 200 | }; 201 | 202 | report.push(format!("\n颜色表 ({} 个颜色):", color_count)); 203 | let color_table_offset = 14 + header_size as usize; 204 | 205 | for i in 0..color_count as usize { 206 | let offset = color_table_offset + i * 4; 207 | if offset + 4 <= data.len() { 208 | report.push(format!( 209 | "颜色 {}: B={:02X} G={:02X} R={:02X} A={:02X}", 210 | i, 211 | data[offset], 212 | data[offset + 1], 213 | data[offset + 2], 214 | data[offset + 3] 215 | )); 216 | } 217 | } 218 | } 219 | 220 | // 检查数据偏移 221 | if data_offset as usize > 54 { 222 | report.push("\n头部与数据之间的间隙:".to_string()); 223 | hex_dump(data, 54, data_offset as usize - 1, report); 224 | } 225 | } 226 | /// 分析 PNG 文件 227 | fn analyse_png(data: &[u8], report: &mut Vec) { 228 | if data.len() < 8 || &data[0..8] != b"\x89PNG\r\n\x1a\n" { 229 | report.push("无效的 PNG 文件头".to_string()); 230 | return; 231 | } 232 | 233 | report.push("文件头: 有效的PNG文件".to_string()); 234 | let mut pos = 8; 235 | 236 | while pos + 12 <= data.len() { 237 | let length = get_dword_be(data, pos) as usize; 238 | let chunk_type = &data[pos + 4..pos + 8]; 239 | let chunk_name = std::str::from_utf8(chunk_type).unwrap_or("未知"); 240 | 241 | report.push(format!("\n块类型: {}", chunk_name)); 242 | report.push(format!("数据长度: {} 字节", length)); 243 | 244 | // CRC32校验 245 | let mut hasher = Hasher::new(); 246 | hasher.update(&data[pos + 4..pos + 8 + length]); 247 | let calculated_crc = hasher.finalize(); 248 | let file_crc = get_dword_be(data, pos + 8 + length); 249 | 250 | report.push(format!("CRC32: {:08X}", file_crc)); 251 | if calculated_crc != file_crc { 252 | report.push(format!("计算得到的CRC32: {:08X} (不匹配)", calculated_crc)); 253 | } 254 | 255 | // 特殊块分析 256 | match chunk_name { 257 | "IHDR" => { 258 | if length >= 13 { 259 | let width = get_dword_be(data, pos + 8); 260 | let height = get_dword_be(data, pos + 12); 261 | let bit_depth = data[pos + 16]; 262 | let color_type = data[pos + 17]; 263 | 264 | report.push(format!("宽度: {}", width)); 265 | report.push(format!("高度: {}", height)); 266 | report.push(format!("位深度: {}", bit_depth)); 267 | report.push(format!("颜色类型: {}", color_type)); 268 | } 269 | } 270 | "IDAT" => { 271 | report.push("图像数据块".to_string()); 272 | } 273 | "IEND" => { 274 | report.push("文件结束标记".to_string()); 275 | break; 276 | } 277 | _ => { 278 | if length > 0 { 279 | report.push("数据内容:".to_string()); 280 | hex_dump(data, pos + 8, pos + 8 + length - 1, report); 281 | } 282 | } 283 | } 284 | 285 | pos += 12 + length; 286 | } 287 | } 288 | 289 | // 改进GIF分析 290 | fn analyse_gif(data: &[u8], report: &mut Vec) { 291 | if data.len() < 13 { 292 | report.push("文件太短,无法解析GIF头".to_string()); 293 | return; 294 | } 295 | 296 | let version = std::str::from_utf8(&data[3..6]).unwrap_or("未知"); 297 | report.push(format!("GIF版本: {}", version)); 298 | 299 | let width = get_word_le(data, 6); 300 | let height = get_word_le(data, 8); 301 | report.push(format!("宽度: {} 像素", width)); 302 | report.push(format!("高度: {} 像素", height)); 303 | 304 | let flags = data[10]; 305 | let global_color_table = (flags & 0x80) != 0; 306 | let color_resolution = ((flags >> 4) & 0x07) + 1; 307 | let sort_flag = (flags & 0x08) != 0; 308 | let size_of_global_color_table = if global_color_table { 309 | 1 << ((flags & 0x07) + 1) 310 | } else { 311 | 0 312 | }; 313 | 314 | report.push(format!( 315 | "全局颜色表: {}", 316 | if global_color_table { "是" } else { "否" } 317 | )); 318 | report.push(format!("颜色分辨率: {}", color_resolution)); 319 | report.push(format!("排序标志: {}", if sort_flag { "是" } else { "否" })); 320 | report.push(format!("全局颜色表大小: {}", size_of_global_color_table)); 321 | 322 | let mut pos = 13; 323 | 324 | // 解析全局颜色表 325 | if global_color_table { 326 | report.push("\n全局颜色表:".to_string()); 327 | for i in 0..size_of_global_color_table { 328 | if pos + 3 > data.len() { 329 | break; 330 | } 331 | report.push(format!( 332 | "颜色 {}: R={:02X} G={:02X} B={:02X}", 333 | i, 334 | data[pos], 335 | data[pos + 1], 336 | data[pos + 2] 337 | )); 338 | pos += 3; 339 | } 340 | } 341 | 342 | // 解析数据块 343 | while pos < data.len() { 344 | match data[pos] { 345 | 0x2C => { 346 | if pos + 10 <= data.len() { 347 | report.push("\n图像描述符:".to_string()); 348 | let left = get_word_le(data, pos + 1); 349 | let top = get_word_le(data, pos + 3); 350 | let width = get_word_le(data, pos + 5); 351 | let height = get_word_le(data, pos + 7); 352 | 353 | report.push(format!("左边界: {}", left)); 354 | report.push(format!("上边界: {}", top)); 355 | report.push(format!("宽度: {}", width)); 356 | report.push(format!("高度: {}", height)); 357 | report.push(format!("标志位: {:02X}", data[pos + 9])); 358 | } 359 | pos += 10; 360 | } 361 | 0x21 => { 362 | if pos + 2 > data.len() { 363 | break; 364 | } 365 | match data[pos + 1] { 366 | 0xF9 => { 367 | report.push("\n图形控制扩展:".to_string()); 368 | if pos + 8 <= data.len() { 369 | let block_size = data[pos + 2]; 370 | let flags = data[pos + 3]; 371 | let delay = get_word_le(data, pos + 4); 372 | report.push(format!("块大小: {}", block_size)); 373 | report.push(format!("标志位: {:02X}", flags)); 374 | report.push(format!("延迟时间: {}", delay)); 375 | } 376 | pos += 8; 377 | } 378 | 0xFE => { 379 | report.push("\n注释扩展:".to_string()); 380 | pos += 2; 381 | while pos < data.len() && data[pos] != 0 { 382 | let size = data[pos] as usize; 383 | pos += 1; 384 | if pos + size <= data.len() { 385 | if let Ok(comment) = std::str::from_utf8(&data[pos..pos + size]) { 386 | report.push(format!("注释: {}", comment)); 387 | } 388 | pos += size; 389 | } else { 390 | break; 391 | } 392 | } 393 | pos += 1; 394 | } 395 | _ => { 396 | report.push(format!("\n未知扩展块: {:02X}", data[pos + 1])); 397 | pos += 2; 398 | } 399 | } 400 | } 401 | 0x3B => { 402 | report.push("\n文件结束标记".to_string()); 403 | break; 404 | } 405 | _ => pos += 1, 406 | } 407 | } 408 | } 409 | 410 | fn analyse_jpg(data: &[u8], report: &mut Vec) { 411 | let mut pos = 0; 412 | 413 | // 检查文件是否以 SOI (Start of Image) 开头 414 | if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 { 415 | report.push("JPEG 文件不包含有效的 SOI 标记".to_string()); 416 | return; 417 | } 418 | 419 | report.push("图像的开头 (SOI)".to_string()); 420 | pos += 2; 421 | 422 | // 解析段 423 | while pos + 4 <= data.len() { 424 | if data[pos] != 0xFF { 425 | report.push(format!("无效的标记位置: {}", pos)); 426 | break; 427 | } 428 | let marker = data[pos + 1]; 429 | let length = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize; 430 | 431 | report.push(format!("段标记: {:02X}", marker)); 432 | report.push(format!("段长度: {} 字节", length)); 433 | 434 | // 处理常见段 435 | match marker { 436 | 0xC0..=0xC3 => { 437 | report.push("帧段 (Start of Frame)".to_string()); 438 | if pos + length <= data.len() { 439 | let height = u16::from_be_bytes([data[pos + 5], data[pos + 6]]); 440 | let width = u16::from_be_bytes([data[pos + 7], data[pos + 8]]); 441 | report.push(format!("宽度: {} 像素", width)); 442 | report.push(format!("高度: {} 像素", height)); 443 | } 444 | } 445 | 0xDA => { 446 | report.push("扫描数据段 (Start of Scan)".to_string()); 447 | } 448 | 0xD9 => { 449 | report.push("图像的结尾 (EOI)".to_string()); 450 | break; 451 | } 452 | _ => { 453 | report.push("其他段".to_string()); 454 | } 455 | } 456 | 457 | pos += length + 2; 458 | } 459 | 460 | if pos < data.len() { 461 | report.push(format!("文件末尾的附加字节数: {}", data.len() - pos)); 462 | } 463 | } 464 | 465 | /// 分析文件格式 466 | pub fn analyse_file_format(file_path: &str) -> Vec { 467 | let mut report = vec!["文件格式报告".to_string()]; 468 | 469 | // 读取文件 470 | if let Ok(mut file) = File::open(file_path) { 471 | let mut data = Vec::new(); 472 | if file.read_to_end(&mut data).is_ok() { 473 | report.push(format!("文件: {}", file_path)); 474 | report.push(format!("文件大小: {} 字节", data.len())); 475 | 476 | // 简单的文件格式检查 477 | if data.len() >= 2 && data[0] == b'B' && data[1] == b'M' { 478 | report.push("文件格式: BMP".to_string()); 479 | analyse_bmp(&data, &mut report); 480 | } else if data.len() >= 4 481 | && data[0] == 0x89 482 | && data[1] == 0x50 483 | && data[2] == 0x4E 484 | && data[3] == 0x47 485 | { 486 | report.push("文件格式: PNG".to_string()); 487 | analyse_png(&data, &mut report); 488 | } else if data.len() >= 6 && data[0] == b'G' && data[1] == b'I' && data[2] == b'F' { 489 | report.push("文件格式: GIF".to_string()); 490 | analyse_gif(&data, &mut report); 491 | } else if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 { 492 | report.push("文件格式: JPEG".to_string()); 493 | analyse_jpg(&data, &mut report); 494 | } else { 495 | report.push("文件格式未知".to_string()); 496 | } 497 | } else { 498 | report.push("读取文件失败".to_string()); 499 | } 500 | } else { 501 | report.push("无法打开文件".to_string()); 502 | } 503 | 504 | report 505 | } 506 | -------------------------------------------------------------------------------- /src/framebrowser.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use egui::{ColorImage, TextureHandle, Ui}; 3 | use image::codecs::gif::GifDecoder; 4 | use image::AnimationDecoder; 5 | use image::{ImageError, ImageFormat, RgbaImage}; 6 | use std::path::Path; 7 | use std::io::BufReader; 8 | 9 | /// 帧浏览器:用于浏览、切换和保存图片帧 10 | pub struct FrameBrowser { 11 | frames: Vec, 12 | textures: Vec>, 13 | current_frame: usize, 14 | } 15 | 16 | impl FrameBrowser { 17 | /// 创建一个新的帧浏览器实例 18 | pub fn new() -> Self { 19 | Self { 20 | frames: Vec::new(), 21 | textures: Vec::new(), 22 | current_frame: 0, 23 | } 24 | } 25 | 26 | /// 从指定路径加载图像帧(目前仅支持单帧) 27 | pub fn load_frames>(&mut self, path: P) -> Result<(), ImageError> { 28 | //输出路径 29 | // println!("{:?}", path.as_ref()); 30 | // 清空现有帧 31 | self.frames.clear(); 32 | self.textures.clear(); 33 | self.current_frame = 0; 34 | 35 | // 加载图像 36 | let path = path.as_ref(); 37 | let file = std::fs::File::open(path)?; 38 | let buf_reader = std::io::BufReader::new(file); 39 | let reader = image::ImageReader::new(buf_reader).with_guessed_format()?; 40 | if let Some(format) = reader.format() { 41 | match format { 42 | ImageFormat::Gif => { 43 | // 由于 GifDecoder 需要独立的文件句柄,所以重新打开文件 44 | let file = std::fs::File::open(path)?; 45 | let buffered = BufReader::new(file); 46 | let decoder = GifDecoder::new(buffered)?; 47 | // 使用 AnimationDecoder trait 提供的 into_frames 方法 48 | let frames = decoder.into_frames().collect_frames()?; 49 | for frame in frames { 50 | self.frames.push(frame.into_buffer()); 51 | self.textures.push(None); 52 | } 53 | return Ok(()); 54 | } 55 | ImageFormat::WebP => { 56 | // 当前 WebP 多帧支持有限,加载首帧(动画 WebP 需要额外处理) 57 | let img = reader.decode()?.to_rgba8(); 58 | self.frames.push(img); 59 | self.textures.push(None); 60 | return Ok(()); 61 | } 62 | _ => { 63 | // 其他格式:加载单帧 64 | let img = reader.decode()?.to_rgba8(); 65 | self.frames.push(img); 66 | self.textures.push(None); 67 | return Ok(()); 68 | } 69 | } 70 | } else { 71 | // 无法判断格式时,尝试按静态图像加载 72 | let img = reader.decode()?.to_rgba8(); 73 | self.frames.push(img); 74 | self.textures.push(None); 75 | return Ok(()); 76 | } 77 | } 78 | 79 | 80 | /// 将 RgbaImage 转换为 egui 所需的 ColorImage 81 | fn image_to_color_image(img: &RgbaImage) -> ColorImage { 82 | let width = img.width() as usize; 83 | let height = img.height() as usize; 84 | ColorImage::from_rgba_unmultiplied([width, height], img.as_raw()) 85 | } 86 | 87 | /// 在传入的 UI 中绘制帧浏览器界面 88 | pub fn ui(&mut self, ui: &mut Ui) { 89 | // 检查键盘输入 90 | let left_pressed = ui.ctx().input(|i| i.key_pressed(egui::Key::ArrowLeft)); 91 | let right_pressed = ui.ctx().input(|i| i.key_pressed(egui::Key::ArrowRight)); 92 | 93 | if !self.frames.is_empty() { 94 | if left_pressed { 95 | if self.current_frame == 0 { 96 | self.current_frame = self.frames.len() - 1; 97 | } else { 98 | self.current_frame -= 1; 99 | } 100 | } 101 | if right_pressed { 102 | self.current_frame = (self.current_frame + 1) % self.frames.len(); 103 | } 104 | } 105 | 106 | ui.vertical(|ui| { 107 | // 如果没有加载帧,则提示 108 | if self.frames.is_empty() { 109 | ui.label("No frames loaded"); 110 | } else { 111 | ui.label(format!("Frame: {} of {}", self.current_frame + 1, self.frames.len())); 112 | // 使用 ScrollArea 显示图片 113 | egui::ScrollArea::both().show(ui, |ui| { 114 | let idx = self.current_frame; 115 | // 若纹理尚未加载,则转换并缓存 116 | if self.textures[idx].is_none() { 117 | let color_img = Self::image_to_color_image(&self.frames[idx]); 118 | let texture = ui.ctx().load_texture( 119 | format!("frame_{}", idx), 120 | color_img, 121 | Default::default(), 122 | ); 123 | self.textures[idx] = Some(texture); 124 | } 125 | if let Some(texture) = &self.textures[idx] { 126 | // let image_size = texture.size_vec2(); 127 | ui.image(texture); 128 | } 129 | }); 130 | // 底部按钮区域 131 | ui.horizontal(|ui| { 132 | let left_button = egui::Button::new("<") 133 | .fill(if left_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 134 | if ui.add(left_button).clicked() { 135 | if self.current_frame == 0 { 136 | self.current_frame = self.frames.len() - 1; 137 | } else { 138 | self.current_frame -= 1; 139 | } 140 | } 141 | 142 | let right_button = egui::Button::new(">") 143 | .fill(if right_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 144 | if ui.add(right_button).clicked() { 145 | self.current_frame = (self.current_frame + 1) % self.frames.len(); 146 | } 147 | if ui.button("Save").clicked() { 148 | if let Some(path) = rfd::FileDialog::new() 149 | .set_file_name(&format!("frame{}.png", self.current_frame + 1)) 150 | .save_file() 151 | { 152 | if let Err(e) = self.frames[self.current_frame].save(&path) { 153 | eprintln!("保存帧失败: {:?}", e); 154 | } 155 | } 156 | } 157 | }); 158 | } 159 | }); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release 2 | #![allow(rustdoc::missing_crate_level_docs)] // it's an example 3 | 4 | mod transform; 5 | mod fileanalysis; 6 | mod stereo; 7 | mod extractanlysis; 8 | mod framebrowser; 9 | mod combine; 10 | 11 | use eframe::egui; 12 | use egui::*; 13 | use rfd; 14 | use stereo::Stereo; 15 | use extractanlysis::ExtractDialog; 16 | use fileanalysis::FileAnalysis; 17 | use framebrowser::FrameBrowser; 18 | 19 | use transform::Transform; 20 | use combine::ImageCombiner; 21 | 22 | #[derive(Default)] 23 | struct StegApp { 24 | transform: Option, 25 | current_file_path: Option, 26 | zoom_level: f32, 27 | texture: Option, 28 | scroll_pos: Vec2, // 新增滚动位置记录 29 | 30 | stereo: Option, 31 | extract_dialog: Option, 32 | frame_browser: Option, 33 | combine_dialog: Option, 34 | 35 | 36 | 37 | current_channel_text: String, 38 | show_file_analysis: bool, 39 | show_extract_dialog: bool, 40 | show_stereo_dialog: bool, 41 | show_frame_browser: bool, 42 | show_combine_dialog: bool, 43 | show_about: bool, 44 | 45 | } 46 | 47 | 48 | 49 | fn main() { 50 | let options = eframe::NativeOptions { 51 | viewport: egui::ViewportBuilder::default() 52 | .with_inner_size([1000.0, 700.0]), 53 | ..Default::default() 54 | }; 55 | if let Err(e)=eframe::run_native( 56 | "StegSolve-rs", 57 | options, 58 | Box::new(|cc| { 59 | let mut fonts = egui::FontDefinitions::default(); 60 | fonts.font_data.insert( 61 | "misans".to_owned(), 62 | std::sync::Arc::new(egui::FontData::from_static( 63 | include_bytes!("../font/MiSans-Normal.ttf") 64 | )), 65 | ); 66 | fonts.families 67 | .entry(egui::FontFamily::Proportional) 68 | .or_default() 69 | .insert(0, "misans".to_owned()); 70 | 71 | cc.egui_ctx.set_fonts(fonts); 72 | Ok(Box::::default()) 73 | }), 74 | ) { 75 | eprintln!("Error: {}", e); 76 | } 77 | 78 | } 79 | 80 | 81 | impl StegApp { 82 | // pub fn new(cc: &eframe::CreationContext<'_>) -> Box { 83 | // // 设置字体 84 | // let mut fonts = egui::FontDefinitions::default(); 85 | // fonts.font_data.insert( 86 | // "misans".to_owned(), 87 | // std::sync::Arc::new(egui::FontData::from_static( 88 | // include_bytes!("../font/MiSans-Normal.ttf") 89 | // )), 90 | // ); 91 | // fonts.families 92 | // .entry(egui::FontFamily::Proportional) 93 | // .or_default() 94 | // .insert(0, "misans".to_owned()); 95 | 96 | // cc.egui_ctx.set_fonts(fonts); 97 | // Box::new(Self::default()) 98 | // } 99 | 100 | fn open_image(&mut self, path: &std::path::Path) { 101 | match image::open(path) { 102 | Ok(img) => { 103 | self.transform = Some(Transform::new(img)); 104 | self.current_file_path = Some(path.to_string_lossy().to_string()); 105 | self.texture = None; 106 | self.zoom_level = 1.0; 107 | self.scroll_pos = Vec2::ZERO; 108 | if let Some(t) = &self.transform { 109 | self.stereo = Some(Stereo::new(t.get_image().clone())); 110 | } 111 | self.frame_browser = Some(framebrowser::FrameBrowser::new()); 112 | if let Some(browser) = &mut self.frame_browser { 113 | let _ = browser.load_frames(&self.current_file_path.as_ref().unwrap()); 114 | } 115 | self.combine_dialog = Some(ImageCombiner::new(self.transform.as_ref().unwrap().get_image().clone())); 116 | 117 | } 118 | Err(e) => eprintln!("打开图片失败: {:?}", e), 119 | } 120 | } 121 | } 122 | 123 | 124 | impl eframe::App for StegApp { 125 | fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { 126 | // 添加拖放支持 127 | if !ctx.input(|i| i.raw.dropped_files.is_empty()) { 128 | // 获取拖放的第一个文件 129 | if let Some(dropped_file) = ctx.input(|i| i.raw.dropped_files.first().cloned()) { 130 | // 如果文件路径可用 131 | if let Some(path) = dropped_file.path { 132 | self.open_image(&path); 133 | } 134 | } 135 | } 136 | 137 | TopBottomPanel::top("top_panel").show(ctx, |ui| { 138 | menu::bar(ui, |ui| { 139 | // 文件菜单 140 | ui.menu_button("文件", |ui| { 141 | if ui.button("打开").clicked() { 142 | if let Some(path) = rfd::FileDialog::new().pick_file() { 143 | self.open_image(&path); 144 | } 145 | ui.close_menu(); 146 | } 147 | if ui.button("另存为").clicked() { 148 | if let Some(transform) = &self.transform { 149 | if let Some(path) = rfd::FileDialog::new().save_file() { 150 | transform.get_image().save(path).unwrap(); 151 | } 152 | } 153 | ui.close_menu(); 154 | } 155 | 156 | }); 157 | 158 | // 分析菜单 159 | ui.menu_button("分析", |ui| { 160 | if ui.button("文件格式").clicked() { 161 | self.show_file_analysis = true; 162 | ui.close_menu(); 163 | } 164 | 165 | if ui.button("数据提取").clicked() { 166 | self.show_extract_dialog = true; 167 | // 初始化数据提取对话框(仅在首次点击时创建) 168 | if self.extract_dialog.is_none() { 169 | self.extract_dialog = Some(ExtractDialog::default()); 170 | } 171 | ui.close_menu(); 172 | } 173 | if ui.button("立体视图").clicked() { 174 | self.show_stereo_dialog = true; 175 | ui.close_menu(); 176 | } 177 | if ui.button("帧浏览器").clicked() { 178 | self.show_frame_browser = true; 179 | ui.close_menu(); 180 | } 181 | if ui.button("图像合成器").clicked() { 182 | self.show_combine_dialog = true; 183 | ui.close_menu(); 184 | } 185 | }); 186 | 187 | // 帮助菜单 188 | ui.menu_button("帮助", |ui| { 189 | if ui.button("关于").clicked() { 190 | self.show_about = true; 191 | ui.close_menu(); 192 | } 193 | }); 194 | }); 195 | }); 196 | 197 | CentralPanel::default().show(ctx, |ui| { 198 | ScrollArea::both() 199 | .id_salt("image_scroll") 200 | .scroll_offset(self.scroll_pos) 201 | .show(ui, |ui| { 202 | if let Some(transform) = &self.transform { 203 | if self.texture.is_none() { 204 | let rgba_image = transform.get_image(); 205 | let size = [rgba_image.width() as usize, rgba_image.height() as usize]; 206 | let image_data = ColorImage::from_rgba_unmultiplied( 207 | size, 208 | rgba_image.as_raw(), 209 | ); 210 | self.texture = Some(ui.ctx().load_texture( 211 | "image", 212 | image_data, 213 | TextureOptions::default() 214 | )); 215 | } 216 | 217 | if let Some(texture) = &self.texture { 218 | let desired_size = texture.size_vec2() * self.zoom_level; 219 | let (rect, response) = ui.allocate_exact_size( 220 | desired_size, 221 | Sense::drag(), 222 | ); 223 | 224 | // 处理拖拽滚动 225 | if response.dragged() { 226 | let delta = response.drag_delta(); 227 | self.scroll_pos -= delta; 228 | } 229 | 230 | // 居中显示图片 231 | let painter = ui.painter_at(rect); 232 | painter.image( 233 | texture.id(), 234 | rect, 235 | Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), 236 | Color32::WHITE, 237 | ); 238 | } 239 | } 240 | }); 241 | }); 242 | 243 | if self.show_file_analysis { 244 | if let Some(file_path) = &self.current_file_path { 245 | let viewport_id = ViewportId::from_hash_of("file_analysis"); 246 | let viewport = ViewportBuilder::default() 247 | .with_title("文件分析") 248 | .with_resizable(true) 249 | .with_inner_size([600.0, 400.0]) 250 | .with_decorations(true); 251 | 252 | let mut should_close = false; 253 | 254 | ctx.show_viewport_immediate( 255 | viewport_id, 256 | viewport, 257 | |ctx, _class| { 258 | CentralPanel::default().show(ctx, |ui| { 259 | if ctx.input(|i| i.viewport().close_requested()) { 260 | should_close = true; 261 | } 262 | 263 | let mut analysis = FileAnalysis::new(file_path); 264 | analysis.ui(ui); 265 | }); 266 | 267 | if should_close { 268 | ctx.send_viewport_cmd(ViewportCommand::Close); 269 | } 270 | }, 271 | ); 272 | 273 | if should_close { 274 | self.show_file_analysis = false; 275 | } 276 | 277 | } 278 | } 279 | 280 | if self.show_extract_dialog { 281 | if let Some(transform) = &self.transform { 282 | let viewport_id = ViewportId::from_hash_of("extract_dialog"); 283 | let viewport = ViewportBuilder::default() 284 | .with_title("数据提取") 285 | .with_resizable(true) 286 | //自动调整大小 287 | .with_inner_size([810.0, 430.0]) 288 | .with_decorations(true); 289 | 290 | // 临时变量跟踪关闭状态 291 | let mut should_close = false; 292 | 293 | ctx.show_viewport_immediate( 294 | viewport_id, 295 | viewport, 296 | |ctx, _class| { 297 | CentralPanel::default().show(ctx, |ui| { 298 | // 检查视口关闭命令(来自系统按钮) 299 | if ctx.input(|i| i.viewport().close_requested()) { 300 | should_close = true; 301 | } 302 | 303 | // 正常绘制对话框内容 304 | if let Some(dialog) = self.extract_dialog.as_mut() { 305 | if dialog.ui(ui, transform.get_image()) { 306 | should_close = true; 307 | } 308 | } 309 | }); 310 | // 如果检测到关闭命令,执行关闭操作 311 | if should_close { 312 | ctx.send_viewport_cmd(ViewportCommand::Close); 313 | } 314 | }, 315 | ); 316 | 317 | // 同步关闭状态到主程序 318 | if should_close { 319 | self.show_extract_dialog = false; 320 | } 321 | } 322 | } 323 | 324 | 325 | 326 | if self.show_stereo_dialog { 327 | if let Some(_transform) = &self.transform { 328 | let viewport_id = ViewportId::from_hash_of("stereo_dialog"); 329 | let viewport = ViewportBuilder::default() 330 | .with_title("立体图分析") 331 | .with_resizable(true) 332 | .with_inner_size([800.0, 600.0]) 333 | .with_decorations(true); 334 | 335 | let mut should_close = false; 336 | 337 | ctx.show_viewport_immediate( 338 | viewport_id, 339 | viewport, 340 | |ctx, _class| { 341 | CentralPanel::default().show(ctx, |ui| { 342 | if ctx.input(|i| i.viewport().close_requested()) { 343 | should_close = true; 344 | } 345 | 346 | if let Some(stereo) = self.stereo.as_mut() { 347 | stereo.update(ctx, ui); 348 | } 349 | }); 350 | 351 | if should_close { 352 | ctx.send_viewport_cmd(ViewportCommand::Close); 353 | } 354 | }, 355 | ); 356 | 357 | if should_close { 358 | self.show_stereo_dialog = false; 359 | } 360 | } 361 | 362 | } 363 | 364 | if self.show_frame_browser { 365 | if let Some(browser) = &mut self.frame_browser { 366 | let viewport_id = ViewportId::from_hash_of("frame_browser"); 367 | let viewport = ViewportBuilder::default() 368 | .with_title("帧浏览器") 369 | .with_resizable(true) 370 | .with_inner_size([800.0, 600.0]) 371 | .with_decorations(true); 372 | 373 | let mut should_close = false; 374 | 375 | ctx.show_viewport_immediate( 376 | viewport_id, 377 | viewport, 378 | |ctx, _class| { 379 | CentralPanel::default().show(ctx, |ui| { 380 | if ctx.input(|i| i.viewport().close_requested()) { 381 | should_close = true; 382 | } 383 | 384 | browser.ui(ui); 385 | }); 386 | 387 | if should_close { 388 | ctx.send_viewport_cmd(ViewportCommand::Close); 389 | } 390 | }, 391 | ); 392 | 393 | if should_close { 394 | self.show_frame_browser = false; 395 | } 396 | } 397 | } 398 | 399 | if self.show_combine_dialog { 400 | let viewport_id = ViewportId::from_hash_of("combine_dialog"); 401 | let viewport = ViewportBuilder::default() 402 | .with_title("图像合成器") 403 | .with_resizable(true) 404 | .with_inner_size([800.0, 600.0]) 405 | .with_decorations(true); 406 | 407 | let mut should_close = false; 408 | 409 | ctx.show_viewport_immediate( 410 | viewport_id, 411 | viewport, 412 | |ctx, _class| { 413 | CentralPanel::default().show(ctx, |ui| { 414 | if ctx.input(|i| i.viewport().close_requested()) { 415 | should_close = true; 416 | } 417 | 418 | if let Some(combiner) = &mut self.combine_dialog { 419 | combiner.update(ui); 420 | } 421 | }); 422 | 423 | if should_close { 424 | ctx.send_viewport_cmd(ViewportCommand::Close); 425 | } 426 | }, 427 | ); 428 | 429 | if should_close { 430 | // 在关闭窗口时重置状态 431 | if let Some(combiner) = &mut self.combine_dialog { 432 | combiner.reset(); 433 | } 434 | self.show_combine_dialog = false; 435 | } 436 | } 437 | 438 | if self.show_about { 439 | Window::new("关于") 440 | .open(&mut self.show_about) 441 | .movable(true) 442 | .show(ctx, |ui| { 443 | ui.label("StegSolve (Rust + Egui)"); 444 | ui.label("版本: 0.2.0"); 445 | ui.label("作者: Sn1waR"); 446 | }); 447 | } 448 | 449 | 450 | 451 | TopBottomPanel::bottom("controls").show(ctx, |ui| { 452 | ui.horizontal(|ui| { 453 | // 处理鼠标滚轮和上下键 454 | let mut zoom_delta = 0.0; 455 | 456 | if let Some(scroll_delta) = ctx.input(|i| { 457 | if i.pointer.hover_pos().is_some() { 458 | Some(i.raw_scroll_delta.y) 459 | } else { 460 | None 461 | } 462 | }) { 463 | zoom_delta = scroll_delta * 0.001; 464 | } 465 | 466 | // 处理上下键 467 | ctx.input(|i| { 468 | if i.key_down(egui::Key::ArrowUp) { 469 | zoom_delta += 0.02; 470 | } 471 | if i.key_down(egui::Key::ArrowDown) { 472 | zoom_delta -= 0.02; 473 | } 474 | }); 475 | 476 | self.zoom_level = (self.zoom_level + zoom_delta).clamp(0.1, 5.0); 477 | 478 | // 缩放控制 479 | ui.add(Slider::new(&mut self.zoom_level, 0.1..=5.0).text("缩放")); 480 | 481 | // 导航按钮 482 | ui.separator(); 483 | 484 | // 检查键盘输入 485 | let left_key_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)); 486 | let right_key_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowRight)); 487 | 488 | let left_button = egui::Button::new("<") 489 | .fill(if left_key_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 490 | let right_button = egui::Button::new(">") 491 | .fill(if right_key_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 492 | 493 | let left_clicked = ui.add(left_button).clicked(); 494 | let right_clicked = ui.add(right_button).clicked(); 495 | 496 | if let Some(transform) = &self.transform { 497 | ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { 498 | ui.set_min_width(200.0); 499 | ui.label(format!("通道: {}", transform.get_text())); 500 | }); 501 | } 502 | 503 | if left_clicked || left_key_pressed { 504 | if let Some(transform) = &mut self.transform { 505 | transform.back(); 506 | self.texture = None; 507 | self.current_channel_text = transform.get_text(); 508 | } 509 | } 510 | 511 | if right_clicked || right_key_pressed { 512 | if let Some(transform) = &mut self.transform { 513 | transform.forward(); 514 | self.texture = None; 515 | self.current_channel_text = transform.get_text(); 516 | } 517 | } 518 | 519 | // 文件操作 520 | ui.separator(); 521 | if ui.button("打开").clicked() { 522 | if let Some(path) = rfd::FileDialog::new().pick_file() { 523 | self.open_image(&path); 524 | } 525 | } 526 | if ui.button("另存为").clicked() { 527 | if let Some(transform) = &self.transform { 528 | if let Some(path) = rfd::FileDialog::new().save_file() { 529 | let img = transform.get_image(); 530 | img.save(path).unwrap(); 531 | } 532 | } 533 | } 534 | }); 535 | }); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/stereo.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | use image::{GenericImageView, RgbaImage}; 3 | use rfd::FileDialog; 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | 7 | pub struct StereoTransform { 8 | original_image: RgbaImage, 9 | transform: RgbaImage, 10 | trans_num: i32, 11 | } 12 | 13 | impl StereoTransform { 14 | pub fn new(img: RgbaImage) -> Self { 15 | let mut st = Self { 16 | original_image: img.clone(), 17 | transform: RgbaImage::new(img.width(), img.height()), 18 | trans_num: 0, 19 | }; 20 | st.calc_trans(); 21 | st 22 | } 23 | 24 | fn calc_trans(&mut self) { 25 | let width = self.original_image.width() as i32; 26 | let height = self.original_image.height() as i32; 27 | 28 | self.transform = RgbaImage::new(width as u32, height as u32); // Recreate the transform image 29 | 30 | for i in 0..width { 31 | for j in 0..height { 32 | let fcol = self.original_image.get_pixel(i as u32, j as u32); 33 | let offset = ((i + self.trans_num).rem_euclid(width)) as u32; 34 | let ocol = self.original_image.get_pixel(offset, j as u32); 35 | 36 | let new_pixel = 37 | image::Rgba([fcol[0] ^ ocol[0], fcol[1] ^ ocol[1], fcol[2] ^ ocol[2], 255]); 38 | 39 | self.transform.put_pixel(i as u32, j as u32, new_pixel); 40 | } 41 | } 42 | } 43 | 44 | pub fn back(&mut self) { 45 | self.trans_num -= 1; 46 | if self.trans_num < 0 { 47 | self.trans_num = self.original_image.width() as i32 - 1; 48 | } 49 | println!("Back pressed: trans_num = {}", self.trans_num); 50 | self.calc_trans(); 51 | } 52 | 53 | pub fn forward(&mut self) { 54 | self.trans_num += 1; 55 | if self.trans_num >= self.original_image.width() as i32 { 56 | self.trans_num = 0; 57 | } 58 | println!("Forward pressed: trans_num = {}", self.trans_num); 59 | self.calc_trans(); 60 | } 61 | 62 | pub fn get_text(&self) -> String { 63 | format!("偏移量: {}", self.trans_num) 64 | } 65 | 66 | pub fn get_image(&self) -> &RgbaImage { 67 | &self.transform 68 | } 69 | } 70 | 71 | pub struct Stereo { 72 | transform: Rc>, // RefCell stores a mutable reference to the StereoTransform 73 | texture: Option, 74 | } 75 | 76 | impl Stereo { 77 | pub fn new(img: RgbaImage) -> Self { 78 | Self { 79 | transform: Rc::new(RefCell::new(StereoTransform::new(img))), 80 | texture: None, 81 | } 82 | } 83 | 84 | fn update_texture(&mut self, ui: &mut egui::Ui) { 85 | let transform_borrow = self.transform.borrow(); 86 | let image = transform_borrow.get_image(); 87 | 88 | let size = [image.width() as usize, image.height() as usize]; 89 | let pixels: Vec = image 90 | .as_raw() 91 | .chunks_exact(4) 92 | .map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) 93 | .collect(); 94 | 95 | let texture_id = "stereo-image"; // Define texture_id outside if let 96 | 97 | let texture = self.texture.get_or_insert_with(|| { 98 | ui.ctx().load_texture( 99 | texture_id, 100 | egui::ColorImage { 101 | size, 102 | pixels: pixels.clone(), 103 | }, 104 | egui::TextureOptions::default(), 105 | ) 106 | }); 107 | 108 | texture.set( 109 | egui::ColorImage { size, pixels }, 110 | egui::TextureOptions::default(), 111 | ); 112 | } 113 | 114 | pub fn update(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { 115 | // 检查键盘输入 116 | let left_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)); 117 | let right_pressed = ctx.input(|i| i.key_pressed(egui::Key::ArrowRight)); 118 | 119 | if left_pressed { 120 | self.transform.borrow_mut().back(); 121 | self.update_texture(ui); 122 | } 123 | if right_pressed { 124 | self.transform.borrow_mut().forward(); 125 | self.update_texture(ui); 126 | } 127 | 128 | ui.vertical(|ui| { 129 | 130 | let text = { 131 | let transform_borrow = self.transform.borrow(); 132 | transform_borrow.get_text() 133 | }; 134 | ui.label(text); 135 | 136 | self.update_texture(ui); 137 | if let Some(texture) = &self.texture { 138 | let size = texture.size_vec2(); 139 | ui.image(texture); 140 | } 141 | 142 | ui.horizontal(|ui| { 143 | let transform_rc = self.transform.clone(); 144 | let left_button = egui::Button::new("◀") 145 | .fill(if left_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 146 | if ui.add(left_button).clicked() { 147 | transform_rc.borrow_mut().back(); 148 | self.update_texture(ui); 149 | } 150 | 151 | let right_button = egui::Button::new("▶") 152 | .fill(if right_pressed { ui.style().visuals.selection.bg_fill } else { ui.style().visuals.widgets.inactive.bg_fill }); 153 | if ui.add(right_button).clicked() { 154 | transform_rc.borrow_mut().forward(); 155 | self.update_texture(ui); 156 | } 157 | if ui.button("保存").clicked() { 158 | if let Some(path) = FileDialog::new() 159 | .add_filter("图片", &["png", "jpg", "jpeg", "bmp"]) 160 | .set_file_name("solved.png") 161 | .save_file() 162 | { 163 | let borrowed_transform = transform_rc.borrow(); 164 | let image = borrowed_transform.get_image(); 165 | save_rgba_image(image, path); 166 | } 167 | } 168 | }); 169 | }); 170 | } 171 | } 172 | 173 | pub fn save_rgba_image(img: &image::RgbaImage, path: std::path::PathBuf) { 174 | let ext = path 175 | .extension() 176 | .and_then(|s| s.to_str()) 177 | .map(|s| s.to_lowercase()) 178 | .unwrap_or_else(|| "png".to_string()); 179 | 180 | let format = match ext.as_str() { 181 | "png" => image::ImageFormat::Png, 182 | "jpg" | "jpeg" => image::ImageFormat::Jpeg, 183 | "bmp" => image::ImageFormat::Bmp, 184 | _ => image::ImageFormat::Png, 185 | }; 186 | 187 | if let Err(e) = img.save_with_format(&path, format) { 188 | eprintln!("保存失败: {}", e); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/transform.rs: -------------------------------------------------------------------------------- 1 | use image::{DynamicImage, RgbaImage, Rgba}; 2 | use rand::Rng; 3 | 4 | pub struct Transform { 5 | original_image: RgbaImage, // 原始图像 6 | transformed_image: RgbaImage,// 变换后的图像 7 | trans_num: i32, // 当前变换编号 8 | max_trans: i32, // 最大变换编号 9 | } 10 | 11 | impl Transform { 12 | pub fn new(img: DynamicImage) -> Self { 13 | let rgba_img = img.to_rgba8(); // 将图像转换为 RgbaImage 14 | Self { 15 | original_image: rgba_img.clone(), 16 | transformed_image: rgba_img, 17 | trans_num: 0, 18 | max_trans: 41, // 最大变换编号 19 | } 20 | } 21 | 22 | // 获取当前变换后的图像 23 | pub fn get_image(&self) -> &RgbaImage { 24 | &self.transformed_image 25 | } 26 | 27 | // 获取当前变换的描述文本(与原版 StegSolve 对应) 28 | pub fn get_text(&self) -> String { 29 | match self.trans_num { 30 | 0 => "正常图像".to_string(), 31 | 1 => "颜色反转 (Xor)".to_string(), 32 | 2..=9 => format!("Alpha plane {}", 9 - self.trans_num), 33 | 10..=17 => format!("Red plane {}", 17 - self.trans_num), 34 | 18..=25 => format!("Green plane {}", 25 - self.trans_num), 35 | 26..=33 => format!("Blue plane {}", 33 - self.trans_num), 36 | 34 => "Full alpha".to_string(), 37 | 35 => "Full red".to_string(), 38 | 36 => "Full green".to_string(), 39 | 37 => "Full blue".to_string(), 40 | 38 => "Random colour map 1".to_string(), 41 | 39 => "Random colour map 2".to_string(), 42 | 40 => "Random colour map 3".to_string(), 43 | 41 => "灰度".to_string(), 44 | _ => "".to_string(), 45 | } 46 | } 47 | 48 | // 切换到上一个变换 49 | pub fn back(&mut self) { 50 | self.trans_num -= 1; 51 | if self.trans_num < 0 { 52 | self.trans_num = self.max_trans; 53 | } 54 | self.calc_trans(); 55 | } 56 | 57 | // 切换到下一个变换 58 | pub fn forward(&mut self) { 59 | self.trans_num += 1; 60 | if self.trans_num > self.max_trans { 61 | self.trans_num = 0; 62 | } 63 | self.calc_trans(); 64 | } 65 | 66 | // 反转颜色 (类似 Java 里的 col ^ 0xffffff) 67 | fn inversion(&mut self) { 68 | let img = &self.original_image; 69 | let mut new_img = RgbaImage::new(img.width(), img.height()); 70 | for (x, y, pixel) in img.enumerate_pixels() { 71 | // 直接对 RGB 做 255 - value; alpha 保持不变或设为255 72 | let new_pixel = Rgba([ 73 | 255 - pixel[0], 74 | 255 - pixel[1], 75 | 255 - pixel[2], 76 | 255, // 与 Java TYPE_INT_RGB 一致,不用原 alpha 77 | ]); 78 | new_img.put_pixel(x, y, new_pixel); 79 | } 80 | self.transformed_image = new_img; 81 | } 82 | 83 | // ===== 关键修改:transform_bit 按照 Java 的 ARGB 做位平面提取 ===== 84 | fn transform_bit(&mut self, bit: i32) { 85 | let img = &self.original_image; 86 | let (width, height) = (img.width(), img.height()); 87 | let mut new_img = RgbaImage::new(width, height); 88 | 89 | for (x, y, pixel) in img.enumerate_pixels() { 90 | // **务必确认 pixel[..] 的含义是真实 RGBA,别拿反顺序** 91 | let fcol = ((pixel[3] as u32) << 24) // A 92 | | ((pixel[0] as u32) << 16) // R 93 | | ((pixel[1] as u32) << 8) // G 94 | | (pixel[2] as u32); // B 95 | 96 | // Java 相当于: if(((fcol >>> bit) & 1) > 0) col=0xffffff else 0 97 | let col = if ((fcol >> bit) & 1) == 1 { 98 | 0xffffff 99 | } else { 100 | 0x000000 101 | }; 102 | 103 | // 写回时,只要保留 RGB,A=255 (StegSolve 的 TYPE_INT_RGB 常用做法) 104 | let r = (col >> 16) as u8; 105 | let g = (col >> 8) as u8; 106 | let b = (col & 0xff) as u8; 107 | new_img.put_pixel(x, y, Rgba([r, g, b, 255])); 108 | } 109 | 110 | self.transformed_image = new_img; 111 | } 112 | 113 | // ===== 关键修改:transform_mask 与原版 Java transmask(int mask) 对齐 ===== 114 | fn transform_mask(&mut self, mask: u32) { 115 | let img = &self.original_image; 116 | let mut new_img = RgbaImage::new(img.width(), img.height()); 117 | 118 | for (x, y, pixel) in img.enumerate_pixels() { 119 | let fcol = (255u32 << 24) // A=255 120 | | ((pixel[0] as u32) << 16) 121 | | ((pixel[1] as u32) << 8) 122 | | (pixel[2] as u32); 123 | 124 | let mut col = fcol & mask; 125 | if col > 0xffffff { 126 | col >>= 8; // 与 Java 的 col >>> 8 对齐 127 | } 128 | 129 | let r = (col >> 16) as u8; 130 | let g = (col >> 8) as u8; 131 | let b = (col & 0xff) as u8; 132 | new_img.put_pixel(x, y, Rgba([r, g, b, 255])); 133 | } 134 | 135 | self.transformed_image = new_img; 136 | } 137 | 138 | fn random_colormap(&mut self) { 139 | let img = &self.original_image; 140 | let mut new_img = RgbaImage::new(img.width(), img.height()); 141 | let mut rng = rand::thread_rng(); 142 | 143 | // 生成随机系数和偏移量 144 | let bm = rng.gen_range(0..256) as u32; 145 | let ba = rng.gen_range(0..256) as u32; 146 | let bx = rng.gen_range(0..256) as u32; 147 | let gm = rng.gen_range(0..256) as u32; 148 | let ga = rng.gen_range(0..256) as u32; 149 | let gx = rng.gen_range(0..256) as u32; 150 | let rm = rng.gen_range(0..256) as u32; 151 | let ra = rng.gen_range(0..256) as u32; 152 | let rx = rng.gen_range(0..256) as u32; 153 | 154 | for (x, y, pixel) in img.enumerate_pixels() { 155 | let b = ((pixel[0] as u32 * bm) ^ bx) + ba; 156 | let g = ((pixel[1] as u32 * gm) ^ gx) + ga; 157 | let r = ((pixel[2] as u32 * rm) ^ rx) + ra; 158 | 159 | // 确保颜色值在 0-255 范围内 160 | let b = (b & 0xff) as u8; 161 | let g = (g & 0xff) as u8; 162 | let r = (r & 0xff) as u8; 163 | 164 | new_img.put_pixel(x, y, Rgba([r, g, b, 255])); 165 | } 166 | 167 | self.transformed_image = new_img; 168 | } 169 | 170 | // 随机颜色映射(适用于 IndexColorModel) 171 | fn random_indexmap(&mut self) { 172 | // 由于 Rust 的 image crate 不支持直接操作调色板, 173 | // 我们可以假设图像是 RGBA 格式,并直接应用随机颜色映射。 174 | // 如果需要支持调色板图像,可以考虑使用其他库或手动处理调色板。 175 | self.random_colormap(); 176 | } 177 | 178 | // 根据图像类型选择随机映射方式 179 | fn random_map(&mut self) { 180 | // 假设图像是 RGBA 格式(ComponentColorModel) 181 | self.random_colormap(); 182 | } 183 | 184 | // 灰度高亮 (r = g = b时显示白色,否则黑色) 185 | fn gray_bits(&mut self) { 186 | let img = &self.original_image; 187 | let mut new_img = RgbaImage::new(img.width(), img.height()); 188 | for (x, y, pixel) in img.enumerate_pixels() { 189 | if pixel[0] == pixel[1] && pixel[0] == pixel[2] { 190 | new_img.put_pixel(x, y, Rgba([255, 255, 255, 255])); 191 | } else { 192 | new_img.put_pixel(x, y, Rgba([0, 0, 0, 255])); 193 | } 194 | } 195 | self.transformed_image = new_img; 196 | } 197 | 198 | // 根据当前 trans_num 计算变换结果 199 | fn calc_trans(&mut self) { 200 | match self.trans_num { 201 | 0 => { 202 | self.transformed_image = self.original_image.clone(); 203 | } 204 | 1 => { 205 | self.inversion(); 206 | } 207 | 2 => { 208 | // Alpha plane 7 209 | self.transform_bit(31); 210 | } 211 | 3 => { 212 | // Alpha plane 6 213 | self.transform_bit(30); 214 | } 215 | 4 => { 216 | // Alpha plane 5 217 | self.transform_bit(29); 218 | } 219 | 5 => { 220 | // Alpha plane 4 221 | self.transform_bit(28); 222 | } 223 | 6 => { 224 | // Alpha plane 3 225 | self.transform_bit(27); 226 | } 227 | 7 => { 228 | // Alpha plane 2 229 | self.transform_bit(26); 230 | } 231 | 8 => { 232 | // Alpha plane 1 233 | self.transform_bit(25); 234 | } 235 | 9 => { 236 | // Alpha plane 0 237 | self.transform_bit(24); 238 | } 239 | 10 => { 240 | // Red plane 7 241 | self.transform_bit(23); 242 | } 243 | 11 => { 244 | // Red plane 6 245 | self.transform_bit(22); 246 | } 247 | 12 => { 248 | // Red plane 5 249 | self.transform_bit(21); 250 | } 251 | 13 => { 252 | // Red plane 4 253 | self.transform_bit(20); 254 | } 255 | 14 => { 256 | // Red plane 3 257 | self.transform_bit(19); 258 | } 259 | 15 => { 260 | // Red plane 2 261 | self.transform_bit(18); 262 | } 263 | 16 => { 264 | // Red plane 1 265 | self.transform_bit(17); 266 | } 267 | 17 => { 268 | // Red plane 0 269 | self.transform_bit(16); 270 | } 271 | 18 => { 272 | // Green plane 7 273 | self.transform_bit(15); 274 | } 275 | 19 => { 276 | // Green plane 6 277 | self.transform_bit(14); 278 | } 279 | 20 => { 280 | // Green plane 5 281 | self.transform_bit(13); 282 | } 283 | 21 => { 284 | // Green plane 4 285 | self.transform_bit(12); 286 | } 287 | 22 => { 288 | // Green plane 3 289 | self.transform_bit(11); 290 | } 291 | 23 => { 292 | // Green plane 2 293 | self.transform_bit(10); 294 | } 295 | 24 => { 296 | // Green plane 1 297 | self.transform_bit(9); 298 | } 299 | 25 => { 300 | // Green plane 0 301 | self.transform_bit(8); 302 | } 303 | 26 => { 304 | // Blue plane 7 305 | self.transform_bit(7); 306 | } 307 | 27 => { 308 | // Blue plane 6 309 | self.transform_bit(6); 310 | } 311 | 28 => { 312 | // Blue plane 5 313 | self.transform_bit(5); 314 | } 315 | 29 => { 316 | // Blue plane 4 317 | self.transform_bit(4); 318 | } 319 | 30 => { 320 | // Blue plane 3 321 | self.transform_bit(3); 322 | } 323 | 31 => { 324 | // Blue plane 2 325 | self.transform_bit(2); 326 | } 327 | 32 => { 328 | // Blue plane 1 329 | self.transform_bit(1); 330 | } 331 | 33 => { 332 | // Blue plane 0 333 | self.transform_bit(0); 334 | } 335 | 34 => { 336 | // Full alpha 337 | self.transform_mask(0xff000000); 338 | } 339 | 35 => { 340 | // Full red 341 | self.transform_mask(0x00ff0000); 342 | } 343 | 36 => { 344 | // Full green 345 | self.transform_mask(0x0000ff00); 346 | } 347 | 37 => { 348 | // Full blue 349 | self.transform_mask(0x000000ff); 350 | } 351 | 38..=40 => { 352 | self.random_map(); 353 | } 354 | 41 => { 355 | self.gray_bits(); 356 | } 357 | _ => { 358 | self.transformed_image = self.original_image.clone(); 359 | } 360 | 361 | } 362 | } 363 | } --------------------------------------------------------------------------------