Initial commit: RustyImage M0-M2
Scientific image viewer/processor built with Tauri + React + Rust.
M0: Workspace setup, Tauri window, PNG display via ritile:// protocol
M1a: Multi-image tabs, zoom/pan, brightness/contrast, native file dialog,
keyboard shortcuts, status bar
M1b: RGB/multichannel composite display with per-channel LUTs,
visibility toggles, additive blending
M2: Pixel inspector, histogram/statistics panel, image processing
operations (invert, gaussian blur, median filter, threshold,
histogram equalization), Z/T stack navigation support
Architecture: 14-crate Cargo workspace (ri-types, ri-core, ri-io,
ri-lut, ri-render, ri-store, ri-ops, ri-measure, ri-ipc, ri-app,
plus phase 2-4 placeholders). React frontend with hooks-based state
management and component architecture.
39 tests, all passing.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
use std::path::Path;
|
||||
|
||||
use image::{ColorType, GenericImageView};
|
||||
use ri_core::{Hyperstack, TypedBuffer};
|
||||
use ri_types::{ImageId, ImageMeta, PixelType, RiError};
|
||||
|
||||
/// Load an image file into a Hyperstack.
|
||||
///
|
||||
/// Preserves channel structure: grayscale → 1ch, RGB → 3ch, RGBA → 4ch.
|
||||
/// Supports PNG, JPEG, TIFF, BMP, GIF via the `image` crate.
|
||||
pub fn load_image(path: &Path) -> Result<Hyperstack, RiError> {
|
||||
let img = image::open(path).map_err(|e| RiError::ImageDecode(e.to_string()))?;
|
||||
let file_size_bytes = std::fs::metadata(path).map(|m| m.len()).ok();
|
||||
let (w, h) = img.dimensions();
|
||||
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
match img.color() {
|
||||
ColorType::Rgb8 | ColorType::Rgb16 | ColorType::Rgb32F => {
|
||||
let rgb = img.to_rgb8();
|
||||
let raw = rgb.into_raw();
|
||||
let npix = (w * h) as usize;
|
||||
let mut r = Vec::with_capacity(npix);
|
||||
let mut g = Vec::with_capacity(npix);
|
||||
let mut b = Vec::with_capacity(npix);
|
||||
for i in 0..npix {
|
||||
r.push(raw[i * 3]);
|
||||
g.push(raw[i * 3 + 1]);
|
||||
b.push(raw[i * 3 + 2]);
|
||||
}
|
||||
let meta = ImageMeta {
|
||||
id: ImageId::new(),
|
||||
name,
|
||||
width: w,
|
||||
height: h,
|
||||
channels: 3,
|
||||
slices: 1,
|
||||
frames: 1,
|
||||
pixel_type: PixelType::U8,
|
||||
file_size_bytes,
|
||||
};
|
||||
Ok(Hyperstack::from_channel_planes(
|
||||
meta,
|
||||
vec![
|
||||
TypedBuffer::U8(r),
|
||||
TypedBuffer::U8(g),
|
||||
TypedBuffer::U8(b),
|
||||
],
|
||||
))
|
||||
}
|
||||
ColorType::Rgba8 | ColorType::Rgba16 | ColorType::Rgba32F => {
|
||||
let rgba = img.to_rgba8();
|
||||
let raw = rgba.into_raw();
|
||||
let npix = (w * h) as usize;
|
||||
let mut r = Vec::with_capacity(npix);
|
||||
let mut g = Vec::with_capacity(npix);
|
||||
let mut b = Vec::with_capacity(npix);
|
||||
let mut a = Vec::with_capacity(npix);
|
||||
for i in 0..npix {
|
||||
r.push(raw[i * 4]);
|
||||
g.push(raw[i * 4 + 1]);
|
||||
b.push(raw[i * 4 + 2]);
|
||||
a.push(raw[i * 4 + 3]);
|
||||
}
|
||||
let meta = ImageMeta {
|
||||
id: ImageId::new(),
|
||||
name,
|
||||
width: w,
|
||||
height: h,
|
||||
channels: 4,
|
||||
slices: 1,
|
||||
frames: 1,
|
||||
pixel_type: PixelType::U8,
|
||||
file_size_bytes,
|
||||
};
|
||||
Ok(Hyperstack::from_channel_planes(
|
||||
meta,
|
||||
vec![
|
||||
TypedBuffer::U8(r),
|
||||
TypedBuffer::U8(g),
|
||||
TypedBuffer::U8(b),
|
||||
TypedBuffer::U8(a),
|
||||
],
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
// Grayscale and other formats → single channel
|
||||
let gray = img.to_luma8();
|
||||
let pixels = gray.into_raw();
|
||||
let meta = ImageMeta {
|
||||
id: ImageId::new(),
|
||||
name,
|
||||
width: w,
|
||||
height: h,
|
||||
channels: 1,
|
||||
slices: 1,
|
||||
frames: 1,
|
||||
pixel_type: PixelType::U8,
|
||||
file_size_bytes,
|
||||
};
|
||||
Ok(Hyperstack::from_single_plane(
|
||||
meta,
|
||||
TypedBuffer::U8(pixels),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn load_nonexistent_file_returns_error() {
|
||||
let result = load_image(Path::new("/nonexistent/file.png"));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user