diff --git a/Cargo.lock b/Cargo.lock index 25cf968..cd0b3f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -644,6 +644,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1433,6 +1442,7 @@ dependencies = [ "bytemuck", "clap 4.5.34", "criterion", + "crossbeam-channel", "egui", "egui-wgpu", "egui-winit", @@ -1441,10 +1451,12 @@ dependencies = [ "jpegxl-rs", "libheif-rs", "pollster", + "rayon", "rexiv2", "sha2", "threadpool", "winit", + "zerocopy 0.8.24", "zune-image", ] diff --git a/Cargo.toml b/Cargo.toml index 868f0f9..bc52acd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ rexiv2 = "0.10.0" threadpool = "1.8.1" bytemuck = "1.22.0" sha2 = "0.10.8" +zerocopy = "0.8.24" +crossbeam-channel = "0.5.15" +rayon = "1.10.0" [profile.release] opt-level = 3 diff --git a/src/app.rs b/src/app.rs index f117d52..ad73bb0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,16 @@ use crate::egui_tools::EguiRenderer; -use egui::{Event, Key, PointerButton}; +use egui::load::{ImageLoadResult, ImageLoader}; +use egui::{Align2, Color32, ColorImage, Event, ImageSource, Key, PointerButton}; use egui_wgpu::wgpu::SurfaceError; use egui_wgpu::{ScreenDescriptor, wgpu}; +use image::metadata::Orientation; +use imflow::image::swap_wh; use imflow::store::ImageStore; +use std::cmp::{max, min}; +use std::collections::HashMap; use std::path::PathBuf; use std::process::exit; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use wgpu::util::DeviceExt; use wgpu::{PipelineCompilationOptions, SurfaceConfiguration}; use winit::application::ApplicationHandler; @@ -15,14 +20,13 @@ use winit::event_loop::ActiveEventLoop; use winit::platform::x11::WindowAttributesExtX11; use winit::window::{Window, WindowId}; -// Uniforms for transformations #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct Transforms { transform: [f32; 16], // 4x4 matrix width: u32, height: u32, - _padding1: u32, + orientation: u32, _padding2: u32, } @@ -32,6 +36,7 @@ pub(crate) struct TransformData { zoom: f32, width: u32, height: u32, + orientation: Orientation, } #[rustfmt::skip] @@ -212,7 +217,7 @@ pub struct AppState { pub surface: wgpu::Surface<'static>, pub scale_factor: f32, pub egui_renderer: EguiRenderer, - pub store: ImageStore, + pub store: Arc>, pub image_texture: wgpu::Texture, pub bind_group: wgpu::BindGroup, pub render_pipeline: wgpu::RenderPipeline, @@ -276,9 +281,13 @@ impl AppState { let egui_renderer = EguiRenderer::new(&device, surface_config.format, None, 1, window); - let scale_factor = 1.0; + let store = Arc::new(RwLock::new(ImageStore::new(path))); - let store = ImageStore::new(path); + let loader = ImflowEguiLoader::new(store.clone()); + + egui_renderer.context().add_image_loader(Arc::new(loader)); + + let scale_factor = 1.0; let (image_texture, bind_group, render_pipeline, transform_buffer) = // setup_texture(&device, surface_config.clone(), 6000, 4000); @@ -290,6 +299,7 @@ impl AppState { zoom: 1.0, width: 10000, height: 10000, + orientation: Orientation::NoTransforms, }; Self { @@ -313,6 +323,10 @@ impl AppState { self.surface_config.height = height; self.surface.configure(&self.device, &self.surface_config); } + + // fn get_store(&mut self) -> &ImageStore { + // &self.store.lock().unwrap() + // } } pub struct App { @@ -358,7 +372,7 @@ impl App { self.window.get_or_insert(window); self.state.get_or_insert(state); - self.pan_zoom(0.0, 0.0, 0.0); + self.reset_transform(); self.update_texture(); } @@ -372,52 +386,60 @@ impl App { pub fn update_texture(&mut self) { let state = self.state.as_mut().unwrap(); - state.store.check_loaded_images(); - let imbuf = if let Some(full) = state.store.get_current_image() { - full - } else { - state.store.get_thumbnail() - }; - let width = imbuf.width as u32; - let height = imbuf.height as u32; - let buffer_u8 = unsafe { - std::slice::from_raw_parts( - imbuf.rgba_buffer.as_ptr() as *const u8, - imbuf.rgba_buffer.len() * 4, - ) - }; + { + let mut store = state.store.write().unwrap(); + store.check_loaded_images(); + let imbuf = if let Some(full) = store.get_current_image() { + full + } else { + store.get_thumbnail() + }; + let width = imbuf.width as u32; + let height = imbuf.height as u32; + let buffer_u8 = unsafe { + std::slice::from_raw_parts( + imbuf.rgba_buffer.as_ptr() as *const u8, + imbuf.rgba_buffer.len() * 4, + ) + }; - state.transform_data.width = width; - state.transform_data.height = height; + state.transform_data.width = width; + state.transform_data.height = height; + state.transform_data.orientation = imbuf.orientation; - state.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &state.image_texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &buffer_u8, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(4 * width), // 4 bytes per ARGB pixel - rows_per_image: Some(height), - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); + state.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &state.image_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &buffer_u8, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width), // 4 bytes per ARGB pixel + rows_per_image: Some(height), + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + } - self.pan_zoom(0.0, 0.0, 0.0); + self.update_transform(); } fn update_transform(&mut self) { let state = self.state.as_mut().unwrap(); - let image_aspect_ratio = - (state.transform_data.width as f32) / (state.transform_data.height as f32); + let (width, height) = swap_wh( + state.transform_data.width, + state.transform_data.height, + state.transform_data.orientation, + ); + let image_aspect_ratio = (width as f32) / (height as f32); let window_size = self.window.as_ref().unwrap().inner_size(); let window_aspect_ratio = window_size.width as f32 / window_size.height as f32; let mut scale_x = 1.0; @@ -433,9 +455,9 @@ impl App { 0, bytemuck::cast_slice(&[Transforms { transform, - width: state.transform_data.width, - height: state.transform_data.height, - _padding1: 0, + width: width as u32, + height: height as u32, + orientation: state.transform_data.orientation as u32, _padding2: 0, }]), ); @@ -603,10 +625,21 @@ impl App { render_pass.draw_indexed(0..6, 0, 0..1); } - let rating = state.store.get_current_rating(); - let path = state.store.current_image_path.clone(); - let filename = path.path.file_name().unwrap(); - let window = self.window.as_ref().unwrap(); + let rating; + let path; + let current_id; + let image_count; + let filename; + let window; + { + let store = state.store.read().unwrap(); + rating = store.get_current_rating(); + path = store.current_image_path.clone(); + current_id = store.current_image_id; + image_count = store.available_images.len(); + filename = path.path.file_name().unwrap(); + window = self.window.as_ref().unwrap(); + } { state.egui_renderer.begin_frame(window); @@ -617,7 +650,7 @@ impl App { .show(state.egui_renderer.context(), |ui| { ui.vertical_centered(|ui| { ui.label( - egui::RichText::new(format!("{:.1}", rating)) + egui::RichText::new(format!("{}", rating)) .size(42.0) .strong(), ); @@ -628,6 +661,59 @@ impl App { ); }); }); + egui::Window::new("Id") + .collapsible(false) + .resizable(false) + .default_width(5.0) + .anchor(Align2::RIGHT_TOP, [-5.0, 5.0]) + .pivot(Align2::RIGHT_TOP) + .show(state.egui_renderer.context(), |ui| { + ui.vertical_centered(|ui| { + ui.label( + egui::RichText::new(format!("{}/{}", current_id, image_count)) + .size(22.0) + .strong(), + ); + }); + }); + + egui::Window::new("Images") + .collapsible(false) + .resizable(false) + .default_width(500.0) + .default_height(300.0) + .anchor(Align2::CENTER_BOTTOM, [0.0, 10.0]) + .pivot(Align2::CENTER_BOTTOM) + .show(state.egui_renderer.context(), |ui| { + ui.horizontal(|ui| { + // ui.label( + // egui::RichText::new(format!("{}/{}", current_id, image_count)) + // .size(22.0) + // .strong(), + // ); + + const NUM: i32 = 5; + for i in max((current_id as i32) - NUM, 0) + ..min((current_id as i32) + NUM + 1, image_count as i32) + { + let source = ImageSource::Bytes { + uri: std::borrow::Cow::Owned(i.to_string()), + bytes: egui::load::Bytes::Static(&[]), + }; + + ui.add( + egui::Image::new(source) + // .sca + // .load_for_size(ctx, available_size) + // .fit_to_fraction(Vec2::new(10.0, 10.0)) + // .max_width(200.0) + .fit_to_original_size(1.0) + .corner_radius(10), + ); + } + // ui.image(source); + }); + }); state.egui_renderer.end_frame_and_draw( &state.device, @@ -668,64 +754,80 @@ impl ApplicationHandler for App { } WindowEvent::RedrawRequested => { self.handle_redraw(); - let (events, _keys_down, pointer) = self + let (events, _keys_down, pointer, scroll) = self .state .as_ref() .unwrap() .egui_renderer .context() - .input(|i| (i.events.clone(), i.keys_down.clone(), i.pointer.clone())); + .input(|i| { + ( + i.events.clone(), + i.keys_down.clone(), + i.pointer.clone(), + i.smooth_scroll_delta.clone(), + ) + }); - events.iter().for_each(|e| { - if let Event::Key { key, pressed, .. } = e { - if !*pressed { - return; + let mut updated_image = false; + let mut reset_transform = false; + { + let mut store = self.state.as_mut().unwrap().store.write().unwrap(); + events.iter().for_each(|e| { + if let Event::Key { key, pressed, .. } = e { + if !*pressed { + return; + } + match *key { + Key::ArrowLeft => { + store.next_image(-1); + updated_image = true; + } + Key::ArrowRight => { + store.next_image(1); + updated_image = true; + } + Key::ArrowUp => { + let rating = store.get_current_rating(); + store.set_rating(rating + 1); + } + Key::ArrowDown => { + let rating = store.get_current_rating(); + store.set_rating(rating - 1); + } + Key::Backtick => store.set_rating(0), + Key::Num0 => store.set_rating(0), + Key::Num1 => store.set_rating(1), + Key::Num2 => store.set_rating(2), + Key::Num3 => store.set_rating(3), + Key::Num4 => store.set_rating(4), + Key::Num5 => store.set_rating(5), + Key::Escape => exit(0), + _ => {} + } + } else if let Event::PointerButton { + button, pressed, .. + } = e + { + if *pressed && *button == PointerButton::Secondary { + reset_transform = true; + } } - match *key { - Key::ArrowLeft => { - self.state.as_mut().unwrap().store.next_image(-1); - self.update_texture(); - } - Key::ArrowRight => { - self.state.as_mut().unwrap().store.next_image(1); - self.update_texture(); - } - Key::ArrowUp => { - let rating = - self.state.as_mut().unwrap().store.get_current_rating(); - self.state.as_mut().unwrap().store.set_rating(rating + 1); - } - Key::ArrowDown => { - let rating = - self.state.as_mut().unwrap().store.get_current_rating(); - self.state.as_mut().unwrap().store.set_rating(rating - 1); - } - Key::Backtick => self.state.as_mut().unwrap().store.set_rating(0), - Key::Num0 => self.state.as_mut().unwrap().store.set_rating(0), - Key::Num1 => self.state.as_mut().unwrap().store.set_rating(1), - Key::Num2 => self.state.as_mut().unwrap().store.set_rating(2), - Key::Num3 => self.state.as_mut().unwrap().store.set_rating(3), - Key::Num4 => self.state.as_mut().unwrap().store.set_rating(4), - Key::Num5 => self.state.as_mut().unwrap().store.set_rating(5), - Key::Escape => exit(0), - _ => {} - } - } else if let Event::MouseWheel { delta, .. } = e { - self.pan_zoom(delta.y * 0.2, 0.0, 0.0); - } else if let Event::PointerButton { - button, pressed, .. - } = e - { - if *pressed && *button == PointerButton::Secondary { - self.reset_transform(); - } - } - }); - + }); + } if pointer.primary_down() && pointer.is_moving() { self.pan_zoom(0.0, pointer.delta().x * 0.001, pointer.delta().y * -0.001); } + if scroll.y != 0.0 { + self.pan_zoom(scroll.y * 0.01, 0.0, 0.0); + } + if updated_image { + self.update_texture(); + } + if reset_transform { + self.reset_transform(); + } self.window.as_ref().unwrap().request_redraw(); } WindowEvent::Resized(new_size) => { @@ -735,3 +837,70 @@ impl ApplicationHandler for App { } } } + +pub struct ImflowEguiLoader { + store: Arc>, + // stored: Option, + cache: egui::mutex::Mutex>, +} + +impl ImflowEguiLoader { + pub fn new(store: Arc>) -> ImflowEguiLoader { + ImflowEguiLoader { + store, + cache: egui::mutex::Mutex::new(HashMap::new()), + } + } +} + +impl ImageLoader for ImflowEguiLoader { + fn id(&self) -> &str { + "ImflowEguiLoader" + } + + fn load( + &self, + _ctx: &egui::Context, + uri: &str, + _size_hint: egui::SizeHint, + ) -> egui::load::ImageLoadResult { + let mut cache = self.cache.lock(); + + let id = uri.parse::().unwrap(); + if let Some(handle) = cache.get(&id) { + handle.clone() + } else { + let imbuf = { + let binding = self.store.read().unwrap(); + binding.get_thumbnail_id(id).clone() + }; + let mut image = ColorImage::new([imbuf.width, imbuf.height], Color32::BLACK); + let image_buffer = image.as_raw_mut(); + for (i, &value) in imbuf.rgba_buffer.iter().enumerate() { + let bytes = value.to_le_bytes(); + let start = i * 4; + image_buffer[start..start + 4].copy_from_slice(&bytes); + } + + let res = ImageLoadResult::Ok(egui::load::ImagePoll::Ready { + image: Arc::new(ColorImage { + size: [imbuf.width, imbuf.height], + pixels: image.pixels, + }), + }); + cache.insert(id, res.clone()); + res.clone() + } + } + + // TODO + fn forget(&self, _uri: &str) {} + + // TODO + fn forget_all(&self) {} + + // TODO + fn byte_size(&self) -> usize { + todo!() + } +} diff --git a/src/image.rs b/src/image.rs index e291ffa..205f500 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,5 +1,6 @@ use image::DynamicImage; -use image::RgbaImage; +use image::ImageBuffer; +use image::Rgba; use image::imageops::FilterType; use image::metadata::Orientation; use itertools::Itertools; @@ -25,7 +26,6 @@ use std::io::BufReader; use std::io::Cursor; use std::io::Read; use std::io::Write; -use std::mem; use std::path::PathBuf; use std::str::FromStr; use std::time::Instant; @@ -79,29 +79,24 @@ pub struct ImflowImageBuffer { pub height: usize, pub rgba_buffer: Vec, pub rating: i32, + pub orientation: Orientation, } pub fn get_rating(image: &ImageData) -> i32 { - let meta = Metadata::new_from_path(&image.path); - match meta { - Ok(meta) => { - let rating = meta.get_tag_numeric("Xmp.xmp.Rating"); - rating - } - Err(e) => panic!("{:?}", e), + if let Ok(meta) = Metadata::new_from_path(&image.path) { + meta.get_tag_numeric("Xmp.xmp.Rating") + } else { + 0 } } pub fn get_orientation(path: &PathBuf) -> Orientation { - let meta = Metadata::new_from_path(path); - match meta { - Ok(meta) => Orientation::from_exif(meta.get_orientation() as u8) - .unwrap_or(Orientation::NoTransforms), - Err(_) => Orientation::NoTransforms, - } + Metadata::new_from_path(path).map_or(Orientation::NoTransforms, |meta| { + Orientation::from_exif(meta.get_orientation() as u8).unwrap() + }) } -fn swap_wh(width: T, height: T, orientation: Orientation) -> (T, T) { +pub fn swap_wh(width: T, height: T, orientation: Orientation) -> (T, T) { if [ Orientation::Rotate90, Orientation::Rotate270, @@ -161,15 +156,11 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer { let (metadata, buffer) = decoder.decode_with::(&file).unwrap(); let width = metadata.width as usize; let height = metadata.height as usize; + // TODO: convert + // let orientation = metadata.orientation; + let orientation = Orientation::NoTransforms; - let rgba_buffer = unsafe { - Vec::from_raw_parts( - buffer.as_ptr() as *mut u32, - buffer.len() / 4, - buffer.len() / 4, - ) - }; - std::mem::forget(buffer); + let rgba_buffer = vec_u8_to_u32(buffer); println!("Total JXL loading time: {:?}", total_start.elapsed()); @@ -178,6 +169,7 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer { height, rgba_buffer, rating, + orientation, } } ImageFormat::Jpg => { @@ -196,50 +188,49 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer { buffer = vec![0; width * height * 4]; decoder.decode_into(buffer.as_mut_slice()).unwrap(); - let orientation_start = Instant::now(); - // TODO: Optimize rotation let orientation = image.orientation; - let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap(); - let mut dynamic_image = DynamicImage::from(image); - dynamic_image.apply_orientation(orientation); - let buffer = dynamic_image.as_rgba8().unwrap(); - let (width, height) = swap_wh(width, height, orientation); - let orientation_time = orientation_start.elapsed(); - - // Reinterpret to avoid copying - let rgba_buffer = unsafe { - Vec::from_raw_parts( - buffer.as_ptr() as *mut u32, - buffer.len() / 4, - buffer.len() / 4, - ) - }; - std::mem::forget(dynamic_image); - let total_time = total_start.elapsed(); - println!("Orientation time: {:?}", orientation_time); - println!("Total loading time: {:?}", total_time); + let rgba_buffer = vec_u8_to_u32(buffer); + println!("Total loading time: {:?}", total_start.elapsed()); ImflowImageBuffer { width, height, rgba_buffer, rating, + orientation, } } } } -pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec { - let flat = img.to_rgba8(); - let mut buffer = flat.to_vec(); - let vec = unsafe { +fn vec_u8_to_u32(buffer: Vec) -> Vec { + let rgba_buffer = unsafe { Vec::from_raw_parts( - buffer.as_mut_ptr() as *mut u32, + buffer.as_ptr() as *mut u32, buffer.len() / 4, buffer.len() / 4, ) }; - mem::forget(buffer); - vec + std::mem::forget(buffer); + rgba_buffer + // bytemuck::cast_vec(buffer) +} + +fn vec_u32_to_u8(buffer: Vec) -> Vec { + let rgba_buffer = unsafe { + Vec::from_raw_parts( + buffer.as_ptr() as *mut u8, + buffer.len() * 4, + buffer.len() * 4, + ) + }; + std::mem::forget(buffer); + rgba_buffer + // bytemuck::cast_vec(buffer) +} + +pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec { + let flat: ImageBuffer, Vec> = img.to_rgba8(); + vec_u8_to_u32(flat.into_vec()) } pub fn load_available_images(dir: PathBuf) -> Vec { @@ -277,44 +268,41 @@ pub fn load_available_images(dir: PathBuf) -> Vec { } pub fn check_embedded_thumbnail(path: &PathBuf) -> bool { - if let Ok(meta) = Metadata::new_from_path(&path) { - meta.get_preview_images().is_some() - } else { - false - } + Metadata::new_from_path(path).map_or(false, |meta| meta.get_preview_images().is_some()) } pub fn get_embedded_thumbnail(image: &ImageData) -> Option> { - let meta = Metadata::new_from_path(&image.path); - match meta { - Ok(meta) => { - if let Some(previews) = meta.get_preview_images() { - for preview in previews { - return Some(preview.get_data().unwrap()); - } - } - None - } - Err(_) => None, - } + Metadata::new_from_path(&image.path) + .ok()? + .get_preview_images()? + .first() + .and_then(|preview| preview.get_data().ok()) } pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer { let cache_path = path.get_cache_path(); + let mut buffer: Option> = None; if cache_path.exists() { - let bytes = fs::read(cache_path).unwrap(); + let read_bytes = fs::read(&cache_path).unwrap(); + if read_bytes.len() != 0 { + buffer = Some(read_bytes); + } + } + if let Some(bytes) = buffer { let width = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize; let height = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; - let buffer: &[u8] = &bytes[8..]; - let mut buffer_u8 = buffer.to_vec(); + let orientation = + Orientation::from_exif(u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as u8) + .unwrap_or(Orientation::NoTransforms); + let (ptr, len, cap) = bytes.into_raw_parts(); + assert!(ptr.align_offset(4) == 0); let buffer_u32 = unsafe { Vec::from_raw_parts( - buffer_u8.as_mut_ptr() as *mut u32, - buffer_u8.len() / 4, - buffer_u8.len() / 4, + (ptr as usize + 12) as *mut u32, + (len - 12) / 4, + (cap - 12) / 4, ) }; - std::mem::forget(buffer_u8); assert_eq!(width * height, buffer_u32.len()); @@ -323,12 +311,13 @@ pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer { height, rgba_buffer: buffer_u32, rating: 0, + orientation, }; } let thumbnail = if path.format == ImageFormat::Heif { load_heif(path, true) } else { - load_thumbnail_exif(path).unwrap_or(load_thumbnail_full(path)) + load_thumbnail_exif(path).unwrap_or_else(|| load_thumbnail_full(path)) }; save_thumbnail(&cache_path, thumbnail.clone()); @@ -336,37 +325,27 @@ pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer { } pub fn load_thumbnail_exif(path: &ImageData) -> Option { - match get_embedded_thumbnail(path) { - Some(thumbnail) => { - let decoder = image::ImageReader::new(Cursor::new(thumbnail)) - .with_guessed_format() - .unwrap(); - let mut image = decoder.decode().unwrap(); + if let Some(thumbnail) = get_embedded_thumbnail(path) { + let decoder = image::ImageReader::new(Cursor::new(thumbnail)) + .with_guessed_format() + .unwrap(); + let image = decoder.decode().unwrap(); - image.apply_orientation(path.orientation); - let width: usize = image.width() as usize; - let height: usize = image.height() as usize; - let flat = image.into_rgba8().into_raw(); - let mut buffer = flat.to_vec(); - let buffer_u32 = unsafe { - Vec::from_raw_parts( - buffer.as_mut_ptr() as *mut u32, - buffer.len() / 4, - buffer.len() / 4, - ) - }; - std::mem::forget(buffer); + let orientation = path.orientation; + let width: usize = image.width() as usize; + let height: usize = image.height() as usize; + let rgba_buffer = image_to_rgba_buffer(image); + let rating = get_rating(path.into()); - let rating = get_rating(path.into()); - - Some(ImflowImageBuffer { - width, - height, - rgba_buffer: buffer_u32, - rating, - }) - } - _ => None, + Some(ImflowImageBuffer { + width, + height, + rgba_buffer, + rating, + orientation, + }) + } else { + None } } @@ -378,17 +357,21 @@ pub fn load_thumbnail_full(path: &ImageData) -> ImflowImageBuffer { .unwrap() .decode() .unwrap() - .resize_to_fill(1920, 1920, FilterType::Lanczos3); + .resize_to_fill(720, 720, FilterType::Nearest); let width = image.width() as usize; let height = image.height() as usize; + let start = std::time::Instant::now(); let buffer = image_to_rgba_buffer(image); + println!("Elapsed: {:?}", start.elapsed()); let rating = get_rating(path.into()); + let orientation = path.orientation; ImflowImageBuffer { width, height, rgba_buffer: buffer, rating, + orientation, } } @@ -446,18 +429,25 @@ pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer { assert_eq!(interleaved_plane.storage_bits_per_pixel, 32); let rgba_buffer = interleaved_plane.data; - let u32_slice = unsafe { - std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4) - }; + let u32_slice = slice_u8_to_u32(rgba_buffer); ImflowImageBuffer { width, height, rgba_buffer: u32_slice.to_vec(), rating, + // TODO: verify + orientation: path.orientation, } } +fn slice_u8_to_u32(rgba_buffer: &[u8]) -> &[u32] { + let u32_slice = unsafe { + std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4) + }; + u32_slice +} + pub fn get_file_hash(path: &PathBuf) -> GenericArray { let mut file = File::open(path).unwrap(); let mut buf = [0u8; 16 * 1024]; @@ -474,18 +464,11 @@ pub fn save_thumbnail(path: &PathBuf, image: ImflowImageBuffer) { if !cache_dir.exists() { fs::create_dir(cache_dir).unwrap(); } - println!("path: {:?}", path); let mut file = File::create(path).unwrap(); - let buffer = image.rgba_buffer; - let u8_buffer = unsafe { - Vec::from_raw_parts( - buffer.as_ptr() as *mut u8, - buffer.len() * 4, - buffer.len() * 4, - ) - }; - std::mem::forget(buffer); + let u8_buffer = vec_u32_to_u8(image.rgba_buffer); file.write(&(image.width as u32).to_le_bytes()).unwrap(); file.write(&(image.height as u32).to_le_bytes()).unwrap(); + file.write(&(image.orientation.to_exif() as u32).to_le_bytes()) + .unwrap(); file.write(&u8_buffer).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index ae3940f..1c4db4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ +#![feature(vec_into_raw_parts)] pub mod image; pub mod store; diff --git a/src/shader.wgsl b/src/shader.wgsl index 0ed19ea..d97372a 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,7 +1,8 @@ struct Transforms { transform: mat4x4, width: u32, - height: u32 + height: u32, + orientation: u32 }; @group(0) @binding(2) var transforms: Transforms; @@ -26,11 +27,24 @@ fn vs_main(in: VertexInput) -> VertexOutput { @group(0) @binding(0) var texture: texture_2d; @group(0) @binding(1) var texture_sampler: sampler; +fn reverse(in: vec2) -> vec2 { + return vec2(in.y, in.x); +} + @fragment -fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { - let texture_size = vec2(f32(transforms.width), f32(transforms.height)); +fn fs_main(@location(0) in: vec2) -> @location(0) vec4 { + var texture_size = vec2(f32(transforms.width), f32(transforms.height)); let out_dim = vec2(textureDimensions(texture)); + var uv = in; + if transforms.orientation == 2 { + uv.x = 1.0-uv.x; + } else if transforms.orientation == 3 { + uv.y = 1.0-uv.y; + } let scale = texture_size / out_dim; - let pixel = uv * scale; + var pixel = uv * scale; + if transforms.orientation == 3 { + pixel = reverse(pixel); + } return textureSample(texture, texture_sampler, pixel); } diff --git a/src/store.rs b/src/store.rs index 407c001..c05f0a5 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,24 +1,25 @@ use crate::image::{ImageData, load_thumbnail}; use crate::image::{ImflowImageBuffer, load_available_images, load_image}; +use crossbeam_channel::{Receiver, Sender, unbounded}; +use rayon::prelude::*; use rexiv2::Metadata; use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; -use std::sync::mpsc; use std::time::Instant; use threadpool::ThreadPool; const PRELOAD_NEXT_IMAGE_N: usize = 16; pub struct ImageStore { - pub(crate) current_image_id: usize, + pub current_image_id: usize, pub(crate) loaded_images: HashMap, pub(crate) loaded_images_thumbnails: HashMap, - pub(crate) available_images: Vec, + pub available_images: Vec, pub current_image_path: ImageData, pub(crate) pool: ThreadPool, - pub(crate) loader_rx: mpsc::Receiver<(ImageData, ImflowImageBuffer)>, - pub(crate) loader_tx: mpsc::Sender<(ImageData, ImflowImageBuffer)>, + pub(crate) loader_rx: Receiver<(ImageData, ImflowImageBuffer)>, + pub(crate) loader_tx: Sender<(ImageData, ImflowImageBuffer)>, pub(crate) currently_loading: HashSet, } @@ -26,27 +27,33 @@ impl ImageStore { pub fn new(path: PathBuf) -> Self { let current_image_id: usize = 0; let mut loaded_images: HashMap = HashMap::new(); - let mut loaded_thumbnails: HashMap = HashMap::new(); + // let mut loaded_thumbnails: HashMap = HashMap::new(); let available_images = load_available_images(path); let new_path = available_images[0].clone(); - let (loader_tx, loader_rx) = mpsc::channel(); + let (loader_tx, loader_rx) = unbounded(); let pool = ThreadPool::new(32); let currently_loading = HashSet::new(); + let first_image_path = available_images[0].clone(); + let first_image_thread = std::thread::spawn(move || { + let image = load_image(&first_image_path); + (first_image_path, image) + }); + let total_start = Instant::now(); - let mut loaded = 0; - let to_load = available_images.len(); - for path in &available_images { - if path.embedded_thumbnail { - let buf = load_thumbnail(path); - loaded_thumbnails.insert(path.clone(), buf); - println!("Loaded embedded thumbnail for: {}/{}", loaded, to_load); - } - loaded += 1; - } + let (sender, receiver) = unbounded(); + available_images + .par_iter() + .for_each_with(sender, |s, path| { + if path.embedded_thumbnail { + let buf = load_thumbnail(path); + s.send((path.clone(), buf)).unwrap(); + } + }); + let loaded_thumbnails: HashMap<_, _> = receiver.iter().collect(); let total_time = total_start.elapsed(); println!( "all thumbnails load time: {:?} for {}", @@ -54,8 +61,10 @@ impl ImageStore { loaded_thumbnails.len() ); - let path = available_images[0].clone(); - let image = load_image(&path.clone()); + // let path = available_images[0].clone(); + // let image = load_image(&path.clone()); + // loaded_images.insert(path, image); + let (path, image) = first_image_thread.join().unwrap(); loaded_images.insert(path, image); let mut state = Self { current_image_id, @@ -121,6 +130,7 @@ impl ImageStore { } pub fn request_load(&mut self, path: ImageData) { + // return; if self.loaded_images.contains_key(&path) || self.currently_loading.contains(&path) { return; } @@ -161,6 +171,11 @@ impl ImageStore { self.loaded_images.get(path) } + pub fn get_thumbnail_id(&self, id: usize) -> &ImflowImageBuffer { + let path = self.available_images.get(id).unwrap(); + self.loaded_images_thumbnails.get(path).unwrap() + } + pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer { if self .loaded_images_thumbnails