Optimizations, thumbnail strip, store RwLock

This commit is contained in:
Dawid Pietrykowski 2025-04-10 01:07:50 +02:00
parent da998ccbf1
commit 207878928f
7 changed files with 441 additions and 244 deletions

12
Cargo.lock generated
View File

@ -644,6 +644,15 @@ dependencies = [
"itertools 0.10.5", "itertools 0.10.5",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@ -1433,6 +1442,7 @@ dependencies = [
"bytemuck", "bytemuck",
"clap 4.5.34", "clap 4.5.34",
"criterion", "criterion",
"crossbeam-channel",
"egui", "egui",
"egui-wgpu", "egui-wgpu",
"egui-winit", "egui-winit",
@ -1441,10 +1451,12 @@ dependencies = [
"jpegxl-rs", "jpegxl-rs",
"libheif-rs", "libheif-rs",
"pollster", "pollster",
"rayon",
"rexiv2", "rexiv2",
"sha2", "sha2",
"threadpool", "threadpool",
"winit", "winit",
"zerocopy 0.8.24",
"zune-image", "zune-image",
] ]

View File

@ -22,6 +22,9 @@ rexiv2 = "0.10.0"
threadpool = "1.8.1" threadpool = "1.8.1"
bytemuck = "1.22.0" bytemuck = "1.22.0"
sha2 = "0.10.8" sha2 = "0.10.8"
zerocopy = "0.8.24"
crossbeam-channel = "0.5.15"
rayon = "1.10.0"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@ -1,11 +1,16 @@
use crate::egui_tools::EguiRenderer; use crate::egui_tools::EguiRenderer;
use egui::{Event, Key, PointerButton}; use egui::load::{ImageLoadResult, ImageLoader};
use egui::{Align2, Color32, ColorImage, Event, ImageSource, Key, PointerButton};
use egui_wgpu::wgpu::SurfaceError; use egui_wgpu::wgpu::SurfaceError;
use egui_wgpu::{ScreenDescriptor, wgpu}; use egui_wgpu::{ScreenDescriptor, wgpu};
use image::metadata::Orientation;
use imflow::image::swap_wh;
use imflow::store::ImageStore; use imflow::store::ImageStore;
use std::cmp::{max, min};
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::sync::Arc; use std::sync::{Arc, RwLock};
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use wgpu::{PipelineCompilationOptions, SurfaceConfiguration}; use wgpu::{PipelineCompilationOptions, SurfaceConfiguration};
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
@ -15,14 +20,13 @@ use winit::event_loop::ActiveEventLoop;
use winit::platform::x11::WindowAttributesExtX11; use winit::platform::x11::WindowAttributesExtX11;
use winit::window::{Window, WindowId}; use winit::window::{Window, WindowId};
// Uniforms for transformations
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Transforms { struct Transforms {
transform: [f32; 16], // 4x4 matrix transform: [f32; 16], // 4x4 matrix
width: u32, width: u32,
height: u32, height: u32,
_padding1: u32, orientation: u32,
_padding2: u32, _padding2: u32,
} }
@ -32,6 +36,7 @@ pub(crate) struct TransformData {
zoom: f32, zoom: f32,
width: u32, width: u32,
height: u32, height: u32,
orientation: Orientation,
} }
#[rustfmt::skip] #[rustfmt::skip]
@ -212,7 +217,7 @@ pub struct AppState {
pub surface: wgpu::Surface<'static>, pub surface: wgpu::Surface<'static>,
pub scale_factor: f32, pub scale_factor: f32,
pub egui_renderer: EguiRenderer, pub egui_renderer: EguiRenderer,
pub store: ImageStore, pub store: Arc<RwLock<ImageStore>>,
pub image_texture: wgpu::Texture, pub image_texture: wgpu::Texture,
pub bind_group: wgpu::BindGroup, pub bind_group: wgpu::BindGroup,
pub render_pipeline: wgpu::RenderPipeline, pub render_pipeline: wgpu::RenderPipeline,
@ -276,9 +281,13 @@ impl AppState {
let egui_renderer = EguiRenderer::new(&device, surface_config.format, None, 1, window); let egui_renderer = EguiRenderer::new(&device, surface_config.format, None, 1, window);
let scale_factor = 1.0; let store = Arc::new(RwLock::new(ImageStore::new(path)));
let store = ImageStore::new(path); let loader = ImflowEguiLoader::new(store.clone());
egui_renderer.context().add_image_loader(Arc::new(loader));
let scale_factor = 1.0;
let (image_texture, bind_group, render_pipeline, transform_buffer) = 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);
@ -290,6 +299,7 @@ impl AppState {
zoom: 1.0, zoom: 1.0,
width: 10000, width: 10000,
height: 10000, height: 10000,
orientation: Orientation::NoTransforms,
}; };
Self { Self {
@ -313,6 +323,10 @@ impl AppState {
self.surface_config.height = height; self.surface_config.height = height;
self.surface.configure(&self.device, &self.surface_config); self.surface.configure(&self.device, &self.surface_config);
} }
// fn get_store(&mut self) -> &ImageStore {
// &self.store.lock().unwrap()
// }
} }
pub struct App { pub struct App {
@ -358,7 +372,7 @@ impl App {
self.window.get_or_insert(window); self.window.get_or_insert(window);
self.state.get_or_insert(state); self.state.get_or_insert(state);
self.pan_zoom(0.0, 0.0, 0.0); self.reset_transform();
self.update_texture(); self.update_texture();
} }
@ -372,11 +386,13 @@ impl App {
pub fn update_texture(&mut self) { pub fn update_texture(&mut self) {
let state = self.state.as_mut().unwrap(); let state = self.state.as_mut().unwrap();
state.store.check_loaded_images(); {
let imbuf = if let Some(full) = state.store.get_current_image() { let mut store = state.store.write().unwrap();
store.check_loaded_images();
let imbuf = if let Some(full) = store.get_current_image() {
full full
} else { } else {
state.store.get_thumbnail() store.get_thumbnail()
}; };
let width = imbuf.width as u32; let width = imbuf.width as u32;
let height = imbuf.height as u32; let height = imbuf.height as u32;
@ -389,6 +405,7 @@ impl App {
state.transform_data.width = width; state.transform_data.width = width;
state.transform_data.height = height; state.transform_data.height = height;
state.transform_data.orientation = imbuf.orientation;
state.queue.write_texture( state.queue.write_texture(
wgpu::TexelCopyTextureInfo { wgpu::TexelCopyTextureInfo {
@ -409,15 +426,20 @@ impl App {
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
); );
}
self.pan_zoom(0.0, 0.0, 0.0); self.update_transform();
} }
fn update_transform(&mut self) { fn update_transform(&mut self) {
let state = self.state.as_mut().unwrap(); let state = self.state.as_mut().unwrap();
let image_aspect_ratio = let (width, height) = swap_wh(
(state.transform_data.width as f32) / (state.transform_data.height as f32); state.transform_data.width,
state.transform_data.height,
state.transform_data.orientation,
);
let image_aspect_ratio = (width as f32) / (height as f32);
let window_size = self.window.as_ref().unwrap().inner_size(); 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 window_aspect_ratio = window_size.width as f32 / window_size.height as f32;
let mut scale_x = 1.0; let mut scale_x = 1.0;
@ -433,9 +455,9 @@ impl App {
0, 0,
bytemuck::cast_slice(&[Transforms { bytemuck::cast_slice(&[Transforms {
transform, transform,
width: state.transform_data.width, width: width as u32,
height: state.transform_data.height, height: height as u32,
_padding1: 0, orientation: state.transform_data.orientation as u32,
_padding2: 0, _padding2: 0,
}]), }]),
); );
@ -603,10 +625,21 @@ impl App {
render_pass.draw_indexed(0..6, 0, 0..1); render_pass.draw_indexed(0..6, 0, 0..1);
} }
let rating = state.store.get_current_rating(); let rating;
let path = state.store.current_image_path.clone(); let path;
let filename = path.path.file_name().unwrap(); let current_id;
let window = self.window.as_ref().unwrap(); let image_count;
let filename;
let window;
{
let store = state.store.read().unwrap();
rating = store.get_current_rating();
path = store.current_image_path.clone();
current_id = store.current_image_id;
image_count = store.available_images.len();
filename = path.path.file_name().unwrap();
window = self.window.as_ref().unwrap();
}
{ {
state.egui_renderer.begin_frame(window); state.egui_renderer.begin_frame(window);
@ -617,7 +650,7 @@ impl App {
.show(state.egui_renderer.context(), |ui| { .show(state.egui_renderer.context(), |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label( ui.label(
egui::RichText::new(format!("{:.1}", rating)) egui::RichText::new(format!("{}", rating))
.size(42.0) .size(42.0)
.strong(), .strong(),
); );
@ -628,6 +661,59 @@ impl App {
); );
}); });
}); });
egui::Window::new("Id")
.collapsible(false)
.resizable(false)
.default_width(5.0)
.anchor(Align2::RIGHT_TOP, [-5.0, 5.0])
.pivot(Align2::RIGHT_TOP)
.show(state.egui_renderer.context(), |ui| {
ui.vertical_centered(|ui| {
ui.label(
egui::RichText::new(format!("{}/{}", current_id, image_count))
.size(22.0)
.strong(),
);
});
});
egui::Window::new("Images")
.collapsible(false)
.resizable(false)
.default_width(500.0)
.default_height(300.0)
.anchor(Align2::CENTER_BOTTOM, [0.0, 10.0])
.pivot(Align2::CENTER_BOTTOM)
.show(state.egui_renderer.context(), |ui| {
ui.horizontal(|ui| {
// ui.label(
// egui::RichText::new(format!("{}/{}", current_id, image_count))
// .size(22.0)
// .strong(),
// );
const NUM: i32 = 5;
for i in max((current_id as i32) - NUM, 0)
..min((current_id as i32) + NUM + 1, image_count as i32)
{
let source = ImageSource::Bytes {
uri: std::borrow::Cow::Owned(i.to_string()),
bytes: egui::load::Bytes::Static(&[]),
};
ui.add(
egui::Image::new(source)
// .sca
// .load_for_size(ctx, available_size)
// .fit_to_fraction(Vec2::new(10.0, 10.0))
// .max_width(200.0)
.fit_to_original_size(1.0)
.corner_radius(10),
);
}
// ui.image(source);
});
});
state.egui_renderer.end_frame_and_draw( state.egui_renderer.end_frame_and_draw(
&state.device, &state.device,
@ -668,14 +754,25 @@ impl ApplicationHandler for App {
} }
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
self.handle_redraw(); self.handle_redraw();
let (events, _keys_down, pointer) = self let (events, _keys_down, pointer, scroll) = self
.state .state
.as_ref() .as_ref()
.unwrap() .unwrap()
.egui_renderer .egui_renderer
.context() .context()
.input(|i| (i.events.clone(), i.keys_down.clone(), i.pointer.clone())); .input(|i| {
(
i.events.clone(),
i.keys_down.clone(),
i.pointer.clone(),
i.smooth_scroll_delta.clone(),
)
});
let mut updated_image = false;
let mut reset_transform = false;
{
let mut store = self.state.as_mut().unwrap().store.write().unwrap();
events.iter().for_each(|e| { events.iter().for_each(|e| {
if let Event::Key { key, pressed, .. } = e { if let Event::Key { key, pressed, .. } = e {
if !*pressed { if !*pressed {
@ -683,49 +780,54 @@ impl ApplicationHandler for App {
} }
match *key { match *key {
Key::ArrowLeft => { Key::ArrowLeft => {
self.state.as_mut().unwrap().store.next_image(-1); store.next_image(-1);
self.update_texture(); updated_image = true;
} }
Key::ArrowRight => { Key::ArrowRight => {
self.state.as_mut().unwrap().store.next_image(1); store.next_image(1);
self.update_texture(); updated_image = true;
} }
Key::ArrowUp => { Key::ArrowUp => {
let rating = let rating = store.get_current_rating();
self.state.as_mut().unwrap().store.get_current_rating(); store.set_rating(rating + 1);
self.state.as_mut().unwrap().store.set_rating(rating + 1);
} }
Key::ArrowDown => { Key::ArrowDown => {
let rating = let rating = store.get_current_rating();
self.state.as_mut().unwrap().store.get_current_rating(); store.set_rating(rating - 1);
self.state.as_mut().unwrap().store.set_rating(rating - 1);
} }
Key::Backtick => self.state.as_mut().unwrap().store.set_rating(0), Key::Backtick => store.set_rating(0),
Key::Num0 => self.state.as_mut().unwrap().store.set_rating(0), Key::Num0 => store.set_rating(0),
Key::Num1 => self.state.as_mut().unwrap().store.set_rating(1), Key::Num1 => store.set_rating(1),
Key::Num2 => self.state.as_mut().unwrap().store.set_rating(2), Key::Num2 => store.set_rating(2),
Key::Num3 => self.state.as_mut().unwrap().store.set_rating(3), Key::Num3 => store.set_rating(3),
Key::Num4 => self.state.as_mut().unwrap().store.set_rating(4), Key::Num4 => store.set_rating(4),
Key::Num5 => self.state.as_mut().unwrap().store.set_rating(5), Key::Num5 => store.set_rating(5),
Key::Escape => exit(0), Key::Escape => exit(0),
_ => {} _ => {}
} }
} else if let Event::MouseWheel { delta, .. } = e {
self.pan_zoom(delta.y * 0.2, 0.0, 0.0);
} else if let Event::PointerButton { } else if let Event::PointerButton {
button, pressed, .. button, pressed, ..
} = e } = e
{ {
if *pressed && *button == PointerButton::Secondary { if *pressed && *button == PointerButton::Secondary {
self.reset_transform(); reset_transform = true;
} }
} }
}); });
}
if pointer.primary_down() && pointer.is_moving() { if pointer.primary_down() && pointer.is_moving() {
self.pan_zoom(0.0, pointer.delta().x * 0.001, pointer.delta().y * -0.001); self.pan_zoom(0.0, pointer.delta().x * 0.001, pointer.delta().y * -0.001);
} }
if scroll.y != 0.0 {
self.pan_zoom(scroll.y * 0.01, 0.0, 0.0);
}
if updated_image {
self.update_texture();
}
if reset_transform {
self.reset_transform();
}
self.window.as_ref().unwrap().request_redraw(); self.window.as_ref().unwrap().request_redraw();
} }
WindowEvent::Resized(new_size) => { WindowEvent::Resized(new_size) => {
@ -735,3 +837,70 @@ impl ApplicationHandler for App {
} }
} }
} }
pub struct ImflowEguiLoader {
store: Arc<RwLock<ImageStore>>,
// stored: Option<ImageLoadResult>,
cache: egui::mutex::Mutex<HashMap<usize, ImageLoadResult>>,
}
impl ImflowEguiLoader {
pub fn new(store: Arc<RwLock<ImageStore>>) -> ImflowEguiLoader {
ImflowEguiLoader {
store,
cache: egui::mutex::Mutex::new(HashMap::new()),
}
}
}
impl ImageLoader for ImflowEguiLoader {
fn id(&self) -> &str {
"ImflowEguiLoader"
}
fn load(
&self,
_ctx: &egui::Context,
uri: &str,
_size_hint: egui::SizeHint,
) -> egui::load::ImageLoadResult {
let mut cache = self.cache.lock();
let id = uri.parse::<usize>().unwrap();
if let Some(handle) = cache.get(&id) {
handle.clone()
} else {
let imbuf = {
let binding = self.store.read().unwrap();
binding.get_thumbnail_id(id).clone()
};
let mut image = ColorImage::new([imbuf.width, imbuf.height], Color32::BLACK);
let image_buffer = image.as_raw_mut();
for (i, &value) in imbuf.rgba_buffer.iter().enumerate() {
let bytes = value.to_le_bytes();
let start = i * 4;
image_buffer[start..start + 4].copy_from_slice(&bytes);
}
let res = ImageLoadResult::Ok(egui::load::ImagePoll::Ready {
image: Arc::new(ColorImage {
size: [imbuf.width, imbuf.height],
pixels: image.pixels,
}),
});
cache.insert(id, res.clone());
res.clone()
}
}
// TODO
fn forget(&self, _uri: &str) {}
// TODO
fn forget_all(&self) {}
// TODO
fn byte_size(&self) -> usize {
todo!()
}
}

