Working HEIF, working aspect ratio, resize

This commit is contained in:
Dawid Pietrykowski 2025-04-06 17:24:17 +02:00
parent ed66f17139
commit b2dc693d0e
6 changed files with 232 additions and 67 deletions

48
Cargo.lock generated
View File

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

View File

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

View File

@ -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<AppState>,
window: Option<Arc<Window>>,
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) {

View File

@ -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<u32>,
pub rgba_buffer: Vec<u32>,
pub rating: i32,
}
@ -25,11 +26,7 @@ pub fn create_iced_handle(width: u32, height: u32, rgba: Vec<u8>) -> 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<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 image_to_rgba_buffer(img: DynamicImage) -> Vec<u32> {
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<PathBuf> {
let mut files: Vec<PathBuf> = 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<Vec<u8>> {
}
}
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<ImflowImageBuffer> {
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 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<ItemId> = vec![0; 1];
// let count = handle.metadata_block_ids(&mut meta_ids, b"Exif");
// assert_eq!(count, 1);
// let exif: Vec<u8> = 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,
}
}

View File

@ -1,5 +1,7 @@
struct Transforms {
transform: mat4x4<f32>,
width: u32,
height: u32
};
@group(0) @binding(2) var<uniform> 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<f32>(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<f32>) -> @location(0) vec4<f32> {
return textureSample(texture, texture_sampler, uv);
let texture_size = vec2<f32>(f32(transforms.width), f32(transforms.height));
let out_dim = vec2<f32>(textureDimensions(texture));
let scale = texture_size / out_dim;
let pixel = uv * scale;
return textureSample(texture, texture_sampler, pixel);
}

View File

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