diff --git a/benches/image_load.rs b/benches/image_load.rs index 98db437..f8a5ae7 100644 --- a/benches/image_load.rs +++ b/benches/image_load.rs @@ -1,15 +1,57 @@ #![allow(unused)] +use std::any::Any; +use std::fs::{File, read}; +use std::io::{BufReader, Cursor}; use std::iter; +use std::ops::Deref; +use std::path::PathBuf; use std::time::Duration; use criterion::{AxisScale, BenchmarkId, PlotConfiguration}; use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use image::codecs::jpeg::JpegDecoder; +use image::metadata::Orientation; +use image::{DynamicImage, ImageResult, RgbaImage}; use imflow::image::{ - load_available_images, load_image, load_thumbnail_exif, load_thumbnail_full + ImflowImageBuffer, get_orientation, get_rating, image_to_rgba_buffer, load_available_images, + load_image, load_thumbnail_exif, load_thumbnail_full, }; +use zune_image::codecs::jpeg::JpegDecoder as ZuneJpegDecoder; +use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; +use zune_image::codecs::qoi::zune_core::options::DecoderOptions; const PATH: &str = "test_images"; +/// Create a new decoder that decodes from the stream ```r``` +// pub fn new(r: R) -> ImageResult> { +// let mut input = Vec::new(); +// let mut r = r; +// r.read_to_end(&mut input)?; +// let options = DecoderOptions::default() +// .set_strict_mode(false) +// .set_max_width(usize::MAX) +// .set_max_height(usize::MAX); +// let mut decoder = ZuneJpegDecoder::new_with_options(input.as_slice(), options); +// decoder.decode_headers().map_err(ImageError::from_jpeg)?; +// // now that we've decoded the headers we can `.unwrap()` +// // all these functions that only fail if called before decoding the headers +// let (width, height) = decoder.dimensions().unwrap(); +// // JPEG can only express dimensions up to 65535x65535, so this conversion cannot fail +// let width: u16 = width.try_into().unwrap(); +// let height: u16 = height.try_into().unwrap(); +// let orig_color_space = decoder.get_output_colorspace().unwrap(); +// // Limits are disabled by default in the constructor for all decoders +// let limits = Limits::no_limits(); +// Ok(JpegDecoder { +// input, +// orig_color_space, +// width, +// height, +// limits, +// orientation: None, +// phantom: PhantomData, +// }) +// } // pub fn full_load_benchmark(c: &mut Criterion) { // let mut group = c.benchmark_group("image_decode"); @@ -33,6 +75,106 @@ const PATH: &str = "test_images"; // group.finish(); // } +fn load_a(path: &PathBuf) -> ImflowImageBuffer { + let file = read(path.clone()).unwrap(); + let mut decoder = ZuneJpegDecoder::new(&file); + let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA); + 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 buffer: Vec = vec![0; width * height * 4]; + decoder.decode_into(buffer.as_mut_slice()).unwrap(); + + // 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); + + // let total_time = total_start.elapsed(); + // println!("Total loading time: {:?}", total_time); + + let rating = get_rating(path); + + ImflowImageBuffer { + width, + height, + rgba_buffer: buffer_u32, + rating, + } +} + +fn load_b(path: &PathBuf) -> ImflowImageBuffer { + let file = read(path.clone()).unwrap(); + let mut decoder = ZuneJpegDecoder::new(&file); + let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA); + 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 buffer: Vec = vec![0; width * height * 4]; + decoder.decode_into(buffer.as_mut_slice()).unwrap(); + + let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap(); + let orientation = Orientation::from_exif(get_orientation(path)).unwrap(); + let mut dynamic_image = DynamicImage::from(image); + dynamic_image.apply_orientation(orientation); + + let rating = get_rating(path); + + let mut buffer = dynamic_image.to_rgba8(); + let buffer_u32 = unsafe { + Vec::from_raw_parts( + buffer.as_mut_ptr() as *mut u32, + buffer.len() / 4, + buffer.len() / 4, + ) + }; + std::mem::forget(buffer); + + ImflowImageBuffer { + width, + height, + rgba_buffer: buffer_u32, + rating, + } +} +// fn load_b(path: &PathBuf) -> ImflowImageBuffer { +// println!("path: {:?}", path); +// // let file = read(path.clone()).unwrap(); +// let file = BufReader::new(File::open(path).unwrap()); +// let decoder = image::ImageReader::new(file).unwrap(); +// let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA); +// decoder.set_options(options); +// let image = reader +// .with_guessed_format() +// .unwrap() +// .decode() +// .unwrap(); +// let width = image.width() as usize; +// let height = image.height() as usize; +// // let buffer = image_to_rgba_buffer(image); +// let im = RgbaImage::from_raw(width, height, image.as_rgba8()).unwrap(); +// let rating = get_rating(path.into()); + +// ImflowImageBuffer { +// width, +// height, +// rgba_buffer: buffer, +// rating, +// } +// } pub fn thumbnail_load_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("thumbnail"); @@ -57,5 +199,29 @@ pub fn thumbnail_load_benchmark(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, thumbnail_load_benchmark); +pub fn file_load_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("image_load"); + + group + .sample_size(10) + .measurement_time(Duration::from_millis(500)) + .warm_up_time(Duration::from_millis(200)); + + let images = load_available_images(PATH.into()); + group.bench_function("zune_jpeg", |b| { + for image in images.iter().take(10) { + b.iter(|| load_a(image)); + } + }); + group.bench_function("image_rs", |b| { + for image in images.iter().take(10) { + b.iter(|| load_b(image)); + } + }); + + group.finish(); +} + +// criterion_group!(benches, thumbnail_load_benchmark); +criterion_group!(benches, file_load_benchmark); criterion_main!(benches); diff --git a/src/app.rs b/src/app.rs index 478bc57..5a974f2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -34,14 +34,16 @@ pub(crate) struct TransformData { height: u32, } +#[rustfmt::skip] fn create_transform_matrix(data: &TransformData, scale_x: f32, scale_y: f32) -> [f32; 16] { const ZOOM_MULTIPLIER: f32 = 3.0; let zoom = data.zoom.powf(ZOOM_MULTIPLIER); + [ - zoom * scale_x, 0.0, 0.0, 0.0, - 0.0, zoom * scale_y, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - data.pan_x, data.pan_y, 0.0, 1.0, + zoom * scale_x, 0.0, 0.0, 0.0, + 0.0, zoom * scale_y, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + data.pan_x, data.pan_y, 0.0, 1.0, ] } @@ -338,8 +340,8 @@ impl App { async fn set_window(&mut self, window: Window) { let window = Arc::new(window); - let initial_width = 1500; - let initial_height = 1000; + let initial_height = 1200; + let initial_width = (initial_height as f32 * 1.5) as u32; let _ = window.request_inner_size(PhysicalSize::new(initial_width, initial_height)); @@ -366,7 +368,6 @@ impl App { } fn handle_resized(&mut self, width: u32, height: u32) { - println!("Resized {} {}", width, height); if width > 0 && height > 0 { self.state.as_mut().unwrap().resize_surface(width, height); } @@ -378,7 +379,6 @@ impl App { state.store.check_loaded_images(); let imbuf = if let Some(full) = state.store.get_current_image() { - // println!("full"); full } else { state.store.get_thumbnail() @@ -392,7 +392,6 @@ impl App { ) }; - state.transform_data.width = width; state.transform_data.height = height; @@ -422,7 +421,8 @@ impl App { pub fn pan_zoom(&mut self, zoom_delta: f32, pan_x: f32, pan_y: f32) { let state = self.state.as_mut().unwrap(); - let image_aspect_ratio = (state.transform_data.width as f32) / (state.transform_data.height as f32); + let image_aspect_ratio = + (state.transform_data.width as f32) / (state.transform_data.height as f32); let window_size = self.window.as_ref().unwrap().inner_size(); let window_aspect_ratio = window_size.width as f32 / window_size.height as f32; let mut scale_x = 1.0; diff --git a/src/egui_tools.rs b/src/egui_tools.rs index d1c2a04..4dda2f3 100644 --- a/src/egui_tools.rs +++ b/src/egui_tools.rs @@ -1,6 +1,6 @@ use egui::Context; use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView}; -use egui_wgpu::{wgpu, Renderer, ScreenDescriptor}; +use egui_wgpu::{Renderer, ScreenDescriptor, wgpu}; use egui_winit::State; use winit::event::WindowEvent; use winit::window::Window; diff --git a/src/image.rs b/src/image.rs index b179a0e..3de9f72 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,17 +1,22 @@ use iced::widget::image::Handle; use image::DynamicImage; +use image::RgbaImage; use image::imageops::FilterType; +use image::metadata::Orientation; use libheif_rs::{HeifContext, LibHeif, RgbChroma}; use rexiv2::Metadata; use zune_image::codecs::jpeg::JpegDecoder; +use zune_image::codecs::jpeg_xl::JxlDecoder; use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; use zune_image::codecs::qoi::zune_core::options::DecoderOptions; +use zune_image::traits::DecoderTrait; use std::fs; use std::fs::File; use std::fs::read; use std::io::BufReader; use std::io::Cursor; +use std::mem; use std::path::PathBuf; use std::time::Instant; @@ -26,7 +31,7 @@ pub fn create_iced_handle(width: u32, height: u32, rgba: Vec) -> Handle { Handle::from_rgba(width, height, rgba) } -fn get_rating(filename: &PathBuf) -> i32 { +pub fn get_rating(filename: &PathBuf) -> i32 { // // Use xmp-toolkit for video files // if is_video(&filename) { // return Ok(read_rating_xmp(filename.clone()).unwrap_or(0)); @@ -43,6 +48,34 @@ fn get_rating(filename: &PathBuf) -> i32 { } } +pub fn get_orientation(filename: &PathBuf) -> u8 { + // // Use xmp-toolkit for video files + // if is_video(&filename) { + // return Ok(read_rating_xmp(filename.clone()).unwrap_or(0)); + // } + + // Use rexiv2 for image files + let meta = Metadata::new_from_path(filename); + match meta { + Ok(meta) => meta.get_orientation() as u8, + Err(e) => panic!("{:?}", e), + } +} + +fn swap_wh(width: T, height: T, orientation: Orientation) -> (T, T) { + if [ + Orientation::Rotate90, + Orientation::Rotate270, + Orientation::Rotate90FlipH, + Orientation::Rotate270FlipH, + ] + .contains(&orientation) + { + return (height, width); + } + (width, height) +} + pub fn load_image(path: &PathBuf) -> ImflowImageBuffer { let total_start = Instant::now(); @@ -53,25 +86,50 @@ pub fn load_image(path: &PathBuf) -> ImflowImageBuffer { return img; } - let file = read(path.clone()).unwrap(); - let mut decoder = JpegDecoder::new(&file); + if is_jxl(path) {} + let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA); - 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 buffer: Vec; + let width: usize; + let height: usize; + if is_jxl(path) { + let file = BufReader::new(File::open(path).unwrap()); + let mut decoder = JxlDecoder::try_new(file, options).unwrap(); + let image = + > as DecoderTrait<&[u8]>>::decode(&mut decoder) + .unwrap(); + (width, height) = image.dimensions(); + buffer = (*image.flatten_to_u8().get(0).unwrap().clone()).to_vec(); + println!("buffer len: {} {} {}", buffer.len(), width, height); + } else { + let file = read(path.clone()).unwrap(); + let mut decoder = JpegDecoder::new(&file); + decoder.set_options(options); - let mut buffer: Vec = vec![0; width * height * 4]; - decoder.decode_into(buffer.as_mut_slice()).unwrap(); + decoder.decode_headers().unwrap(); + let info = decoder.info().unwrap(); + width = info.width as usize; + height = info.height as usize; + buffer = vec![0; width * height * 4]; + decoder.decode_into(buffer.as_mut_slice()).unwrap(); + }; + + // TODO: Optimize rotation + // let orientation = + // Orientation::from_exif(get_orientation(path)).unwrap_or(Orientation::NoTransforms); + // let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap(); + // let mut dynamic_image = DynamicImage::from(image); + // dynamic_image.apply_orientation(orientation); + // let mut buffer = dynamic_image.to_rgba8(); + // let (width, height) = swap_wh(width, height, orientation); // 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, + buffer.len() / 4, ) }; std::mem::forget(buffer); @@ -92,13 +150,15 @@ pub fn load_image(path: &PathBuf) -> ImflowImageBuffer { pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec { let flat = img.to_rgba8(); let mut buffer = flat.to_vec(); - unsafe { + let vec = unsafe { Vec::from_raw_parts( buffer.as_mut_ptr() as *mut u32, buffer.len() / 4, buffer.len() / 4, ) - } + }; + mem::forget(buffer); + vec } pub fn load_available_images(dir: PathBuf) -> Vec { @@ -130,7 +190,7 @@ fn is_image(path: &PathBuf) -> bool { if !path.is_file() { return false; } - ["jpg", "heic", "heif"].contains( + ["jpg", "jxl", "heic", "heif"].contains( &path .extension() .unwrap() @@ -151,6 +211,17 @@ fn is_heif(path: &PathBuf) -> bool { ) } +fn is_jxl(path: &PathBuf) -> bool { + ["jxl", "jpgxl"].contains( + &path + .extension() + .unwrap() + .to_ascii_lowercase() + .to_str() + .unwrap(), + ) +} + pub fn load_thumbnail(path: &PathBuf) -> ImflowImageBuffer { if is_heif(path) { return load_heif(path, true); @@ -268,3 +339,23 @@ pub fn load_heif(path: &PathBuf, resize: bool) -> ImflowImageBuffer { rating, } } + +// fn load_jxl(path: &PathBuf) -> ImflowImageBuffer { +// let file = BufReader::new(File::open(path).unwrap()); +// let decoder = JxlDecoder::try_new(file, DecoderOptions::new_fast()).unwrap(); +// // let reader = image::ImageReader::new(file); +// let image = decoder +// .decode() +// .unwrap(); +// let width = image.width() as usize; +// let height = image.height() as usize; +// let buffer = image_to_rgba_buffer(image); +// let rating = get_rating(path.into()); + +// ImflowImageBuffer { +// width, +// height, +// rgba_buffer: buffer, +// rating, +// } +// } diff --git a/src/main.rs b/src/main.rs index 859a3e2..2ee1b84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -323,7 +323,6 @@ mod egui_tools; use winit::event_loop::{ControlFlow, EventLoop}; fn main() { - let args = Args::parse(); let path = args.path.unwrap_or("./test_images".into()); #[cfg(not(target_arch = "wasm32"))] @@ -379,7 +378,6 @@ async fn run(path: PathBuf) { // } } - struct MyApp { // image: Image, store: ImageStore, @@ -388,10 +386,7 @@ struct MyApp { impl MyApp { fn new(store: ImageStore, texture: TextureHandle) -> Self { - Self { - store, - texture, - } + Self { store, texture } } } @@ -425,7 +420,7 @@ struct Args { } // fn init_app() { - + // let mut store = ImageStore::new("./test_images".into()); // let mut imbuf = store.get_current_image().unwrap(); diff --git a/src/store.rs b/src/store.rs index 0bef53f..b111838 100644 --- a/src/store.rs +++ b/src/store.rs @@ -84,7 +84,10 @@ impl ImageStore { if let Some(full) = self.loaded_images.get_mut(&self.current_image_path.clone()) { full.rating = rating; } - if let Some(thumbnail) = self.loaded_images_thumbnails.get_mut(&self.current_image_path.clone()) { + if let Some(thumbnail) = self + .loaded_images_thumbnails + .get_mut(&self.current_image_path.clone()) + { thumbnail.rating = rating; } }