From 21ec2db40f79b0a9d136d89645f99ee818a6d817 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sun, 30 Mar 2025 19:33:14 +0200 Subject: [PATCH] WIP thumbnails --- Cargo.lock | 22 +++++++++ Cargo.toml | 4 +- src/image.rs | 114 +++++++++++++++++++++++++++++++-------------- src/main.rs | 128 ++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 204 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d5fa3e..2944dfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,6 +1591,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gexiv2-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4edf7a47be383873c52eb34426723c7c9b040f9e58cf5088f2253cef149daf1" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "gif" version = "0.13.1" @@ -2106,6 +2116,7 @@ dependencies = [ "memmap2", "minifb", "pollster", + "rexiv2", "threadpool", "tokio", "tracing-subscriber", @@ -3792,6 +3803,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rexiv2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ecb9dccad43fea1bf58ea7bb8e34614446bea8aa00f78add6624d7ee26fb87" +dependencies = [ + "gexiv2-sys", + "libc", + "num-rational", +] + [[package]] name = "rgb" version = "0.8.50" diff --git a/Cargo.toml b/Cargo.toml index b9f04a7..215365a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ itertools = "0.12" memmap2 = "0.9.5" minifb = "0.28.0" pollster = "0.4.0" +rexiv2 = "0.10.0" threadpool = "1.8.1" # rustc-hash.workspace = true tokio = { version = "1.44.1", features = ["sync"] } @@ -20,9 +21,6 @@ tracing-subscriber = "0.3" wgpu = "24.0.3" winit = "0.30.9" zune-image = {version = "0.4.15", features = ["all"]} - -[profile.dev.package.zune-jpeg] -opt-level = 3 [profile.release] opt-level = 3 debug = false diff --git a/src/image.rs b/src/image.rs index b6f64b8..72b6763 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,13 +1,18 @@ use iced::widget::image::Handle; use iced::widget::image::Image as IcedImage; +// use image::codecs::jpeg::JpegDecoder; +// use image::codecs::jpeg::JpegDecoder; use image::DynamicImage; use image::ImageReader; +use itertools::Itertools; use memmap2::Mmap; +use zune_image::codecs::jpeg::JpegDecoder; 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::Read; use std::ops::Deref; @@ -53,6 +58,12 @@ pub fn read_zune_image(mmap: &[u8]) -> Result { } 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() } @@ -134,48 +145,67 @@ pub fn load_thumbnail( pub fn load_image_argb(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 mmap = map_file_path(path.clone()).unwrap(); let stage1_time = stage1_start.elapsed(); - println!("File mapping took: {:?}", stage1_time); - + // println!("File mapping took: {:?}", stage1_time); + + // let file = File::open(path).unwrap(); + let file_contents = read(path).unwrap(); + // let mmap = map_file_path(path.into()).unwrap(); + + let mut decoder = JpegDecoder::new(&file_contents); + let options = DecoderOptions::new_fast() + .jpeg_set_max_scans(5) + .jpeg_set_out_colorspace(zune_image::codecs::qoi::zune_core::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; + println!("{} x {}", width, height); + 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); - + // 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); + // 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]; + // let mut buffer: Vec = vec![0; width * height]; - for (rgba, argb) in flat.chunks_mut(3).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; - } + // 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, + argb_buffer: buffer.to_vec(), } } @@ -187,26 +217,28 @@ pub struct ImflowImageBuffer { pub fn load_image_argb_imagers(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); - + // println!("File mapping took: {:?}", stage1_time); + // 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 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); - + // println!("Image decoding took: {:?}", stage2_time); + // 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); + // println!("Image flattening took: {:?}", stage3_time); // Stage 4: Convert to ARGB format let stage4_start = Instant::now(); @@ -219,12 +251,12 @@ pub fn load_image_argb_imagers(path: PathBuf) -> ImflowImageBuffer { *argb = r << 16 | g << 8 | b; } let stage4_time = stage4_start.elapsed(); - println!("RGBA to ARGB conversion took: {:?}", stage4_time); - + // 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, @@ -240,3 +272,15 @@ pub fn load_available_images(dir: PathBuf) -> Vec { files } +pub fn get_embedded_thumbnail(path: PathBuf) -> Option> { + let meta = rexiv2::Metadata::new_from_path(path); + match meta { + Ok(meta) => { + meta.get_thumbnail().map(|v| v.to_vec()) + } + Err(e) => None, + } + // let file = std::fs::File::open(path).ok()?; + // let exif = Reader::new().read_from_container(&mut std::io::BufReader::new(file)).ok()?; + // exif.get_thumbnail() +} diff --git a/src/main.rs b/src/main.rs index 44092ec..52ba032 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use std::cmp::min; use std::collections::{HashMap, HashSet}; use std::fs::{self}; -use std::io::Read; +use std::io::{Cursor, Read}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::time::{self, Duration, Instant}; @@ -20,15 +20,15 @@ use iced::widget::{ use iced::{Center, Element, Fill, Length, Size, Subscription, Task, Theme, keyboard}; use image::{self, DynamicImage, EncodableLayout, GenericImageView, ImageBuffer, ImageReader}; +use clap::Parser; use imflow::image::{ - Approach, ImflowImageBuffer, flatten_image_image, flatten_zune_image, load_available_images, - load_image_argb, load_image_argb_imagers, load_thumbnail, map_file, map_file_path, - read_zune_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 clap::Parser; // use image::io::Reader as ImageReader; // specifically for Reader use std::sync::{Arc, mpsc}; @@ -336,22 +336,25 @@ 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 + 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()); - loaded_images.insert(new_path.clone(), current_image); + // 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(); @@ -359,6 +362,35 @@ impl State { let currently_loading = HashSet::new(); + 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); + } + } + let mut state = Self { current_image_id, loaded_images, @@ -367,10 +399,13 @@ impl State { pool, loader_rx, loader_tx, - currently_loading + currently_loading, + loaded_images_thumbnails: loaded_thumbnails, }; - state.preload_next_images(min(state.available_images.len(), 64)); + // state.preload_next_images(min(state.available_images.len(), 1)); + + // let thumbnail_path = state.available_images[0].clone(); state } @@ -386,10 +421,11 @@ impl State { return; } let tx = self.loader_tx.clone(); - self.currently_loading.insert(path.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)); }); } @@ -407,7 +443,7 @@ impl State { 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.request_load(new_path.clone()); } self.current_image_path = new_path; } @@ -415,6 +451,41 @@ impl State { 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 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, + }; + + self.loaded_images_thumbnails.insert(path.clone(), buf); + } + panic!() + } } #[derive(Parser, Debug)] @@ -425,8 +496,8 @@ struct Args { fn main() { let args = Args::parse(); - const WIDTH: usize = 1920; - const HEIGHT: usize = 1080; + const WIDTH: usize = 2000; + const HEIGHT: usize = 1000; let mut window = Window::new( "Test - ESC to exit", WIDTH, @@ -441,27 +512,32 @@ fn main() { let path = args.path.unwrap_or("./test_images".into()); let mut state = State::new(path); - let mut waiting = true; + let mut waiting = false; window.set_key_repeat_delay(0.1); window.set_key_repeat_rate(0.1); + let thumbs: Vec<&ImflowImageBuffer> = state.loaded_images_thumbnails.values().collect(); + show_image(&mut window, thumbs[0]); + while window.is_open() && !window.is_key_down(Key::Escape) { window.update(); state.check_loaded_images(); - if window.is_key_pressed(Key::Right, minifb::KeyRepeat::Yes) { + if !waiting && window.is_key_pressed(Key::Right, minifb::KeyRepeat::Yes) { state.next_image(1); - waiting = true; - } else if window.is_key_pressed(Key::Left, minifb::KeyRepeat::Yes) { + 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); - waiting = true; - } - if waiting { - if let Some(image) = state.get_current_image(){ - waiting = false; - - show_image(&mut window, &image); - } + 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); + // } + // } } }