From b2dc693d0e40b515fdd8d0960a6d8818c0b9304e Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sun, 6 Apr 2025 17:24:17 +0200 Subject: [PATCH] Working HEIF, working aspect ratio, resize --- Cargo.lock | 48 +++++++++++++++ Cargo.toml | 1 + src/app.rs | 84 ++++++++++++++++++--------- src/image.rs | 151 +++++++++++++++++++++++++++++++++++++----------- src/shader.wgsl | 9 ++- src/store.rs | 6 +- 6 files changed, 232 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f4f61b..068412a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,6 +1553,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -1788,6 +1799,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "four-cc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629" + [[package]] name = "futures" version = "0.3.31" @@ -2666,6 +2683,7 @@ dependencies = [ "iced", "image 0.25.6", "itertools 0.12.1", + "libheif-rs", "memmap2", "minifb", "pollster", @@ -3019,6 +3037,30 @@ dependencies = [ "cc", ] +[[package]] +name = "libheif-rs" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a26370abb4723a3ce73083e479b98017604206cadb0e35da5eac4813600d85" +dependencies = [ + "enumn", + "four-cc", + "libc", + "libheif-sys", +] + +[[package]] +name = "libheif-sys" +version = "3.1.0+1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e663db80d4272b60c066c5a9d17370ffa0433a31d424152f95f1e1effb9b3860" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", + "walkdir", +] + [[package]] name = "libloading" version = "0.7.4" @@ -5363,6 +5405,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 78aa2ff..7172d68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ wgpu = "24.0.3" # winit = "0.30.9" zune-image = {version = "0.4.15", features = ["all"]} bytemuck = "1.22.0" +libheif-rs = "1.1.0" [profile.release] opt-level = 3 debug = false diff --git a/src/app.rs b/src/app.rs index 3b0754f..478bc57 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,11 @@ use crate::egui_tools::EguiRenderer; -use eframe::WindowAttributes; use egui::{Event, Key}; use egui_wgpu::wgpu::SurfaceError; use egui_wgpu::{ScreenDescriptor, wgpu}; use imflow::store::ImageStore; -use std::any::Any; use std::path::PathBuf; use std::process::exit; use std::sync::Arc; -use std::time; use wgpu::util::DeviceExt; use wgpu::{PipelineCompilationOptions, SurfaceConfiguration}; use winit::application::ApplicationHandler; @@ -23,20 +20,28 @@ use winit::window::{Window, WindowId}; #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct Transforms { transform: [f32; 16], // 4x4 matrix + width: u32, + height: u32, + _padding1: u32, + _padding2: u32, } -struct TransformData { +pub(crate) struct TransformData { pan_x: f32, pan_y: f32, zoom: f32, + width: u32, + height: u32, } -fn create_transform_matrix(data: &TransformData) -> [f32; 16] { +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, 0.0, 0.0, 0.0, 0.0, zoom, 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, ] } @@ -62,7 +67,7 @@ fn setup_texture( mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); @@ -101,7 +106,7 @@ fn setup_texture( }, wgpu::BindGroupLayoutEntry { binding: 2, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::all(), ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, @@ -225,7 +230,7 @@ impl AppState { window: &Window, width: u32, height: u32, - path: PathBuf + path: PathBuf, ) -> Self { let power_pref = wgpu::PowerPreference::default(); let adapter = instance @@ -279,12 +284,15 @@ impl AppState { let store = ImageStore::new(path); let (image_texture, bind_group, render_pipeline, transform_buffer) = - setup_texture(&device, surface_config.clone(), 6000, 4000); + // setup_texture(&device, surface_config.clone(), 6000, 4000); + setup_texture(&device, surface_config.clone(), 8192, 8192); let transform_data = TransformData { pan_x: 0.0, pan_y: 0.0, zoom: 1.0, + width: 10000, + height: 10000, }; Self { @@ -314,7 +322,7 @@ pub struct App { instance: wgpu::Instance, state: Option, window: Option>, - path: PathBuf + path: PathBuf, } impl App { @@ -324,7 +332,7 @@ impl App { instance, state: None, window: None, - path + path, } } @@ -346,7 +354,7 @@ impl App { &window, initial_width, initial_width, - self.path.clone() + self.path.clone(), ) .await; @@ -362,6 +370,7 @@ impl App { if width > 0 && height > 0 { self.state.as_mut().unwrap().resize_surface(width, height); } + self.pan_zoom(0.0, 0.0, 0.0); } pub fn update_texture(&mut self) { @@ -369,7 +378,7 @@ impl App { state.store.check_loaded_images(); let imbuf = if let Some(full) = state.store.get_current_image() { - println!("full"); + // println!("full"); full } else { state.store.get_thumbnail() @@ -378,11 +387,15 @@ impl App { let height = imbuf.height as u32; let buffer_u8 = unsafe { std::slice::from_raw_parts( - imbuf.argb_buffer.as_ptr() as *const u8, - imbuf.argb_buffer.len() * 4, + imbuf.rgba_buffer.as_ptr() as *const u8, + imbuf.rgba_buffer.len() * 4, ) }; + + state.transform_data.width = width; + state.transform_data.height = height; + state.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &state.image_texture, @@ -402,18 +415,38 @@ impl App { depth_or_array_layers: 1, }, ); + + self.pan_zoom(0.0, 0.0, 0.0); } 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 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; + let mut scale_y = 1.0; + if window_aspect_ratio > image_aspect_ratio { + scale_x = image_aspect_ratio / window_aspect_ratio; + } else { + scale_y = window_aspect_ratio / image_aspect_ratio; + } + state.transform_data.zoom = (state.transform_data.zoom + zoom_delta).clamp(1.0, 20.0); state.transform_data.pan_x += pan_x; state.transform_data.pan_y += pan_y; - let transform = create_transform_matrix(&state.transform_data); + let transform = create_transform_matrix(&state.transform_data, scale_x, scale_y); state.queue.write_buffer( &state.transform_buffer, 0, - bytemuck::cast_slice(&[Transforms { transform }]), + bytemuck::cast_slice(&[Transforms { + transform, + width: state.transform_data.width, + height: state.transform_data.height, + _padding1: 0, + _padding2: 0, + }]), ); } @@ -473,7 +506,7 @@ impl App { ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, - g: 1.0, // Green + g: 0.0, // Green b: 0.0, a: 1.0, }), @@ -611,7 +644,7 @@ impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let attributes = Window::default_attributes() .with_base_size(LogicalSize::new(2000, 4000)) - .with_resizable(false); + .with_resizable(true); let window = event_loop.create_window(attributes).unwrap(); pollster::block_on(self.set_window(window)); } @@ -630,7 +663,7 @@ impl ApplicationHandler for App { event_loop.exit(); } WindowEvent::RedrawRequested => { - let start = time::Instant::now(); + // let start = time::Instant::now(); self.handle_redraw(); // println!("Updated in: {}ms", start.elapsed().as_millis()); // Extract the events by cloning them from the input context @@ -677,12 +710,7 @@ impl ApplicationHandler for App { Key::Escape => exit(0), _ => {} } - } else if let Event::MouseWheel { - unit, - delta, - modifiers, - } = e - { + } else if let Event::MouseWheel { delta, .. } = e { self.pan_zoom(delta.y * 0.2, 0.0, 0.0); } else if let Event::PointerMoved(pos) = e { if keys_down.contains(&Key::Tab) { diff --git a/src/image.rs b/src/image.rs index c174e35..b179a0e 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,6 +1,7 @@ use iced::widget::image::Handle; use image::DynamicImage; use image::imageops::FilterType; +use libheif_rs::{HeifContext, LibHeif, RgbChroma}; use rexiv2::Metadata; use zune_image::codecs::jpeg::JpegDecoder; use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; @@ -17,7 +18,7 @@ use std::time::Instant; pub struct ImflowImageBuffer { pub width: usize, pub height: usize, - pub argb_buffer: Vec, + pub rgba_buffer: Vec, pub rating: i32, } @@ -25,11 +26,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 { - // if !path_exists(filename.clone()) { - // anyhow::bail!("File doesn't exist"); - // } - +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)); @@ -46,12 +43,19 @@ fn get_rating(filename: PathBuf) -> i32 { } } -pub fn load_image(path: PathBuf) -> ImflowImageBuffer { +pub fn load_image(path: &PathBuf) -> ImflowImageBuffer { let total_start = Instant::now(); + if is_heif(path) { + let img = load_heif(path, false); + let total_time = total_start.elapsed(); + println!("Total HEIF loading time: {:?}", total_time); + return img; + } + let file = read(path.clone()).unwrap(); let mut decoder = JpegDecoder::new(&file); - let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::BGRA); + let options = DecoderOptions::new_fast().jpeg_set_out_colorspace(ColorSpace::RGBA); decoder.set_options(options); decoder.decode_headers().unwrap(); @@ -80,30 +84,28 @@ pub fn load_image(path: PathBuf) -> ImflowImageBuffer { ImflowImageBuffer { width, height, - argb_buffer: buffer_u32, + rgba_buffer: buffer_u32, rating, } } -pub fn image_to_argb_buffer(img: DynamicImage) -> Vec { - 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 image_to_rgba_buffer(img: DynamicImage) -> Vec { + let flat = img.to_rgba8(); + let mut buffer = flat.to_vec(); + unsafe { + Vec::from_raw_parts( + buffer.as_mut_ptr() as *mut u32, + buffer.len() / 4, + buffer.len() / 4, + ) + } } pub fn load_available_images(dir: PathBuf) -> Vec { let mut files: Vec = fs::read_dir(dir) .unwrap() .map(|f| f.unwrap().path()) - .filter(|f| ["jpg", "heic"].contains(&f.extension().unwrap().to_ascii_lowercase().to_str().unwrap())) + .filter(is_image) .collect(); files.sort(); files @@ -124,7 +126,35 @@ pub fn get_embedded_thumbnail(path: PathBuf) -> Option> { } } +fn is_image(path: &PathBuf) -> bool { + if !path.is_file() { + return false; + } + ["jpg", "heic", "heif"].contains( + &path + .extension() + .unwrap() + .to_ascii_lowercase() + .to_str() + .unwrap(), + ) +} + +fn is_heif(path: &PathBuf) -> bool { + ["heif", "heic"].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); + } match load_thumbnail_exif(path) { Some(thumbnail) => return thumbnail, None => load_thumbnail_full(path), @@ -141,22 +171,22 @@ pub fn load_thumbnail_exif(path: &PathBuf) -> Option { 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 flat = image.into_rgba8().into_raw(); + let mut buffer = flat.to_vec(); + let buffer_u32 = unsafe { + Vec::from_raw_parts( + buffer.as_mut_ptr() as *mut u32, + buffer.len() / 4, + buffer.len() / 4, + ) + }; let rating = get_rating(path.into()); Some(ImflowImageBuffer { width, height, - argb_buffer: buffer, + rgba_buffer: buffer_u32, rating, }) } @@ -175,13 +205,66 @@ pub fn load_thumbnail_full(path: &PathBuf) -> ImflowImageBuffer { .resize(640, 480, FilterType::Nearest); let width = image.width() as usize; let height = image.height() as usize; - let buffer = image_to_argb_buffer(image); + let buffer = image_to_rgba_buffer(image); let rating = get_rating(path.into()); ImflowImageBuffer { width, height, - argb_buffer: buffer, + rgba_buffer: buffer, + rating, + } +} + +pub fn load_heif(path: &PathBuf, resize: bool) -> ImflowImageBuffer { + let lib_heif = LibHeif::new(); + let ctx = HeifContext::read_from_file(path.to_str().unwrap()).unwrap(); + let handle = ctx.primary_image_handle().unwrap(); + // assert_eq!(handle.width(), 1652); + // assert_eq!(handle.height(), 1791); + + // Get Exif + // let mut meta_ids: Vec = vec![0; 1]; + // let count = handle.metadata_block_ids(&mut meta_ids, b"Exif"); + // assert_eq!(count, 1); + // let exif: Vec = handle.metadata(meta_ids[0]).unwrap(); + + // Decode the image + let mut image = lib_heif + .decode(&handle, libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba), None) + .unwrap(); + assert_eq!( + image.color_space(), + Some(libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba)), + ); + + // Scale the image + if resize { + image = image.scale(640, 480, None).unwrap(); + assert_eq!(image.width(), 640); + assert_eq!(image.height(), 480); + } + + let width = image.width() as usize; + let height = image.height() as usize; + let rating = get_rating(path); + + // Get "pixels" + let planes = image.planes(); + let interleaved_plane = planes.interleaved.unwrap(); + assert!(!interleaved_plane.data.is_empty()); + assert!(interleaved_plane.stride > 0); + + let rgba_buffer = interleaved_plane.data; + // Create a slice of u32 from the u8 slice + let u32_slice = unsafe { + std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4) + }; + + ImflowImageBuffer { + width, + height, + rgba_buffer: u32_slice.to_vec(), rating, } } diff --git a/src/shader.wgsl b/src/shader.wgsl index e557902..0ed19ea 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,5 +1,7 @@ struct Transforms { transform: mat4x4, + width: u32, + height: u32 }; @group(0) @binding(2) var transforms: Transforms; @@ -16,7 +18,6 @@ struct VertexOutput { @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; - // Apply zoom/transformation matrix out.position = transforms.transform * vec4(in.position, 1.0); out.uv = in.uv; return out; @@ -27,5 +28,9 @@ fn vs_main(in: VertexInput) -> VertexOutput { @fragment fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { - return textureSample(texture, texture_sampler, uv); + let texture_size = vec2(f32(transforms.width), f32(transforms.height)); + let out_dim = vec2(textureDimensions(texture)); + let scale = texture_size / out_dim; + let pixel = uv * scale; + return textureSample(texture, texture_sampler, pixel); } diff --git a/src/store.rs b/src/store.rs index 802e187..0bef53f 100644 --- a/src/store.rs +++ b/src/store.rs @@ -53,7 +53,7 @@ impl ImageStore { ); let path = available_images[0].clone(); - let image = load_image(path.clone()); + let image = load_image(&path.clone()); loaded_images.insert(path, image); let mut state = Self { current_image_id, @@ -91,7 +91,7 @@ impl ImageStore { pub fn get_current_rating(&self) -> i32 { let imbuf = if let Some(full) = self.get_current_image() { - println!("full"); + // println!("full"); full } else { // TODO: this assumes loaded thumbnail @@ -122,7 +122,7 @@ impl ImageStore { self.currently_loading.insert(path.clone()); self.pool.execute(move || { - let image = load_image(path.clone()); + let image = load_image(&path.clone()); let _ = tx.send((path, image)); }); }