View File

@ -1,5 +1,6 @@
use image::DynamicImage; use image::DynamicImage;
use image::RgbaImage; use image::ImageBuffer;
use image::Rgba;
use image::imageops::FilterType; use image::imageops::FilterType;
use image::metadata::Orientation; use image::metadata::Orientation;
use itertools::Itertools; use itertools::Itertools;
@ -25,7 +26,6 @@ use std::io::BufReader;
use std::io::Cursor; use std::io::Cursor;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::mem;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::time::Instant; use std::time::Instant;
@ -79,29 +79,24 @@ pub struct ImflowImageBuffer {
pub height: usize, pub height: usize,
pub rgba_buffer: Vec<u32>, pub rgba_buffer: Vec<u32>,
pub rating: i32, pub rating: i32,
pub orientation: Orientation,
} }
pub fn get_rating(image: &ImageData) -> i32 { pub fn get_rating(image: &ImageData) -> i32 {
let meta = Metadata::new_from_path(&image.path); if let Ok(meta) = Metadata::new_from_path(&image.path) {
match meta { meta.get_tag_numeric("Xmp.xmp.Rating")
Ok(meta) => { } else {
let rating = meta.get_tag_numeric("Xmp.xmp.Rating"); 0
rating
}
Err(e) => panic!("{:?}", e),
} }
} }
pub fn get_orientation(path: &PathBuf) -> Orientation { pub fn get_orientation(path: &PathBuf) -> Orientation {
let meta = Metadata::new_from_path(path); Metadata::new_from_path(path).map_or(Orientation::NoTransforms, |meta| {
match meta { Orientation::from_exif(meta.get_orientation() as u8).unwrap()
Ok(meta) => Orientation::from_exif(meta.get_orientation() as u8) })
.unwrap_or(Orientation::NoTransforms),
Err(_) => Orientation::NoTransforms,
}
} }
fn swap_wh<T>(width: T, height: T, orientation: Orientation) -> (T, T) { pub fn swap_wh<T>(width: T, height: T, orientation: Orientation) -> (T, T) {
if [ if [
Orientation::Rotate90, Orientation::Rotate90,
Orientation::Rotate270, Orientation::Rotate270,
@ -161,15 +156,11 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
let (metadata, buffer) = decoder.decode_with::<u8>(&file).unwrap(); let (metadata, buffer) = decoder.decode_with::<u8>(&file).unwrap();
let width = metadata.width as usize; let width = metadata.width as usize;
let height = metadata.height as usize; let height = metadata.height as usize;
// TODO: convert
// let orientation = metadata.orientation;
let orientation = Orientation::NoTransforms;
let rgba_buffer = unsafe { let rgba_buffer = vec_u8_to_u32(buffer);
Vec::from_raw_parts(
buffer.as_ptr() as *mut u32,
buffer.len() / 4,
buffer.len() / 4,
)
};
std::mem::forget(buffer);
println!("Total JXL loading time: {:?}", total_start.elapsed()); println!("Total JXL loading time: {:?}", total_start.elapsed());
@ -178,6 +169,7 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
height, height,
rgba_buffer, rgba_buffer,
rating, rating,
orientation,
} }
} }
ImageFormat::Jpg => { ImageFormat::Jpg => {
@ -196,17 +188,21 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
buffer = vec![0; width * height * 4]; buffer = vec![0; width * height * 4];
decoder.decode_into(buffer.as_mut_slice()).unwrap(); decoder.decode_into(buffer.as_mut_slice()).unwrap();
let orientation_start = Instant::now();
// TODO: Optimize rotation
let orientation = image.orientation; let orientation = image.orientation;
let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap(); let rgba_buffer = vec_u8_to_u32(buffer);
let mut dynamic_image = DynamicImage::from(image); println!("Total loading time: {:?}", total_start.elapsed());
dynamic_image.apply_orientation(orientation); ImflowImageBuffer {
let buffer = dynamic_image.as_rgba8().unwrap(); width,
let (width, height) = swap_wh(width, height, orientation); height,
let orientation_time = orientation_start.elapsed(); rgba_buffer,
rating,
orientation,
}
}
}
}
// Reinterpret to avoid copying fn vec_u8_to_u32(buffer: Vec<u8>) -> Vec<u32> {
let rgba_buffer = unsafe { let rgba_buffer = unsafe {
Vec::from_raw_parts( Vec::from_raw_parts(
buffer.as_ptr() as *mut u32, buffer.as_ptr() as *mut u32,
@ -214,32 +210,27 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
buffer.len() / 4, buffer.len() / 4,
) )
}; };
std::mem::forget(dynamic_image); std::mem::forget(buffer);
let total_time = total_start.elapsed(); rgba_buffer
println!("Orientation time: {:?}", orientation_time); // bytemuck::cast_vec(buffer)
println!("Total loading time: {:?}", total_time);
ImflowImageBuffer {
width,
height,
rgba_buffer,
rating,
}
}
} }
fn vec_u32_to_u8(buffer: Vec<u32>) -> Vec<u8> {
let rgba_buffer = unsafe {
Vec::from_raw_parts(
buffer.as_ptr() as *mut u8,
buffer.len() * 4,
buffer.len() * 4,
)
};
std::mem::forget(buffer);
rgba_buffer
// bytemuck::cast_vec(buffer)
} }
pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec<u32> { pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec<u32> {
let flat = img.to_rgba8(); let flat: ImageBuffer<Rgba<u8>, Vec<u8>> = img.to_rgba8();
let mut buffer = flat.to_vec(); vec_u8_to_u32(flat.into_vec())
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<ImageData> { pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
@ -277,44 +268,41 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
} }
pub fn check_embedded_thumbnail(path: &PathBuf) -> bool { pub fn check_embedded_thumbnail(path: &PathBuf) -> bool {
if let Ok(meta) = Metadata::new_from_path(&path) { Metadata::new_from_path(path).map_or(false, |meta| meta.get_preview_images().is_some())
meta.get_preview_images().is_some()
} else {
false
}
} }
pub fn get_embedded_thumbnail(image: &ImageData) -> Option<Vec<u8>> { pub fn get_embedded_thumbnail(image: &ImageData) -> Option<Vec<u8>> {
let meta = Metadata::new_from_path(&image.path); Metadata::new_from_path(&image.path)
match meta { .ok()?
Ok(meta) => { .get_preview_images()?
if let Some(previews) = meta.get_preview_images() { .first()
for preview in previews { .and_then(|preview| preview.get_data().ok())
return Some(preview.get_data().unwrap());
}
}
None
}
Err(_) => None,
}
} }
pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer { pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
let cache_path = path.get_cache_path(); let cache_path = path.get_cache_path();
let mut buffer: Option<Vec<u8>> = None;
if cache_path.exists() { if cache_path.exists() {
let bytes = fs::read(cache_path).unwrap(); let read_bytes = fs::read(&cache_path).unwrap();
if read_bytes.len() != 0 {
buffer = Some(read_bytes);
}
}
if let Some(bytes) = buffer {
let width = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize; let width = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize;
let height = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; let height = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
let buffer: &[u8] = &bytes[8..]; let orientation =
let mut buffer_u8 = buffer.to_vec(); Orientation::from_exif(u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as u8)
.unwrap_or(Orientation::NoTransforms);
let (ptr, len, cap) = bytes.into_raw_parts();
assert!(ptr.align_offset(4) == 0);
let buffer_u32 = unsafe { let buffer_u32 = unsafe {
Vec::from_raw_parts( Vec::from_raw_parts(
buffer_u8.as_mut_ptr() as *mut u32, (ptr as usize + 12) as *mut u32,
buffer_u8.len() / 4, (len - 12) / 4,
buffer_u8.len() / 4, (cap - 12) / 4,
) )
}; };
std::mem::forget(buffer_u8);
assert_eq!(width * height, buffer_u32.len()); assert_eq!(width * height, buffer_u32.len());
@ -323,12 +311,13 @@ pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
height, height,
rgba_buffer: buffer_u32, rgba_buffer: buffer_u32,
rating: 0, rating: 0,
orientation,
}; };
} }
let thumbnail = if path.format == ImageFormat::Heif { let thumbnail = if path.format == ImageFormat::Heif {
load_heif(path, true) load_heif(path, true)
} else { } else {
load_thumbnail_exif(path).unwrap_or(load_thumbnail_full(path)) load_thumbnail_exif(path).unwrap_or_else(|| load_thumbnail_full(path))
}; };
save_thumbnail(&cache_path, thumbnail.clone()); save_thumbnail(&cache_path, thumbnail.clone());
@ -336,37 +325,27 @@ pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
} }
pub fn load_thumbnail_exif(path: &ImageData) -> Option<ImflowImageBuffer> { pub fn load_thumbnail_exif(path: &ImageData) -> Option<ImflowImageBuffer> {
match get_embedded_thumbnail(path) { if let Some(thumbnail) = get_embedded_thumbnail(path) {
Some(thumbnail) => {
let decoder = image::ImageReader::new(Cursor::new(thumbnail)) let decoder = image::ImageReader::new(Cursor::new(thumbnail))
.with_guessed_format() .with_guessed_format()
.unwrap(); .unwrap();
let mut image = decoder.decode().unwrap(); let image = decoder.decode().unwrap();
image.apply_orientation(path.orientation); let orientation = path.orientation;
let width: usize = image.width() as usize; let width: usize = image.width() as usize;
let height: usize = image.height() as usize; let height: usize = image.height() as usize;
let flat = image.into_rgba8().into_raw(); let rgba_buffer = image_to_rgba_buffer(image);
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,
)
};
std::mem::forget(buffer);
let rating = get_rating(path.into()); let rating = get_rating(path.into());
Some(ImflowImageBuffer { Some(ImflowImageBuffer {
width, width,
height, height,
rgba_buffer: buffer_u32, rgba_buffer,
rating, rating,
orientation,
}) })
} } else {
_ => None, None
} }
} }
@ -378,17 +357,21 @@ pub fn load_thumbnail_full(path: &ImageData) -> ImflowImageBuffer {
.unwrap() .unwrap()
.decode() .decode()
.unwrap() .unwrap()
.resize_to_fill(1920, 1920, FilterType::Lanczos3); .resize_to_fill(720, 720, FilterType::Nearest);
let width = image.width() as usize; let width = image.width() as usize;
let height = image.height() as usize; let height = image.height() as usize;
let start = std::time::Instant::now();
let buffer = image_to_rgba_buffer(image); let buffer = image_to_rgba_buffer(image);
println!("Elapsed: {:?}", start.elapsed());
let rating = get_rating(path.into()); let rating = get_rating(path.into());
let orientation = path.orientation;
ImflowImageBuffer { ImflowImageBuffer {
width, width,
height, height,
rgba_buffer: buffer, rgba_buffer: buffer,
rating, rating,
orientation,
} }
} }
@ -446,18 +429,25 @@ pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
assert_eq!(interleaved_plane.storage_bits_per_pixel, 32); assert_eq!(interleaved_plane.storage_bits_per_pixel, 32);
let rgba_buffer = interleaved_plane.data; let rgba_buffer = interleaved_plane.data;
let u32_slice = unsafe { let u32_slice = slice_u8_to_u32(rgba_buffer);
std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4)
};
ImflowImageBuffer { ImflowImageBuffer {
width, width,
height, height,
rgba_buffer: u32_slice.to_vec(), rgba_buffer: u32_slice.to_vec(),
rating, rating,
// TODO: verify
orientation: path.orientation,
} }
} }
fn slice_u8_to_u32(rgba_buffer: &[u8]) -> &[u32] {
let u32_slice = unsafe {
std::slice::from_raw_parts(rgba_buffer.as_ptr() as *const u32, rgba_buffer.len() / 4)
};
u32_slice
}
pub fn get_file_hash(path: &PathBuf) -> GenericArray<u8, U32> { pub fn get_file_hash(path: &PathBuf) -> GenericArray<u8, U32> {
let mut file = File::open(path).unwrap(); let mut file = File::open(path).unwrap();
let mut buf = [0u8; 16 * 1024]; let mut buf = [0u8; 16 * 1024];
@ -474,18 +464,11 @@ pub fn save_thumbnail(path: &PathBuf, image: ImflowImageBuffer) {
if !cache_dir.exists() { if !cache_dir.exists() {
fs::create_dir(cache_dir).unwrap(); fs::create_dir(cache_dir).unwrap();
} }
println!("path: {:?}", path);
let mut file = File::create(path).unwrap(); let mut file = File::create(path).unwrap();
let buffer = image.rgba_buffer; let u8_buffer = vec_u32_to_u8(image.rgba_buffer);
let u8_buffer = unsafe {
Vec::from_raw_parts(
buffer.as_ptr() as *mut u8,
buffer.len() * 4,
buffer.len() * 4,
)
};
std::mem::forget(buffer);
file.write(&(image.width as u32).to_le_bytes()).unwrap(); file.write(&(image.width as u32).to_le_bytes()).unwrap();
file.write(&(image.height as u32).to_le_bytes()).unwrap(); file.write(&(image.height as u32).to_le_bytes()).unwrap();
file.write(&(image.orientation.to_exif() as u32).to_le_bytes())
.unwrap();
file.write(&u8_buffer).unwrap(); file.write(&u8_buffer).unwrap();
} }

View File

@ -1,2 +1,3 @@
#![feature(vec_into_raw_parts)]
pub mod image; pub mod image;
pub mod store; pub mod store;

View File

@ -1,7 +1,8 @@
struct Transforms { struct Transforms {
transform: mat4x4<f32>, transform: mat4x4<f32>,
width: u32, width: u32,
height: u32 height: u32,
orientation: u32
}; };
@group(0) @binding(2) var<uniform> transforms: Transforms; @group(0) @binding(2) var<uniform> transforms: Transforms;
@ -26,11 +27,24 @@ fn vs_main(in: VertexInput) -> VertexOutput {
@group(0) @binding(0) var texture: texture_2d<f32>; @group(0) @binding(0) var texture: texture_2d<f32>;
@group(0) @binding(1) var texture_sampler: sampler; @group(0) @binding(1) var texture_sampler: sampler;
fn reverse(in: vec2<f32>) -> vec2<f32> {
return vec2<f32>(in.y, in.x);
}
@fragment @fragment
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> { fn fs_main(@location(0) in: vec2<f32>) -> @location(0) vec4<f32> {
let texture_size = vec2<f32>(f32(transforms.width), f32(transforms.height)); var texture_size = vec2<f32>(f32(transforms.width), f32(transforms.height));
let out_dim = vec2<f32>(textureDimensions(texture)); let out_dim = vec2<f32>(textureDimensions(texture));
var uv = in;
if transforms.orientation == 2 {
uv.x = 1.0-uv.x;
} else if transforms.orientation == 3 {
uv.y = 1.0-uv.y;
}
let scale = texture_size / out_dim; let scale = texture_size / out_dim;
let pixel = uv * scale; var pixel = uv * scale;
if transforms.orientation == 3 {
pixel = reverse(pixel);
}
return textureSample(texture, texture_sampler, pixel); return textureSample(texture, texture_sampler, pixel);
} }

View File

@ -1,24 +1,25 @@
use crate::image::{ImageData, load_thumbnail}; use crate::image::{ImageData, load_thumbnail};
use crate::image::{ImflowImageBuffer, load_available_images, load_image}; use crate::image::{ImflowImageBuffer, load_available_images, load_image};
use crossbeam_channel::{Receiver, Sender, unbounded};
use rayon::prelude::*;
use rexiv2::Metadata; use rexiv2::Metadata;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Instant; use std::time::Instant;
use threadpool::ThreadPool; use threadpool::ThreadPool;
const PRELOAD_NEXT_IMAGE_N: usize = 16; const PRELOAD_NEXT_IMAGE_N: usize = 16;
pub struct ImageStore { pub struct ImageStore {
pub(crate) current_image_id: usize, pub current_image_id: usize,
pub(crate) loaded_images: HashMap<ImageData, ImflowImageBuffer>, pub(crate) loaded_images: HashMap<ImageData, ImflowImageBuffer>,
pub(crate) loaded_images_thumbnails: HashMap<ImageData, ImflowImageBuffer>, pub(crate) loaded_images_thumbnails: HashMap<ImageData, ImflowImageBuffer>,
pub(crate) available_images: Vec<ImageData>, pub available_images: Vec<ImageData>,
pub current_image_path: ImageData, pub current_image_path: ImageData,
pub(crate) pool: ThreadPool, pub(crate) pool: ThreadPool,
pub(crate) loader_rx: mpsc::Receiver<(ImageData, ImflowImageBuffer)>, pub(crate) loader_rx: Receiver<(ImageData, ImflowImageBuffer)>,
pub(crate) loader_tx: mpsc::Sender<(ImageData, ImflowImageBuffer)>, pub(crate) loader_tx: Sender<(ImageData, ImflowImageBuffer)>,
pub(crate) currently_loading: HashSet<ImageData>, pub(crate) currently_loading: HashSet<ImageData>,
} }
@ -26,27 +27,33 @@ impl ImageStore {
pub fn new(path: PathBuf) -> Self { pub fn new(path: PathBuf) -> Self {
let current_image_id: usize = 0; let current_image_id: usize = 0;
let mut loaded_images: HashMap<ImageData, ImflowImageBuffer> = HashMap::new(); let mut loaded_images: HashMap<ImageData, ImflowImageBuffer> = HashMap::new();
let mut loaded_thumbnails: HashMap<ImageData, ImflowImageBuffer> = HashMap::new(); // let mut loaded_thumbnails: HashMap<ImageData, ImflowImageBuffer> = HashMap::new();
let available_images = load_available_images(path); let available_images = load_available_images(path);
let new_path = available_images[0].clone(); let new_path = available_images[0].clone();
let (loader_tx, loader_rx) = mpsc::channel(); let (loader_tx, loader_rx) = unbounded();
let pool = ThreadPool::new(32); let pool = ThreadPool::new(32);
let currently_loading = HashSet::new(); let currently_loading = HashSet::new();
let first_image_path = available_images[0].clone();
let first_image_thread = std::thread::spawn(move || {
let image = load_image(&first_image_path);
(first_image_path, image)
});
let total_start = Instant::now(); let total_start = Instant::now();
let mut loaded = 0; let (sender, receiver) = unbounded();
let to_load = available_images.len(); available_images
for path in &available_images { .par_iter()
.for_each_with(sender, |s, path| {
if path.embedded_thumbnail { if path.embedded_thumbnail {
let buf = load_thumbnail(path); let buf = load_thumbnail(path);
loaded_thumbnails.insert(path.clone(), buf); s.send((path.clone(), buf)).unwrap();
println!("Loaded embedded thumbnail for: {}/{}", loaded, to_load);
}
loaded += 1;
} }
});
let loaded_thumbnails: HashMap<_, _> = receiver.iter().collect();
let total_time = total_start.elapsed(); let total_time = total_start.elapsed();
println!( println!(
"all thumbnails load time: {:?} for {}", "all thumbnails load time: {:?} for {}",
@ -54,8 +61,10 @@ impl ImageStore {
loaded_thumbnails.len() loaded_thumbnails.len()
); );
let path = available_images[0].clone(); // let path = available_images[0].clone();
let image = load_image(&path.clone()); // let image = load_image(&path.clone());
// loaded_images.insert(path, image);
let (path, image) = first_image_thread.join().unwrap();
loaded_images.insert(path, image); loaded_images.insert(path, image);
let mut state = Self { let mut state = Self {
current_image_id, current_image_id,
@ -121,6 +130,7 @@ impl ImageStore {
} }
pub fn request_load(&mut self, path: ImageData) { pub fn request_load(&mut self, path: ImageData) {
// return;
if self.loaded_images.contains_key(&path) || self.currently_loading.contains(&path) { if self.loaded_images.contains_key(&path) || self.currently_loading.contains(&path) {
return; return;
} }
@ -161,6 +171,11 @@ impl ImageStore {
self.loaded_images.get(path) self.loaded_images.get(path)
} }
pub fn get_thumbnail_id(&self, id: usize) -> &ImflowImageBuffer {
let path = self.available_images.get(id).unwrap();
self.loaded_images_thumbnails.get(path).unwrap()
}
pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer { pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer {
if self if self
.loaded_images_thumbnails .loaded_images_thumbnails