Orientation fix, WIP JXL

This commit is contained in:
Dawid Pietrykowski 2025-04-07 00:33:52 +02:00
parent b2dc693d0e
commit 417a2373c1
6 changed files with 290 additions and 35 deletions

View File

@ -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<JpegDecoder<R>> {
// 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<u8> = 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<u8> = 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);

View File

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

View File

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

View File

@ -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<u8>) -> 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<T>(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<u8>;
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 =
<JxlDecoder<std::io::BufReader<File>> 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<u8> = 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<u32> {
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<PathBuf> {
@ -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,
// }
// }

View File

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

View File

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