Refactor, optimizations

This commit is contained in:
Dawid Pietrykowski 2025-03-31 20:10:39 +02:00
parent aed4212043
commit 4e42cfdc05
5 changed files with 544 additions and 764 deletions

View File

@ -3,44 +3,59 @@
use std::iter; use std::iter;
use std::time::Duration; use std::time::Duration;
use criterion::BenchmarkId; use criterion::{AxisScale, BenchmarkId, PlotConfiguration};
use criterion::{Criterion, black_box, criterion_group, criterion_main}; 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"; const PATH: &str = "test_images";
pub fn criterion_benchmark(c: &mut Criterion) { // pub fn full_load_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("image_decode"); // 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 group
.sample_size(10) // Reduce number of samples (default is 100) .sample_size(10)
.measurement_time(Duration::from_millis(500)) // Reduce measurement time (default is 5 seconds) .measurement_time(Duration::from_millis(500))
.warm_up_time(Duration::from_millis(200)); // Reduce warm-up time (default is 3 seconds) .warm_up_time(Duration::from_millis(200));
let images = load_available_images(PATH.into()); let images = load_available_images(PATH.into());
for image in images.iter() { group.bench_function("exif", |b| {
let image_name = image.to_str().unwrap(); for image in images.iter().take(10) {
b.iter(|| load_thumbnail_exif(image));
// Benchmark zune for this image
group.bench_with_input(
format!("{}/zune", image_name),
image,
|b, image| {
b.iter(|| load_image_argb(image.clone().into()));
},
);
// 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.bench_function("full", |b| {
for image in images.iter().take(10) {
b.iter(|| load_thumbnail_full(image));
}
});
group.finish(); group.finish();
} }
criterion_group!(benches, criterion_benchmark); criterion_group!(benches, thumbnail_load_benchmark);
criterion_main!(benches); criterion_main!(benches);

View File

@ -1,273 +1,76 @@
use iced::widget::image::Handle; 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::DynamicImage;
use image::ImageReader; use image::imageops::FilterType;
use itertools::Itertools;
use memmap2::Mmap;
use zune_image::codecs::jpeg::JpegDecoder; use zune_image::codecs::jpeg::JpegDecoder;
use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
use zune_image::codecs::qoi::zune_core::options::DecoderOptions; use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
use zune_image::image::Image as ZuneImage;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::fs::read; use std::fs::read;
use std::io; use std::io::BufReader;
use std::io::Cursor; use std::io::Cursor;
use std::io::Read;
use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant; use std::time::Instant;
pub enum Approach {
Mmap,
Path,
ImageRs,
Iced,
ImageRsPath,
}
pub fn convert_zune_rgb_to_rgba(rgb_data: Vec<Vec<u8>>) -> Vec<u8> {
let r_channel = &rgb_data[0];
let num_pixels = r_channel.len() / 3;
let mut rgba_data: Vec<u8> = 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<Mmap> {
let file = File::open(path)?;
unsafe { Mmap::map(&file) }
}
pub fn map_file_path(path: PathBuf) -> io::Result<Mmap> {
let file = File::open(path)?;
unsafe { Mmap::map(&file) }
}
pub fn read_zune_image(mmap: &[u8]) -> Result<ZuneImage, String> {
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<Vec<u8>> {
img.flatten_to_u8()
}
pub fn flatten_image_image(img: DynamicImage) -> Vec<u8> {
img.into_rgba8().into_raw()
}
pub fn create_iced_handle(width: u32, height: u32, rgba: Vec<u8>) -> Handle {
Handle::from_rgba(width, height, rgba)
}
// pub fn load_thumbnail(
// path: &str,
// approach: Approach,
// ) -> Result<iced::widget::image::Handle, String> {
// 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<u8> = 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<u32> = 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 struct ImflowImageBuffer {
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
pub argb_buffer: Vec<u32>, pub argb_buffer: Vec<u32>,
} }
pub fn image_to_argb_buffer(img: DynamicImage) -> Vec<u32> { pub fn create_iced_handle(width: u32, height: u32, rgba: Vec<u8>) -> Handle {
let mut buffer: Vec<u32> = vec![0; (img.width() * img.height()) as usize]; Handle::from_rgba(width, height, rgba)
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 load_image_argb_imagers(path: PathBuf) -> ImflowImageBuffer { pub fn load_image(path: PathBuf) -> ImflowImageBuffer {
let total_start = Instant::now(); let total_start = Instant::now();
// Stage 1: Memory map the file let file = read(path).unwrap();
let stage1_start = Instant::now(); let mut decoder = JpegDecoder::new(&file);
let mmap = map_file_path(path).unwrap(); let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::BGRA);
let stage1_time = stage1_start.elapsed(); decoder.set_options(options);
// println!("File mapping took: {:?}", stage1_time);
// Stage 2: Read the image decoder.decode_headers().unwrap();
let stage2_start = Instant::now(); let info = decoder.info().unwrap();
let img = image::load_from_memory(mmap.deref()) let width = info.width as usize;
.map_err(|e| e.to_string()) let height = info.height as usize;
.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);
// Stage 3: Flatten the image let mut buffer: Vec<u8> = vec![0; width * height * 4];
let stage3_start = Instant::now(); decoder.decode_into(buffer.as_mut_slice()).unwrap();
let mut flat = img.into_rgba8().into_raw();
let stage3_time = stage3_start.elapsed();
// println!("Image flattening took: {:?}", stage3_time);
// Stage 4: Convert to ARGB format // Reinterpret to avoid copying
let stage4_start = Instant::now(); let buffer_u32 = unsafe {
let mut buffer: Vec<u32> = vec![0; width * height]; 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(); let total_time = total_start.elapsed();
println!("Total loading time: {:?}", total_time); println!("Total loading time: {:?}", total_time);
ImflowImageBuffer { ImflowImageBuffer {
width, width,
height, height,
argb_buffer: buffer, argb_buffer: buffer_u32,
} }
} }
pub fn image_to_argb_buffer(img: DynamicImage) -> Vec<u32> {
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<PathBuf> { pub fn load_available_images(dir: PathBuf) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = fs::read_dir(dir) let mut files: Vec<PathBuf> = fs::read_dir(dir)
.unwrap() .unwrap()
@ -282,15 +85,25 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option<Vec<u8>> {
let meta = rexiv2::Metadata::new_from_path(path); let meta = rexiv2::Metadata::new_from_path(path);
match meta { match meta {
Ok(meta) => { Ok(meta) => {
// println!("{:?}", meta.get_preview_images()); for preview in meta.get_preview_images().unwrap() {
meta.get_thumbnail().map(|v| v.to_vec()) return Some(preview.get_data().unwrap());
}, }
None
}
Err(_) => None, Err(_) => None,
} }
} }
pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer { pub fn load_thumbnail(path: &PathBuf) -> ImflowImageBuffer {
if let Some(thumbnail) = get_embedded_thumbnail(path.clone()) { match load_thumbnail_exif(path) {
Some(thumbnail) => return thumbnail,
None => load_thumbnail_full(path),
}
}
pub fn load_thumbnail_exif(path: &PathBuf) -> Option<ImflowImageBuffer> {
match get_embedded_thumbnail(path.clone()) {
Some(thumbnail) => {
let decoder = image::ImageReader::new(Cursor::new(thumbnail)) let decoder = image::ImageReader::new(Cursor::new(thumbnail))
.with_guessed_format() .with_guessed_format()
.unwrap(); .unwrap();
@ -308,14 +121,25 @@ pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer {
*argb = r << 16 | g << 8 | b; *argb = r << 16 | g << 8 | b;
} }
ImflowImageBuffer { Some(ImflowImageBuffer {
width, width,
height, height,
argb_buffer: buffer, argb_buffer: buffer,
})
} }
} else { _ => None,
let reader = image::ImageReader::new(Cursor::new(read(path).unwrap())); }
let image = reader.decode().unwrap().resize(640, 480, FilterType::Nearest); }
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 width = image.width() as usize;
let height = image.height() as usize; let height = image.height() as usize;
let buffer = image_to_argb_buffer(image); let buffer = image_to_argb_buffer(image);
@ -325,5 +149,4 @@ pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer {
height, height,
argb_buffer: buffer, argb_buffer: buffer,
} }
}
} }

