From 4e42cfdc051232245440c094adcdaad0dade163e Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Mon, 31 Mar 2025 20:10:39 +0200 Subject: [PATCH] Refactor, optimizations --- benches/image_load.rs | 73 +++-- src/image.rs | 371 ++++++---------------- src/lib.rs | 1 + src/main.rs | 718 +++++++++++++++--------------------------- src/store.rs | 145 +++++++++ 5 files changed, 544 insertions(+), 764 deletions(-) create mode 100644 src/store.rs diff --git a/benches/image_load.rs b/benches/image_load.rs index dcb67c7..98db437 100644 --- a/benches/image_load.rs +++ b/benches/image_load.rs @@ -3,44 +3,59 @@ use std::iter; use std::time::Duration; -use criterion::BenchmarkId; +use criterion::{AxisScale, BenchmarkId, PlotConfiguration}; use criterion::{Criterion, black_box, criterion_group, criterion_main}; -use imflow::image::{load_available_images, load_image_argb, load_image_argb_imagers, load_thumbnail, Approach}; +use imflow::image::{ + load_available_images, load_image, load_thumbnail_exif, load_thumbnail_full +}; const PATH: &str = "test_images"; -pub fn criterion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("image_decode"); +// pub fn full_load_benchmark(c: &mut Criterion) { +// let mut group = c.benchmark_group("image_decode"); + +// group +// .sample_size(10) +// .measurement_time(Duration::from_millis(500)) +// .warm_up_time(Duration::from_millis(200)); + +// let images = load_available_images(PATH.into()); +// for image in images.iter() { +// let image_name = image.to_str().unwrap(); + +// group.bench_with_input(format!("{}/zune", image_name), image, |b, image| { +// b.iter(|| load_image_argb(image.clone().into())); +// }); + +// group.bench_with_input(format!("{}/image-rs", image_name), image, |b, image| { +// b.iter(|| load_image_argb_imagers(image.clone().into())); +// }); +// } + +// group.finish(); +// } + +pub fn thumbnail_load_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("thumbnail"); group - .sample_size(10) // Reduce number of samples (default is 100) - .measurement_time(Duration::from_millis(500)) // Reduce measurement time (default is 5 seconds) - .warm_up_time(Duration::from_millis(200)); // Reduce warm-up time (default is 3 seconds) + .sample_size(10) + .measurement_time(Duration::from_millis(500)) + .warm_up_time(Duration::from_millis(200)); let images = load_available_images(PATH.into()); - for image in images.iter() { - let image_name = image.to_str().unwrap(); - - // Benchmark zune for this image - group.bench_with_input( - format!("{}/zune", image_name), - image, - |b, image| { - b.iter(|| load_image_argb(image.clone().into())); - }, - ); + group.bench_function("exif", |b| { + for image in images.iter().take(10) { + b.iter(|| load_thumbnail_exif(image)); + } + }); + group.bench_function("full", |b| { + for image in images.iter().take(10) { + b.iter(|| load_thumbnail_full(image)); + } + }); - // Benchmark image-rs for the same image - group.bench_with_input( - format!("{}/image-rs", image_name), - image, - |b, image| { - b.iter(|| load_image_argb_imagers(image.clone().into())); - }, - ); - } - group.finish(); } -criterion_group!(benches, criterion_benchmark); +criterion_group!(benches, thumbnail_load_benchmark); criterion_main!(benches); diff --git a/src/image.rs b/src/image.rs index 19d9cee..22a7a59 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,273 +1,76 @@ use iced::widget::image::Handle; -use iced::widget::image::Image as IcedImage; -use image::imageops::FilterType; -// use image::codecs::jpeg::JpegDecoder; -// use image::codecs::jpeg::JpegDecoder; use image::DynamicImage; -use image::ImageReader; -use itertools::Itertools; -use memmap2::Mmap; +use image::imageops::FilterType; use zune_image::codecs::jpeg::JpegDecoder; use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; use zune_image::codecs::qoi::zune_core::options::DecoderOptions; -use zune_image::image::Image as ZuneImage; use std::fs; use std::fs::File; use std::fs::read; -use std::io; +use std::io::BufReader; use std::io::Cursor; -use std::io::Read; -use std::ops::Deref; use std::path::PathBuf; use std::time::Instant; -pub enum Approach { - Mmap, - Path, - ImageRs, - Iced, - ImageRsPath, -} - -pub fn convert_zune_rgb_to_rgba(rgb_data: Vec>) -> Vec { - let r_channel = &rgb_data[0]; - - let num_pixels = r_channel.len() / 3; - let mut rgba_data: Vec = Vec::with_capacity(num_pixels * 4); - - for i in 0..num_pixels { - rgba_data.push(r_channel[i * 3]); - rgba_data.push(r_channel[i * 3 + 1]); - rgba_data.push(r_channel[i * 3 + 2]); - rgba_data.push(255); // Fully opaque Alpha value - } - - rgba_data -} - -pub fn map_file(path: &str) -> io::Result { - let file = File::open(path)?; - unsafe { Mmap::map(&file) } -} - -pub fn map_file_path(path: PathBuf) -> io::Result { - let file = File::open(path)?; - unsafe { Mmap::map(&file) } -} - -pub fn read_zune_image(mmap: &[u8]) -> Result { - ZuneImage::read(mmap, DecoderOptions::new_fast()).map_err(|e| e.to_string()) -} - -pub fn read_zune_image_path(path: &str) -> ZuneImage { - // let file = File::open(path).unwrap(); - // let file_contents = read(path).unwrap(); - // let mmap = map_file_path(path.into()).unwrap(); - // let decoder = JpegDecoder::new(&file_contents); - // decoder.decode_into() - // ZuneImage::read(mmap, DecoderOptions::new_fast()).map_err(|e| e.to_string()) - zune_image::image::Image::open_with_options(path, DecoderOptions::new_fast()).unwrap() -} - -pub fn flatten_zune_image(img: &ZuneImage) -> Vec> { - img.flatten_to_u8() -} - -pub fn flatten_image_image(img: DynamicImage) -> Vec { - img.into_rgba8().into_raw() -} - -pub fn create_iced_handle(width: u32, height: u32, rgba: Vec) -> Handle { - Handle::from_rgba(width, height, rgba) -} - -// pub fn load_thumbnail( -// path: &str, -// approach: Approach, -// ) -> Result { -// match approach { -// Approach::Mmap => { -// let mmap = map_file(path).unwrap(); -// println!("mapped file"); -// let img = read_zune_image(mmap.deref())?; -// let width = img.dimensions().0 as u32; -// let height = img.dimensions().1 as u32; -// println!("loaded"); -// let flat = flatten_zune_image(&img); -// println!("flattened"); -// let rgba = convert_zune_rgb_to_rgba(flat); -// println!("rgbad"); -// let conv = create_iced_handle(width, height, rgba); -// println!("iced"); - -// Ok(conv) -// } -// Approach::Path => { -// let img = read_zune_image_path(path); -// let width = img.dimensions().0 as u32; -// let height = img.dimensions().1 as u32; -// println!("loaded"); -// let flat = flatten_zune_image(&img); -// println!("flattened"); -// let rgba = convert_zune_rgb_to_rgba(flat); -// println!("rgbad"); -// let conv = create_iced_handle(width, height, rgba); -// println!("iced"); - -// Ok(conv) -// } -// Approach::ImageRs => { -// let mmap = map_file(path).unwrap(); -// let img = image::load_from_memory(mmap.deref()).map_err(|e| e.to_string())?; -// let width = img.width(); -// let height = img.height(); -// println!("loaded"); -// let rgba = flatten_image_image(img); -// println!("rgbad"); -// let conv = create_iced_handle(width, height, rgba); -// println!("iced"); - -// Ok(conv) -// } -// Approach::ImageRsPath => { -// let img = ImageReader::open(path).unwrap().decode().unwrap(); -// let width = img.width(); -// let height = img.height(); -// println!("loaded"); -// let rgba = flatten_image_image(img); -// println!("rgbad"); -// let conv = create_iced_handle(width, height, rgba); -// println!("iced"); - -// Ok(conv) -// } -// Approach::Iced => Ok(Handle::from_path(path)), -// } -// } - -pub fn load_image_argb(path: PathBuf) -> ImflowImageBuffer { - let total_start = Instant::now(); - - let file_contents = read(path).unwrap(); - - let mut decoder = JpegDecoder::new(&file_contents); - let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::BGRA); - decoder.set_options(options); - decoder.decode_headers().unwrap(); - let info = decoder.info().unwrap(); - let width = info.width as usize; - let height = info.height as usize; - let mut buffer2: Vec = vec![0; width * height * 4]; - decoder.decode_into(buffer2.as_mut_slice()); - - // Stage 2: Read the image - // let stage2_start = Instant::now(); - // let img = read_zune_image(mmap.deref()).unwrap(); - // let width = img.dimensions().0; - // let height = img.dimensions().1; - // let stage2_time = stage2_start.elapsed(); - // println!("Image decoding took: {:?}", stage2_time); - - // Stage 3: Flatten the image - // let stage3_start = Instant::now(); - // let flat = &mut flatten_zune_image(&img)[0]; - // let stage3_time = stage3_start.elapsed(); - // println!("Image flattening took: {:?}", stage3_time); - - // Stage 4: Convert to ARGB format - let stage4_start = Instant::now(); - // let mut buffer: Vec = vec![0; width * height]; - - // for (rgba, argb) in buffer2.chunks_mut(4).zip(buffer.iter_mut()) { - // let r = rgba[0] as u32; - // let g = rgba[1] as u32; - // let b = rgba[2] as u32; - // *argb = r << 16 | g << 8 | b; - // } - let buffer: &[u32] = - unsafe { std::slice::from_raw_parts(buffer2.as_ptr() as *const u32, buffer2.len() / 4) }; - let stage4_time = stage4_start.elapsed(); - println!("RGBA to ARGB conversion took: {:?}", stage4_time); - - // Total time - let total_time = total_start.elapsed(); - println!("Total loading time: {:?}", total_time); - - ImflowImageBuffer { - width, - height, - argb_buffer: buffer.to_vec(), - } -} - pub struct ImflowImageBuffer { pub width: usize, pub height: usize, pub argb_buffer: Vec, } -pub fn image_to_argb_buffer(img: DynamicImage) -> Vec { - let mut buffer: Vec = vec![0; (img.width() * img.height()) as usize]; - let mut flat = img.into_rgba8().into_raw(); - - for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { - let r = rgba[0] as u32; - let g = rgba[1] as u32; - let b = rgba[2] as u32; - *argb = r << 16 | g << 8 | b; - } - buffer +pub fn create_iced_handle(width: u32, height: u32, rgba: Vec) -> Handle { + Handle::from_rgba(width, height, rgba) } -pub fn load_image_argb_imagers(path: PathBuf) -> ImflowImageBuffer { +pub fn load_image(path: PathBuf) -> ImflowImageBuffer { let total_start = Instant::now(); - // Stage 1: Memory map the file - let stage1_start = Instant::now(); - let mmap = map_file_path(path).unwrap(); - let stage1_time = stage1_start.elapsed(); - // println!("File mapping took: {:?}", stage1_time); + let file = read(path).unwrap(); + let mut decoder = JpegDecoder::new(&file); + let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::BGRA); + decoder.set_options(options); - // Stage 2: Read the image - let stage2_start = Instant::now(); - let img = image::load_from_memory(mmap.deref()) - .map_err(|e| e.to_string()) - .unwrap(); - let width = img.width() as usize; - let height = img.height() as usize; - let stage2_time = stage2_start.elapsed(); - // println!("Image decoding took: {:?}", stage2_time); + decoder.decode_headers().unwrap(); + let info = decoder.info().unwrap(); + let width = info.width as usize; + let height = info.height as usize; - // Stage 3: Flatten the image - let stage3_start = Instant::now(); - let mut flat = img.into_rgba8().into_raw(); - let stage3_time = stage3_start.elapsed(); - // println!("Image flattening took: {:?}", stage3_time); + let mut buffer: Vec = vec![0; width * height * 4]; + decoder.decode_into(buffer.as_mut_slice()).unwrap(); - // Stage 4: Convert to ARGB format - let stage4_start = Instant::now(); - let mut buffer: Vec = vec![0; width * height]; + // Reinterpret to avoid copying + let buffer_u32 = unsafe { + Vec::from_raw_parts( + buffer.as_mut_ptr() as *mut u32, + buffer.len() / 4, + buffer.capacity() / 4, + ) + }; + std::mem::forget(buffer); - for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { - let r = rgba[0] as u32; - let g = rgba[1] as u32; - let b = rgba[2] as u32; - *argb = r << 16 | g << 8 | b; - } - let stage4_time = stage4_start.elapsed(); - // println!("RGBA to ARGB conversion took: {:?}", stage4_time); - - // Total time let total_time = total_start.elapsed(); println!("Total loading time: {:?}", total_time); ImflowImageBuffer { width, height, - argb_buffer: buffer, + argb_buffer: buffer_u32, } } + +pub fn image_to_argb_buffer(img: DynamicImage) -> Vec { + let flat = img.into_rgba8(); + let buf = flat.as_raw(); + + buf.chunks_exact(4).map(|rgba| { + let r = rgba[0] as u32; + let g = rgba[1] as u32; + let b = rgba[2] as u32; + r << 16 | g << 8 | b + }).collect() +} + pub fn load_available_images(dir: PathBuf) -> Vec { let mut files: Vec = fs::read_dir(dir) .unwrap() @@ -282,48 +85,68 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option> { let meta = rexiv2::Metadata::new_from_path(path); match meta { Ok(meta) => { - // println!("{:?}", meta.get_preview_images()); - meta.get_thumbnail().map(|v| v.to_vec()) - }, + for preview in meta.get_preview_images().unwrap() { + return Some(preview.get_data().unwrap()); + } + None + } Err(_) => None, } } -pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer { - if let Some(thumbnail) = get_embedded_thumbnail(path.clone()) { - let decoder = image::ImageReader::new(Cursor::new(thumbnail)) - .with_guessed_format() - .unwrap(); - let image = decoder.decode().unwrap(); - - let width: usize = image.width() as usize; - let height: usize = image.height() as usize; - let mut flat = image.into_rgba8().into_raw(); - let mut buffer: Vec = vec![0; width * height]; - - for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { - let r = rgba[0] as u32; - let g = rgba[1] as u32; - let b = rgba[2] as u32; - *argb = r << 16 | g << 8 | b; - } - - ImflowImageBuffer { - width, - height, - argb_buffer: buffer, - } - } else { - let reader = image::ImageReader::new(Cursor::new(read(path).unwrap())); - let image = reader.decode().unwrap().resize(640, 480, FilterType::Nearest); - let width = image.width() as usize; - let height = image.height() as usize; - let buffer = image_to_argb_buffer(image); - - ImflowImageBuffer { - width, - height, - argb_buffer: buffer, - } +pub fn load_thumbnail(path: &PathBuf) -> ImflowImageBuffer { + match load_thumbnail_exif(path) { + Some(thumbnail) => return thumbnail, + None => load_thumbnail_full(path), + } +} + +pub fn load_thumbnail_exif(path: &PathBuf) -> Option { + match get_embedded_thumbnail(path.clone()) { + Some(thumbnail) => { + let decoder = image::ImageReader::new(Cursor::new(thumbnail)) + .with_guessed_format() + .unwrap(); + let image = decoder.decode().unwrap(); + + let width: usize = image.width() as usize; + let height: usize = image.height() as usize; + let mut flat = image.into_rgba8().into_raw(); + let mut buffer: Vec = vec![0; width * height]; + + for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { + let r = rgba[0] as u32; + let g = rgba[1] as u32; + let b = rgba[2] as u32; + *argb = r << 16 | g << 8 | b; + } + + Some(ImflowImageBuffer { + width, + height, + argb_buffer: buffer, + }) + } + _ => None, + } +} + +pub fn load_thumbnail_full(path: &PathBuf) -> ImflowImageBuffer { + let file = BufReader::new(File::open(path).unwrap()); + let reader = image::ImageReader::new(file); + let image = reader + .with_guessed_format() + .unwrap() + .decode() + .unwrap() + .resize(640, 480, FilterType::Nearest); + let width = image.width() as usize; + let height = image.height() as usize; + let buffer = image_to_argb_buffer(image); + + ImflowImageBuffer { + width, + height, + argb_buffer: buffer, } } diff --git a/src/lib.rs b/src/lib.rs index 14995d4..ae3940f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod image; +pub mod store; diff --git a/src/main.rs b/src/main.rs index 88f0069..80ba79e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,19 @@ -//! This example showcases an interactive version of the Game of Life, invented -//! by John Conway. It leverages a `Canvas` together with other widgets. -// use grid::Grid; +// use std::fs::{self}; +// use std::path::{Path, PathBuf}; +// use std::collections::HashMap; +// use iced::widget::image::FilterMethod; +// use iced::widget::{ +// Column, Container, button, center, checkbox, column, container, row, slider, text, +// }; +// use iced::{Center, Element, Fill, Length, Subscription, Task, keyboard}; -use std::cmp::min; -use std::collections::{HashMap, HashSet}; -use std::fs::{self}; -use std::io::{Cursor, Read}; -use std::ops::Deref; -use std::path::{Path, PathBuf}; -use std::time::{self, Duration, Instant}; - -use iced::futures::AsyncReadExt; -use iced::widget::shader::wgpu::core::command::LoadOp; -// use iced::time::milliseconds; -use iced::widget::image::FilterMethod; -use iced::widget::{ - Column, Container, button, center, checkbox, column, container, pick_list, row, slider, text, -}; -use iced::{Center, Element, Fill, Length, Size, Subscription, Task, Theme, keyboard}; -use image::{self, DynamicImage, EncodableLayout, GenericImageView, ImageBuffer, ImageReader}; +use std::path::PathBuf; use clap::Parser; -use imflow::image::{ - Approach, ImflowImageBuffer, flatten_image_image, flatten_zune_image, get_embedded_thumbnail, - load_available_images, load_image_argb, load_image_argb_imagers, load_thumbnail, map_file, - map_file_path, read_zune_image, -}; use minifb::{Key, Window, WindowOptions}; -use threadpool::ThreadPool; -use zune_image::codecs::qoi::zune_core::options::DecoderOptions; // for general image operations -// use image::io::Reader as ImageReader; // specifically for Reader -use std::sync::{Arc, mpsc}; +use imflow::image::ImflowImageBuffer; +use imflow::store::ImageStore; // use winit::{ // application::ApplicationHandler, @@ -333,200 +315,6 @@ use std::sync::{Arc, mpsc}; // } // -struct State { - current_image_id: usize, - loaded_images: HashMap, - loaded_images_thumbnails: HashMap, - available_images: Vec, - current_image_path: PathBuf, - pool: ThreadPool, - loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>, - loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>, - currently_loading: HashSet, // Track what's being loaded -} - -impl State { - 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 available_images = load_available_images(path); - let new_path = available_images[0].clone(); - // let current_image = load_image_argb_imagers(new_path.clone()); - // let current_image = load_image_argb_imagers(new_path.clone()); - // loaded_images.insert(new_path.clone(), current_image); - - let (loader_tx, loader_rx) = mpsc::channel(); - - let pool = ThreadPool::new(32); - - let currently_loading = HashSet::new(); - - let total_start = Instant::now(); - let mut loaded = 0; - let to_load = available_images.len(); - for path in &available_images { - if let Some(thumbnail) = get_embedded_thumbnail(path.clone()) { - let decoder = image::ImageReader::new(Cursor::new(thumbnail)) - .with_guessed_format() - .unwrap(); - let image = decoder.decode().unwrap(); - - let width: usize = image.width() as usize; - let height: usize = image.height() as usize; - let mut flat = image.into_rgba8().into_raw(); - let mut buffer: Vec = vec![0; width * height]; - - for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { - let r = rgba[0] as u32; - let g = rgba[1] as u32; - let b = rgba[2] as u32; - *argb = r << 16 | g << 8 | b; - } - - let buf = ImflowImageBuffer { - width, - height, - argb_buffer: buffer, - }; - - loaded_thumbnails.insert(path.clone(), buf); - loaded += 1; - println!("{}/{}", loaded, to_load); - } else { - loaded += 1; - if !loaded_images.contains_key(&path.clone()) { - let loaded = load_image_argb(path.clone()); - loaded_images.insert(path.clone(), loaded); - // self.request_load(new_path.clone()); - } - // let loaded = load_image_argb(path.clone()); - // loaded_images.get(&path.clone()).unwrap() - println!("none for {:?}", path); - } - } - let total_time = total_start.elapsed(); - println!( - "all thumbnails load time: {:?} for {}", - total_time, - loaded_thumbnails.len() - ); - - let mut state = Self { - current_image_id, - loaded_images, - available_images, - current_image_path: new_path, - pool, - loader_rx, - loader_tx, - currently_loading, - loaded_images_thumbnails: loaded_thumbnails, - }; - - // state.preload_next_images(min(state.available_images.len(), 1)); - - // let thumbnail_path = state.available_images[0].clone(); - - state - } - - fn preload_next_images(&mut self, n: usize) { - for image in self.available_images.clone().iter().take(n) { - self.request_load(image.clone()); - } - } - - fn request_load(&mut self, path: PathBuf) { - if self.loaded_images.contains_key(&path) || self.currently_loading.contains(&path) { - return; - } - let tx = self.loader_tx.clone(); - self.currently_loading.insert(path.clone()); - - self.pool.execute(move || { - let image = load_image_argb(path.clone()); - // let image = load_image_argb_imagers(path.clone()); - let _ = tx.send((path, image)); - }); - } - - fn check_loaded_images(&mut self) { - while let Ok((path, image)) = self.loader_rx.try_recv() { - self.loaded_images.insert(path.clone(), image); - self.currently_loading.remove(&path); - } - } - - fn next_image(&mut self, change: i32) { - self.current_image_id = (self.current_image_id as i32 + change) - .clamp(0, self.available_images.len() as i32 - 1) - as usize; - let new_path = self.available_images[self.current_image_id].clone(); - if !self.loaded_images.contains_key(&new_path) { - // self.request_load(new_path.clone()); - } - self.current_image_path = new_path; - } - - fn get_current_image(&self) -> Option<&ImflowImageBuffer> { - self.loaded_images.get(&self.current_image_path) - } - - fn get_thumbnail(&mut self) -> &ImflowImageBuffer { - if self - .loaded_images_thumbnails - .contains_key(&self.current_image_path) - { - return self - .loaded_images_thumbnails - .get(&self.current_image_path) - .unwrap(); - } - - let path = &self.current_image_path; - if let Some(thumbnail) = get_embedded_thumbnail(path.clone()) { - let total_start = Instant::now(); - let decoder = image::ImageReader::new(Cursor::new(thumbnail)) - .with_guessed_format() - .unwrap(); - let image = decoder.decode().unwrap(); - - let width: usize = image.width() as usize; - let height: usize = image.height() as usize; - let mut flat = image.into_rgba8().into_raw(); - let mut buffer: Vec = vec![0; width * height]; - - for (rgba, argb) in flat.chunks_mut(4).zip(buffer.iter_mut()) { - let r = rgba[0] as u32; - let g = rgba[1] as u32; - let b = rgba[2] as u32; - *argb = r << 16 | g << 8 | b; - } - - let buf = ImflowImageBuffer { - width, - height, - argb_buffer: buffer, - }; - let total_time = total_start.elapsed(); - println!("thumbnail load time: {:?}", total_time); - - self.loaded_images_thumbnails.insert(path.clone(), buf); - - return self.loaded_images_thumbnails.get(&path.clone()).unwrap(); - } - println!("skipping {:?}", path); - if !self.loaded_images.contains_key(&path.clone()) { - let loaded = load_image_argb(path.clone()); - self.loaded_images.insert(path.clone(), loaded); - // self.request_load(new_path.clone()); - } - // let loaded = load_image_argb(path.clone()); - self.loaded_images.get(&path.clone()).unwrap() - } -} - #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { @@ -550,8 +338,8 @@ fn main() { window.set_target_fps(120); let path = args.path.unwrap_or("./test_images".into()); - let mut state = State::new(path); - let mut waiting = false; + let mut state = ImageStore::new(path); + let mut waiting = true; window.set_key_repeat_delay(0.1); window.set_key_repeat_rate(0.1); @@ -560,22 +348,30 @@ fn main() { while window.is_open() && !window.is_key_down(Key::Escape) { window.update(); state.check_loaded_images(); - if !waiting && window.is_key_pressed(Key::Right, minifb::KeyRepeat::Yes) { + if window.is_key_pressed(Key::Right, minifb::KeyRepeat::Yes) { state.next_image(1); - show_image(&mut window, state.get_thumbnail()); - // waiting = true; - } else if !waiting && window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) { + if let Some(full) = state.get_current_image() { + show_image(&mut window, full); + } else { + show_image(&mut window, state.get_thumbnail()); + waiting = true; + } + } else if window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) { state.next_image(-1); - show_image(&mut window, state.get_thumbnail()); - // waiting = true; + if let Some(full) = state.get_current_image() { + show_image(&mut window, full); + } else { + show_image(&mut window, state.get_thumbnail()); + waiting = true; + } } - // if waiting { - // if let Some(image) = state.get_current_image() { - // waiting = false; + if waiting { + if let Some(image) = state.get_current_image() { + waiting = false; - // show_image(&mut window, &image); - // } - // } + show_image(&mut window, &image); + } + } } } @@ -585,247 +381,247 @@ fn show_image(window: &mut Window, image: &ImflowImageBuffer) { .unwrap(); } -struct MainApp { - is_playing: bool, - queued_ticks: usize, - speed: usize, - next_speed: Option, - version: usize, - image_filter_method: FilterMethod, - current_image: Option, - width: u32, - available_images: Vec, - current_image_id: usize, - loaded_images: HashMap, -} +// struct MainApp { +// is_playing: bool, +// queued_ticks: usize, +// speed: usize, +// next_speed: Option, +// version: usize, +// image_filter_method: FilterMethod, +// current_image: Option, +// width: u32, +// available_images: Vec, +// current_image_id: usize, +// loaded_images: HashMap, +// } -#[derive(Debug, Clone)] -enum Message { - TogglePlayback, - ToggleGrid(bool), - Clear, - SpeedChanged(f32), - Tick, - Next(i32), - ImageWidthChanged(u32), - ImageUseNearestToggled(bool), -} +// #[derive(Debug, Clone)] +// enum Message { +// TogglePlayback, +// ToggleGrid(bool), +// Clear, +// SpeedChanged(f32), +// Tick, +// Next(i32), +// ImageWidthChanged(u32), +// ImageUseNearestToggled(bool), +// } -impl MainApp { - fn new() -> Self { - let mut dir: Vec = fs::read_dir(Path::new("./test_images")) - .unwrap() - .map(|f| f.unwrap().path()) - .collect(); - dir.sort(); - let mut res = Self { - is_playing: false, - queued_ticks: 0, - speed: 5, - next_speed: None, - version: 0, - image_filter_method: FilterMethod::Nearest, - width: 1400, - current_image: Some(dir.first().unwrap().clone()), - available_images: dir, - current_image_id: 0, - loaded_images: HashMap::new(), - }; - let _ = res.update(Message::Next(0)); - res - } +// impl MainApp { +// fn new() -> Self { +// let mut dir: Vec = fs::read_dir(Path::new("./test_images")) +// .unwrap() +// .map(|f| f.unwrap().path()) +// .collect(); +// dir.sort(); +// let mut res = Self { +// is_playing: false, +// queued_ticks: 0, +// speed: 5, +// next_speed: None, +// version: 0, +// image_filter_method: FilterMethod::Nearest, +// width: 1400, +// current_image: Some(dir.first().unwrap().clone()), +// available_images: dir, +// current_image_id: 0, +// loaded_images: HashMap::new(), +// }; +// let _ = res.update(Message::Next(0)); +// res +// } - fn update(&mut self, message: Message) -> Task { - match message { - Message::Tick => { - self.queued_ticks = (self.queued_ticks + 1).min(self.speed); +// fn update(&mut self, message: Message) -> Task { +// match message { +// Message::Tick => { +// self.queued_ticks = (self.queued_ticks + 1).min(self.speed); - // if let Some(task) = self.grid.tick(self.queued_ticks) { - // if let Some(speed) = self.next_speed.take() { - // self.speed = speed; - // } +// // if let Some(task) = self.grid.tick(self.queued_ticks) { +// // if let Some(speed) = self.next_speed.take() { +// // self.speed = speed; +// // } - // self.queued_ticks = 0; +// // self.queued_ticks = 0; - // let version = self.version; +// // let version = self.version; - // // return Task::perform(task, Message::Grid.with(version)); - // } - } - Message::TogglePlayback => { - self.is_playing = !self.is_playing; - } - Message::ToggleGrid(show_grid_lines) => { - // self.grid.toggle_lines(show_grid_lines); - } - Message::Clear => { - // self.grid.clear(); - self.version += 1; - } - Message::SpeedChanged(speed) => { - if self.is_playing { - self.next_speed = Some(speed.round() as usize); - } else { - self.speed = speed.round() as usize; - } - } - Message::ImageWidthChanged(image_width) => { - self.width = image_width; - } - Message::ImageUseNearestToggled(use_nearest) => { - self.image_filter_method = if use_nearest { - FilterMethod::Nearest - } else { - FilterMethod::Linear - }; - } - Message::Next(change) => { - let elements = self.available_images.len() as i32; - let new_id = (self.current_image_id as i32 + change).clamp(0, elements - 1); - println!( - "updated id: {} from {} total {}", - new_id, self.current_image_id, elements - ); - self.current_image_id = new_id as usize; - let path = self - .available_images - .get(self.current_image_id) - .unwrap() - .clone(); - self.current_image = Some(path.clone()); - if !self.loaded_images.contains_key(&path.to_path_buf()) { - // self.loaded_images.insert( - // path.to_path_buf(), - // load_thumbnail(path.to_str().unwrap(), Approach::ImageRs).unwrap(), - // ); - } - } - } +// // // return Task::perform(task, Message::Grid.with(version)); +// // } +// } +// Message::TogglePlayback => { +// self.is_playing = !self.is_playing; +// } +// Message::ToggleGrid(show_grid_lines) => { +// // self.grid.toggle_lines(show_grid_lines); +// } +// Message::Clear => { +// // self.grid.clear(); +// self.version += 1; +// } +// Message::SpeedChanged(speed) => { +// if self.is_playing { +// self.next_speed = Some(speed.round() as usize); +// } else { +// self.speed = speed.round() as usize; +// } +// } +// Message::ImageWidthChanged(image_width) => { +// self.width = image_width; +// } +// Message::ImageUseNearestToggled(use_nearest) => { +// self.image_filter_method = if use_nearest { +// FilterMethod::Nearest +// } else { +// FilterMethod::Linear +// }; +// } +// Message::Next(change) => { +// let elements = self.available_images.len() as i32; +// let new_id = (self.current_image_id as i32 + change).clamp(0, elements - 1); +// println!( +// "updated id: {} from {} total {}", +// new_id, self.current_image_id, elements +// ); +// self.current_image_id = new_id as usize; +// let path = self +// .available_images +// .get(self.current_image_id) +// .unwrap() +// .clone(); +// self.current_image = Some(path.clone()); +// if !self.loaded_images.contains_key(&path.to_path_buf()) { +// // self.loaded_images.insert( +// // path.to_path_buf(), +// // load_thumbnail(path.to_str().unwrap(), Approach::ImageRs).unwrap(), +// // ); +// } +// } +// } - Task::none() - } +// Task::none() +// } - fn subscription(&self) -> Subscription { - keyboard::on_key_press(|key, _modifiers| match key { - keyboard::Key::Named(keyboard::key::Named::ArrowRight) => Some(Message::Next(1)), - keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(Message::Next(-1)), - _ => None, - }) - } +// fn subscription(&self) -> Subscription { +// keyboard::on_key_press(|key, _modifiers| match key { +// keyboard::Key::Named(keyboard::key::Named::ArrowRight) => Some(Message::Next(1)), +// keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(Message::Next(-1)), +// _ => None, +// }) +// } - fn view(&self) -> Element<'_, Message> { - let version = self.version; - let selected_speed = self.next_speed.unwrap_or(self.speed); - let controls = view_controls( - self.is_playing, - true, - // self.grid.are_lines_visible(), - selected_speed, - // self.grid.preset(), - ); +// fn view(&self) -> Element<'_, Message> { +// let version = self.version; +// let selected_speed = self.next_speed.unwrap_or(self.speed); +// let controls = view_controls( +// self.is_playing, +// true, +// // self.grid.are_lines_visible(), +// selected_speed, +// // self.grid.preset(), +// ); - let content = column![ - // image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into(), - // self.grid.view().map(Message::Grid.with(version)), - self.image(), - controls, - ] - .height(Fill); +// let content = column![ +// // image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into(), +// // self.grid.view().map(Message::Grid.with(version)), +// self.image(), +// controls, +// ] +// .height(Fill); - container(content).width(Fill).height(Fill).into() - // image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into() - } +// container(content).width(Fill).height(Fill).into() +// // image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into() +// } - fn image(&self) -> Column { - let width = self.width; - let filter_method = self.image_filter_method; +// fn image(&self) -> Column { +// let width = self.width; +// let filter_method = self.image_filter_method; - Self::container("Image") - .push("An image that tries to keep its aspect ratio.") - .push(self.ferris( - width, - filter_method, - self.current_image.as_ref().unwrap().as_ref(), - )) - .push(slider(100..=1500, width, Message::ImageWidthChanged)) - .push(text!("Width: {width} px").width(Fill).align_x(Center)) - .push( - checkbox( - "Use nearest interpolation", - filter_method == FilterMethod::Nearest, - ) - .on_toggle(Message::ImageUseNearestToggled), - ) - .align_x(Center) - } +// Self::container("Image") +// .push("An image that tries to keep its aspect ratio.") +// .push(self.ferris( +// width, +// filter_method, +// self.current_image.as_ref().unwrap().as_ref(), +// )) +// .push(slider(100..=1500, width, Message::ImageWidthChanged)) +// .push(text!("Width: {width} px").width(Fill).align_x(Center)) +// .push( +// checkbox( +// "Use nearest interpolation", +// filter_method == FilterMethod::Nearest, +// ) +// .on_toggle(Message::ImageUseNearestToggled), +// ) +// .align_x(Center) +// } - fn container(title: &str) -> Column<'_, Message> { - column![text(title).size(50)].spacing(20) - } +// fn container(title: &str) -> Column<'_, Message> { +// column![text(title).size(50)].spacing(20) +// } - fn ferris<'a>( - &self, - width: u32, - filter_method: iced::widget::image::FilterMethod, - path: &Path, - ) -> Container<'a, Message> { - if self.loaded_images.get(path).is_none() { - return center(text("loading")); - } - let img = iced::widget::image::Image::new(self.loaded_images.get(path).unwrap()); - center( - // This should go away once we unify resource loading on native - // platforms - img.filter_method(filter_method) - .width(Length::Fixed(width as f32)), - ) - } -} +// fn ferris<'a>( +// &self, +// width: u32, +// filter_method: iced::widget::image::FilterMethod, +// path: &Path, +// ) -> Container<'a, Message> { +// if self.loaded_images.get(path).is_none() { +// return center(text("loading")); +// } +// let img = iced::widget::image::Image::new(self.loaded_images.get(path).unwrap()); +// center( +// // This should go away once we unify resource loading on native +// // platforms +// img.filter_method(filter_method) +// .width(Length::Fixed(width as f32)), +// ) +// } +// } -impl Default for MainApp { - fn default() -> Self { - Self::new() - } -} +// impl Default for MainApp { +// fn default() -> Self { +// Self::new() +// } +// } -fn view_controls<'a>( - is_playing: bool, - is_grid_enabled: bool, - speed: usize, - // preset: Preset, -) -> Element<'a, Message> { - let playback_controls = row![ - button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback), - button("Previous") - .on_press(Message::Next(-1)) - .style(button::secondary), - button("Next") - .on_press(Message::Next(1)) - .style(button::secondary), - ] - .spacing(10); +// fn view_controls<'a>( +// is_playing: bool, +// is_grid_enabled: bool, +// speed: usize, +// // preset: Preset, +// ) -> Element<'a, Message> { +// let playback_controls = row![ +// button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback), +// button("Previous") +// .on_press(Message::Next(-1)) +// .style(button::secondary), +// button("Next") +// .on_press(Message::Next(1)) +// .style(button::secondary), +// ] +// .spacing(10); - let speed_controls = row![ - slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), - text!("x{speed}").size(16), - ] - .align_y(Center) - .spacing(10); +// let speed_controls = row![ +// slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), +// text!("x{speed}").size(16), +// ] +// .align_y(Center) +// .spacing(10); - row![ - playback_controls, - speed_controls, - // checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid), - // row![ - // pick_list(preset::ALL, Some(preset), Message::PresetPicked), - // button("Clear") - // .on_press(Message::Clear) - // .style(button::danger) - // ] - // .spacing(10) - ] - .padding(10) - .spacing(20) - .align_y(Center) - .into() -} +// row![ +// playback_controls, +// speed_controls, +// // checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid), +// // row![ +// // pick_list(preset::ALL, Some(preset), Message::PresetPicked), +// // button("Clear") +// // .on_press(Message::Clear) +// // .style(button::danger) +// // ] +// // .spacing(10) +// ] +// .padding(10) +// .spacing(20) +// .align_y(Center) +// .into() +// } diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..3bcf704 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,145 @@ +use crate::image::load_thumbnail; +use crate::image::{ + ImflowImageBuffer, load_available_images, load_image, +}; +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(crate) loaded_images: HashMap, + pub(crate) loaded_images_thumbnails: HashMap, + pub(crate) available_images: Vec, + pub(crate) current_image_path: PathBuf, + pub(crate) pool: ThreadPool, + pub(crate) loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>, + pub(crate) loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>, + pub(crate) currently_loading: HashSet, +} + +impl ImageStore { + pub fn new(path: PathBuf) -> Self { + let current_image_id: usize = 0; + let loaded_images: 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 pool = ThreadPool::new(32); + + let currently_loading = HashSet::new(); + + let total_start = Instant::now(); + let mut loaded = 0; + let to_load = available_images.len(); + for path in &available_images { + let buf = load_thumbnail(path); + loaded_thumbnails.insert(path.clone(), buf); + loaded += 1; + println!("{}/{}", loaded, to_load); + } + let total_time = total_start.elapsed(); + println!( + "all thumbnails load time: {:?} for {}", + total_time, + loaded_thumbnails.len() + ); + + let mut state = Self { + current_image_id, + loaded_images, + available_images, + current_image_path: new_path, + pool, + loader_rx, + loader_tx, + currently_loading, + loaded_images_thumbnails: loaded_thumbnails, + }; + + state.preload_next_images(PRELOAD_NEXT_IMAGE_N); + + state + } + + pub fn preload_next_images(&mut self, n: usize) { + for image in self + .available_images + .clone() + .iter() + .skip(self.current_image_id) + .take(n) + { + self.request_load(image.clone()); + } + } + + pub fn request_load(&mut self, path: PathBuf) { + if self.loaded_images.contains_key(&path) || self.currently_loading.contains(&path) { + return; + } + let tx = self.loader_tx.clone(); + self.currently_loading.insert(path.clone()); + + self.pool.execute(move || { + let image = load_image(path.clone()); + let _ = tx.send((path, image)); + }); + } + + pub fn check_loaded_images(&mut self) { + while let Ok((path, image)) = self.loader_rx.try_recv() { + self.loaded_images.insert(path.clone(), image); + self.currently_loading.remove(&path); + } + } + + pub fn next_image(&mut self, change: i32) { + self.current_image_id = (self.current_image_id as i32 + change) + .clamp(0, self.available_images.len() as i32 - 1) + as usize; + + let new_path = self.available_images[self.current_image_id].clone(); + if !self.loaded_images.contains_key(&new_path) { + self.request_load(new_path.clone()); + } + self.current_image_path = new_path; + self.preload_next_images(PRELOAD_NEXT_IMAGE_N); + } + + pub fn get_current_image(&self) -> Option<&ImflowImageBuffer> { + self.loaded_images.get(&self.current_image_path) + } + + pub fn get_image(&self, path: &PathBuf) -> Option<&ImflowImageBuffer> { + self.loaded_images.get(path) + } + + pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer { + if self + .loaded_images_thumbnails + .contains_key(&self.current_image_path) + { + return self + .loaded_images_thumbnails + .get(&self.current_image_path) + .unwrap(); + } + + let buf = load_thumbnail(&self.current_image_path); + self.loaded_images_thumbnails + .insert(self.current_image_path.clone(), buf); + return self + .loaded_images_thumbnails + .get(&self.current_image_path) + .unwrap(); + } +}