WIP thumbnails

This commit is contained in:
Dawid Pietrykowski 2025-03-30 19:33:14 +02:00
parent 24d71027f5
commit 21ec2db40f
4 changed files with 204 additions and 64 deletions

22
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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<ZuneImage, 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()
}
@ -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<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);
// 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<u32> = vec![0; width * height];
// let mut buffer: Vec<u32> = 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<PathBuf> {
files
}
pub fn get_embedded_thumbnail(path: PathBuf) -> Option<Vec<u8>> {
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()
}

View File

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