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::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()));
},
);
// 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("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));
}
});
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_group!(benches, thumbnail_load_benchmark);
criterion_main!(benches);

View File

@ -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<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 width: usize,
pub height: usize,
pub argb_buffer: Vec<u32>,
}
pub fn image_to_argb_buffer(img: DynamicImage) -> Vec<u32> {
let mut buffer: Vec<u32> = 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<u8>) -> 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<u8> = 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<u32> = 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<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> {
let mut files: Vec<PathBuf> = fs::read_dir(dir)
.unwrap()
@ -282,15 +85,25 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option<Vec<u8>> {
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()) {
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<ImflowImageBuffer> {
match get_embedded_thumbnail(path.clone()) {
Some(thumbnail) => {
let decoder = image::ImageReader::new(Cursor::new(thumbnail))
.with_guessed_format()
.unwrap();
@ -308,14 +121,25 @@ pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer {
*argb = r << 16 | g << 8 | b;
}
ImflowImageBuffer {
Some(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);
_ => 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);
@ -326,4 +150,3 @@ pub fn load_thumbnail(path: PathBuf) -> ImflowImageBuffer {
argb_buffer: buffer,
}
}
}

View File

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

View File

@ -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<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)]
#[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);
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 !waiting && window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) {
state.next_image(-1);
show_image(&mut window, state.get_thumbnail());
// waiting = true;
waiting = true;
}
// if waiting {
// if let Some(image) = state.get_current_image() {
// waiting = false;
} else if window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) {
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());
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();
}
struct MainApp {
is_playing: bool,
queued_ticks: usize,
speed: usize,
next_speed: Option<usize>,
version: usize,
image_filter_method: FilterMethod,
current_image: Option<PathBuf>,
width: u32,
available_images: Vec<PathBuf>,
current_image_id: usize,
loaded_images: HashMap<PathBuf, iced::widget::image::Handle>,
}
#[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<PathBuf> = 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<Message> {
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;
// struct MainApp {
// is_playing: bool,
// queued_ticks: usize,
// speed: usize,
// next_speed: Option<usize>,
// version: usize,
// image_filter_method: FilterMethod,
// current_image: Option<PathBuf>,
// width: u32,
// available_images: Vec<PathBuf>,
// current_image_id: usize,
// loaded_images: HashMap<PathBuf, iced::widget::image::Handle>,
// }
// self.queued_ticks = 0;
// let version = self.version;
// // return Task::perform(task, Message::Grid.with(version));
// #[derive(Debug, Clone)]
// enum Message {
// TogglePlayback,
// ToggleGrid(bool),
// Clear,
// SpeedChanged(f32),
// Tick,
// Next(i32),
// ImageWidthChanged(u32),
// ImageUseNearestToggled(bool),
// }
}
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(),
// impl MainApp {
// fn new() -> Self {
// let mut dir: Vec<PathBuf> = 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<Message> {
// 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;
// // }
// // self.queued_ticks = 0;
// // 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(),
// // );
// }
// }
// }
Task::none()
}
// Task::none()
// }
fn subscription(&self) -> Subscription<Message> {
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<Message> {
// 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);
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<Message> {
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)
}
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)),
)
}
}
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);
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)
// 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,
// ]
// .spacing(10)
]
.padding(10)
.spacing(20)
.align_y(Center)
.into()
}
// .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()
// }
// fn image(&self) -> Column<Message> {
// 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)
// }
// 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)),
// )
// }
// }
// 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);
// 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()
// }

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();
}
}