View File

@ -1 +1,2 @@
pub mod image; pub mod image;
pub mod store;

View File

@ -1,37 +1,19 @@
//! This example showcases an interactive version of the Game of Life, invented // use std::fs::{self};
//! by John Conway. It leverages a `Canvas` together with other widgets. // use std::path::{Path, PathBuf};
// use grid::Grid; // 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::path::PathBuf;
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 clap::Parser; 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 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::{ // use winit::{
// application::ApplicationHandler, // application::ApplicationHandler,
@ -333,200 +315,6 @@ use std::sync::{Arc, mpsc};
// } // }
// //
struct State {
current_image_id: usize,
loaded_images: HashMap<PathBuf, ImflowImageBuffer>,
loaded_images_thumbnails: HashMap<PathBuf, ImflowImageBuffer>,
available_images: Vec<PathBuf>,
current_image_path: PathBuf,
pool: ThreadPool,
loader_rx: mpsc::Receiver<(PathBuf, ImflowImageBuffer)>,
loader_tx: mpsc::Sender<(PathBuf, ImflowImageBuffer)>,
currently_loading: HashSet<PathBuf>, // Track what's being loaded
}
impl State {
fn new(path: PathBuf) -> Self {
let current_image_id: usize = 0;
let mut loaded_images: HashMap<PathBuf, ImflowImageBuffer> = HashMap::new();
let mut loaded_thumbnails: HashMap<PathBuf, ImflowImageBuffer> = 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<u32> = 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<u32> = 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)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
@ -550,8 +338,8 @@ fn main() {
window.set_target_fps(120); window.set_target_fps(120);
let path = args.path.unwrap_or("./test_images".into()); let path = args.path.unwrap_or("./test_images".into());
let mut state = State::new(path); let mut state = ImageStore::new(path);
let mut waiting = false; let mut waiting = true;
window.set_key_repeat_delay(0.1); window.set_key_repeat_delay(0.1);
window.set_key_repeat_rate(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) { while window.is_open() && !window.is_key_down(Key::Escape) {
window.update(); window.update();
state.check_loaded_images(); 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); state.next_image(1);
if let Some(full) = state.get_current_image() {
show_image(&mut window, full);
} else {
show_image(&mut window, state.get_thumbnail()); show_image(&mut window, state.get_thumbnail());
// waiting = true; waiting = true;
} else if !waiting && window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) {
state.next_image(-1);
show_image(&mut window, state.get_thumbnail());
// waiting = true;
} }
// if waiting { } else if window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) {
// if let Some(image) = state.get_current_image() { state.next_image(-1);
// waiting = false; 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;
// show_image(&mut window, &image); show_image(&mut window, &image);
// } }
// } }
} }
} }
@ -585,247 +381,247 @@ fn show_image(window: &mut Window, image: &ImflowImageBuffer) {
.unwrap(); .unwrap();
} }
struct MainApp { // struct MainApp {
is_playing: bool, // is_playing: bool,
queued_ticks: usize, // queued_ticks: usize,
speed: usize, // speed: usize,
next_speed: Option<usize>, // next_speed: Option<usize>,
version: usize, // version: usize,
image_filter_method: FilterMethod, // image_filter_method: FilterMethod,
current_image: Option<PathBuf>, // current_image: Option<PathBuf>,
width: u32, // width: u32,
available_images: Vec<PathBuf>, // available_images: Vec<PathBuf>,
current_image_id: usize, // current_image_id: usize,
loaded_images: HashMap<PathBuf, iced::widget::image::Handle>, // loaded_images: HashMap<PathBuf, iced::widget::image::Handle>,
} // }
#[derive(Debug, Clone)] // #[derive(Debug, Clone)]
enum Message { // enum Message {
TogglePlayback, // TogglePlayback,
ToggleGrid(bool), // ToggleGrid(bool),
Clear, // Clear,
SpeedChanged(f32), // SpeedChanged(f32),
Tick, // Tick,
Next(i32), // Next(i32),
ImageWidthChanged(u32), // ImageWidthChanged(u32),
ImageUseNearestToggled(bool), // ImageUseNearestToggled(bool),
} // }
impl MainApp { // impl MainApp {
fn new() -> Self { // fn new() -> Self {
let mut dir: Vec<PathBuf> = fs::read_dir(Path::new("./test_images")) // let mut dir: Vec<PathBuf> = fs::read_dir(Path::new("./test_images"))
.unwrap() // .unwrap()
.map(|f| f.unwrap().path()) // .map(|f| f.unwrap().path())
.collect(); // .collect();
dir.sort(); // dir.sort();
let mut res = Self { // let mut res = Self {
is_playing: false, // is_playing: false,
queued_ticks: 0, // queued_ticks: 0,
speed: 5, // speed: 5,
next_speed: None, // next_speed: None,
version: 0, // version: 0,
image_filter_method: FilterMethod::Nearest, // image_filter_method: FilterMethod::Nearest,
width: 1400, // width: 1400,
current_image: Some(dir.first().unwrap().clone()), // current_image: Some(dir.first().unwrap().clone()),
available_images: dir, // available_images: dir,
current_image_id: 0, // current_image_id: 0,
loaded_images: HashMap::new(), // loaded_images: HashMap::new(),
}; // };
let _ = res.update(Message::Next(0)); // let _ = res.update(Message::Next(0));
res // res
} // }
fn update(&mut self, message: Message) -> Task<Message> { // fn update(&mut self, message: Message) -> Task<Message> {
match message { // match message {
Message::Tick => { // Message::Tick => {
self.queued_ticks = (self.queued_ticks + 1).min(self.speed); // self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
// if let Some(task) = self.grid.tick(self.queued_ticks) { // // if let Some(task) = self.grid.tick(self.queued_ticks) {
// if let Some(speed) = self.next_speed.take() { // // if let Some(speed) = self.next_speed.take() {
// self.speed = speed; // // 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)); // // // return Task::perform(task, Message::Grid.with(version));
// } // // }
} // }
Message::TogglePlayback => { // Message::TogglePlayback => {
self.is_playing = !self.is_playing; // self.is_playing = !self.is_playing;
} // }
Message::ToggleGrid(show_grid_lines) => { // Message::ToggleGrid(show_grid_lines) => {
// self.grid.toggle_lines(show_grid_lines); // // self.grid.toggle_lines(show_grid_lines);
} // }
Message::Clear => { // Message::Clear => {
// self.grid.clear(); // // self.grid.clear();
self.version += 1; // self.version += 1;
} // }
Message::SpeedChanged(speed) => { // Message::SpeedChanged(speed) => {
if self.is_playing { // if self.is_playing {
self.next_speed = Some(speed.round() as usize); // self.next_speed = Some(speed.round() as usize);
} else { // } else {
self.speed = speed.round() as usize; // self.speed = speed.round() as usize;
} // }
} // }
Message::ImageWidthChanged(image_width) => { // Message::ImageWidthChanged(image_width) => {
self.width = image_width; // self.width = image_width;
} // }
Message::ImageUseNearestToggled(use_nearest) => { // Message::ImageUseNearestToggled(use_nearest) => {
self.image_filter_method = if use_nearest { // self.image_filter_method = if use_nearest {
FilterMethod::Nearest // FilterMethod::Nearest
} else { // } else {
FilterMethod::Linear // FilterMethod::Linear
}; // };
} // }
Message::Next(change) => { // Message::Next(change) => {
let elements = self.available_images.len() as i32; // let elements = self.available_images.len() as i32;
let new_id = (self.current_image_id as i32 + change).clamp(0, elements - 1); // let new_id = (self.current_image_id as i32 + change).clamp(0, elements - 1);
println!( // println!(
"updated id: {} from {} total {}", // "updated id: {} from {} total {}",
new_id, self.current_image_id, elements // new_id, self.current_image_id, elements
); // );
self.current_image_id = new_id as usize; // self.current_image_id = new_id as usize;
let path = self // let path = self
.available_images // .available_images
.get(self.current_image_id) // .get(self.current_image_id)
.unwrap() // .unwrap()
.clone(); // .clone();
self.current_image = Some(path.clone()); // self.current_image = Some(path.clone());
if !self.loaded_images.contains_key(&path.to_path_buf()) { // if !self.loaded_images.contains_key(&path.to_path_buf()) {
// self.loaded_images.insert( // // self.loaded_images.insert(
// path.to_path_buf(), // // path.to_path_buf(),
// load_thumbnail(path.to_str().unwrap(), Approach::ImageRs).unwrap(), // // load_thumbnail(path.to_str().unwrap(), Approach::ImageRs).unwrap(),
// ); // // );
} // }
} // }
} // }
Task::none() // Task::none()
} // }
fn subscription(&self) -> Subscription<Message> { // fn subscription(&self) -> Subscription<Message> {
keyboard::on_key_press(|key, _modifiers| match key { // 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::ArrowRight) => Some(Message::Next(1)),
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(Message::Next(-1)), // keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => Some(Message::Next(-1)),
_ => None, // _ => None,
}) // })
} // }
fn view(&self) -> Element<'_, Message> { // fn view(&self) -> Element<'_, Message> {
let version = self.version; // let version = self.version;
let selected_speed = self.next_speed.unwrap_or(self.speed); // let selected_speed = self.next_speed.unwrap_or(self.speed);
let controls = view_controls( // let controls = view_controls(
self.is_playing, // self.is_playing,
true, // true,
// self.grid.are_lines_visible(), // // self.grid.are_lines_visible(),
selected_speed, // selected_speed,
// self.grid.preset(), // // self.grid.preset(),
); // );
let content = column![ // let content = column![
// image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into(), // // 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.grid.view().map(Message::Grid.with(version)),
self.image(), // self.image(),
controls, // controls,
] // ]
.height(Fill); // .height(Fill);
container(content).width(Fill).height(Fill).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() // // image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into()
} // }
fn image(&self) -> Column<Message> { // fn image(&self) -> Column<Message> {
let width = self.width; // let width = self.width;
let filter_method = self.image_filter_method; // let filter_method = self.image_filter_method;
Self::container("Image") // Self::container("Image")
.push("An image that tries to keep its aspect ratio.") // .push("An image that tries to keep its aspect ratio.")
.push(self.ferris( // .push(self.ferris(
width, // width,
filter_method, // filter_method,
self.current_image.as_ref().unwrap().as_ref(), // self.current_image.as_ref().unwrap().as_ref(),
)) // ))
.push(slider(100..=1500, width, Message::ImageWidthChanged)) // .push(slider(100..=1500, width, Message::ImageWidthChanged))
.push(text!("Width: {width} px").width(Fill).align_x(Center)) // .push(text!("Width: {width} px").width(Fill).align_x(Center))
.push( // .push(
checkbox( // checkbox(
"Use nearest interpolation", // "Use nearest interpolation",
filter_method == FilterMethod::Nearest, // filter_method == FilterMethod::Nearest,
) // )
.on_toggle(Message::ImageUseNearestToggled), // .on_toggle(Message::ImageUseNearestToggled),
) // )
.align_x(Center) // .align_x(Center)
} // }
fn container(title: &str) -> Column<'_, Message> { // fn container(title: &str) -> Column<'_, Message> {
column![text(title).size(50)].spacing(20) // column![text(title).size(50)].spacing(20)
} // }
fn ferris<'a>( // fn ferris<'a>(
&self, // &self,
width: u32, // width: u32,
filter_method: iced::widget::image::FilterMethod, // filter_method: iced::widget::image::FilterMethod,
path: &Path, // path: &Path,
) -> Container<'a, Message> { // ) -> Container<'a, Message> {
if self.loaded_images.get(path).is_none() { // if self.loaded_images.get(path).is_none() {
return center(text("loading")); // return center(text("loading"));
} // }
let img = iced::widget::image::Image::new(self.loaded_images.get(path).unwrap()); // let img = iced::widget::image::Image::new(self.loaded_images.get(path).unwrap());
center( // center(
// This should go away once we unify resource loading on native // // This should go away once we unify resource loading on native
// platforms // // platforms
img.filter_method(filter_method) // img.filter_method(filter_method)
.width(Length::Fixed(width as f32)), // .width(Length::Fixed(width as f32)),
) // )
} // }
} // }
impl Default for MainApp { // impl Default for MainApp {
fn default() -> Self { // fn default() -> Self {
Self::new() // Self::new()
} // }
} // }
fn view_controls<'a>( // fn view_controls<'a>(
is_playing: bool, // is_playing: bool,
is_grid_enabled: bool, // is_grid_enabled: bool,
speed: usize, // speed: usize,
// preset: Preset, // // preset: Preset,
) -> Element<'a, Message> { // ) -> Element<'a, Message> {
let playback_controls = row![ // let playback_controls = row![
button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback), // button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback),
button("Previous") // button("Previous")
.on_press(Message::Next(-1)) // .on_press(Message::Next(-1))
.style(button::secondary), // .style(button::secondary),
button("Next") // button("Next")
.on_press(Message::Next(1)) // .on_press(Message::Next(1))
.style(button::secondary), // .style(button::secondary),
] // ]
.spacing(10); // .spacing(10);
let speed_controls = row![ // let speed_controls = row![
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), // slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
text!("x{speed}").size(16), // text!("x{speed}").size(16),
] // ]
.align_y(Center) // .align_y(Center)
.spacing(10); // .spacing(10);
row![ // row![
playback_controls, // playback_controls,
speed_controls, // speed_controls,
// checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid), // // checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid),
// row![ // // row![
// pick_list(preset::ALL, Some(preset), Message::PresetPicked), // // pick_list(preset::ALL, Some(preset), Message::PresetPicked),
// button("Clear") // // button("Clear")
// .on_press(Message::Clear) // // .on_press(Message::Clear)
// .style(button::danger) // // .style(button::danger)
// ] // // ]
// .spacing(10) // // .spacing(10)
] // ]
.padding(10) // .padding(10)
.spacing(20) // .spacing(20)
.align_y(Center) // .align_y(Center)
.into() // .into()
} // }

145
src/store.rs Normal file
View File

@ -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<PathBuf, ImflowImageBuffer>,
pub(crate) loaded_images_thumbnails: HashMap<PathBuf, ImflowImageBuffer>,
pub(crate) available_images: Vec<PathBuf>,
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<PathBuf>,
}
impl ImageStore {
pub fn new(path: PathBuf) -> Self {
let current_image_id: usize = 0;
let loaded_images: HashMap<PathBuf, ImflowImageBuffer> = HashMap::new();
let mut loaded_thumbnails: HashMap<PathBuf, ImflowImageBuffer> = 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();
}
}