Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2d73b89df0 | ||
|
ec879c31b1 | ||
|
b00bbbadc0 | ||
|
b41554c608 | ||
|
207878928f | ||
|
da998ccbf1 | ||
|
2c5cd88eb4 |
78
Cargo.lock
generated
78
Cargo.lock
generated
@ -300,6 +300,15 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@ -581,6 +590,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@ -626,6 +644,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@ -657,6 +684,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.3.1"
|
||||
@ -750,6 +787,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@ -1021,6 +1068,16 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.4.3"
|
||||
@ -1385,6 +1442,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"clap 4.5.34",
|
||||
"criterion",
|
||||
"crossbeam-channel",
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
"egui-winit",
|
||||
@ -1393,9 +1451,12 @@ dependencies = [
|
||||
"jpegxl-rs",
|
||||
"libheif-rs",
|
||||
"pollster",
|
||||
"rayon",
|
||||
"rexiv2",
|
||||
"sha2",
|
||||
"threadpool",
|
||||
"winit",
|
||||
"zerocopy 0.8.24",
|
||||
"zune-image",
|
||||
]
|
||||
|
||||
@ -2804,6 +2865,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@ -3181,6 +3253,12 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
|
@ -21,6 +21,10 @@ itertools = "0.12"
|
||||
rexiv2 = "0.10.0"
|
||||
threadpool = "1.8.1"
|
||||
bytemuck = "1.22.0"
|
||||
sha2 = "0.10.8"
|
||||
zerocopy = "0.8.24"
|
||||
crossbeam-channel = "0.5.15"
|
||||
rayon = "1.10.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
@ -14,8 +14,8 @@ use image::codecs::jpeg::JpegDecoder;
|
||||
use image::metadata::Orientation;
|
||||
use image::{DynamicImage, ImageResult, RgbaImage};
|
||||
use imflow::image::{
|
||||
ImflowImageBuffer, get_orientation, get_rating, image_to_rgba_buffer, load_available_images,
|
||||
load_image, load_thumbnail_exif, load_thumbnail_full,
|
||||
ImflowImageBuffer, check_embedded_thumbnail, get_orientation, get_rating, image_to_rgba_buffer,
|
||||
load_available_images, load_image, load_thumbnail_exif, load_thumbnail_full,
|
||||
};
|
||||
use jpegxl_rs::Endianness;
|
||||
use jpegxl_rs::decode::{Data, PixelFormat, Pixels};
|
||||
@ -105,7 +105,7 @@ fn load_a(path: &PathBuf) -> ImflowImageBuffer {
|
||||
// let total_time = total_start.elapsed();
|
||||
// println!("Total loading time: {:?}", total_time);
|
||||
|
||||
let rating = get_rating(path);
|
||||
let rating = 0;
|
||||
|
||||
ImflowImageBuffer {
|
||||
width,
|
||||
@ -130,11 +130,11 @@ fn load_b(path: &PathBuf) -> ImflowImageBuffer {
|
||||
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 orientation = Orientation::from_exif(get_orientation(path)).unwrap();
|
||||
let mut dynamic_image = DynamicImage::from(image);
|
||||
dynamic_image.apply_orientation(orientation);
|
||||
// dynamic_image.apply_orientation(orientation);
|
||||
|
||||
let rating = get_rating(path);
|
||||
let rating = 0;
|
||||
|
||||
let mut buffer = dynamic_image.to_rgba8();
|
||||
let buffer_u32 = unsafe {
|
||||
@ -254,12 +254,12 @@ pub fn file_load_benchmark(c: &mut Criterion) {
|
||||
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));
|
||||
b.iter(|| load_a(&image.path));
|
||||
}
|
||||
});
|
||||
group.bench_function("image_rs", |b| {
|
||||
for image in images.iter().take(10) {
|
||||
b.iter(|| load_b(image));
|
||||
b.iter(|| load_b(&image.path));
|
||||
}
|
||||
});
|
||||
|
||||
@ -277,12 +277,29 @@ pub fn jxl_multithreading_benchmark(c: &mut Criterion) {
|
||||
let images = load_available_images("./test_images/jxl".into());
|
||||
group.bench_function("single", |b| {
|
||||
for image in images.iter().take(10) {
|
||||
b.iter(|| load_jxl_single(image));
|
||||
b.iter(|| load_jxl_single(&image.path));
|
||||
}
|
||||
});
|
||||
group.bench_function("multi", |b| {
|
||||
for image in images.iter().take(10) {
|
||||
b.iter(|| load_jxl_multi(image));
|
||||
b.iter(|| load_jxl_multi(&image.path));
|
||||
}
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
pub fn thumbnail_check_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("thumbnail_check");
|
||||
|
||||
group
|
||||
.sample_size(10)
|
||||
.measurement_time(Duration::from_millis(500))
|
||||
.warm_up_time(Duration::from_millis(200));
|
||||
|
||||
let images = load_available_images("./test_images/jxl".into());
|
||||
group.bench_function("has_thumbnail", |b| {
|
||||
for image in images.iter().take(10) {
|
||||
b.iter(|| check_embedded_thumbnail(&image.path));
|
||||
}
|
||||
});
|
||||
|
||||
@ -290,5 +307,6 @@ pub fn jxl_multithreading_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
// criterion_group!(benches, thumbnail_load_benchmark);
|
||||
// criterion_group!(benches, file_load_benchmark);
|
||||
criterion_group!(benches, jxl_multithreading_benchmark);
|
||||
// criterion_group!(benches, jxl_multithreading_benchmark);
|
||||
criterion_group!(benches, thumbnail_check_benchmark);
|
||||
criterion_main!(benches);
|
||||
|
415
src/app.rs
415
src/app.rs
@ -1,11 +1,18 @@
|
||||
use crate::egui_tools::EguiRenderer;
|
||||
use egui::{Event, Key, PointerButton};
|
||||
use egui::load::{ImageLoadResult, ImageLoader};
|
||||
use egui::{
|
||||
Align, Align2, Color32, ColorImage, Event, Image, ImageSource, Key, PointerButton, Sense,
|
||||
};
|
||||
use egui_wgpu::wgpu::SurfaceError;
|
||||
use egui_wgpu::{ScreenDescriptor, wgpu};
|
||||
use imflow::store::ImageStore;
|
||||
use image::metadata::Orientation;
|
||||
use imflow::image::{ImageData, ImageFormat, swap_wh};
|
||||
use imflow::store::{FileFilters, ImageStore};
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use wgpu::util::DeviceExt;
|
||||
use wgpu::{PipelineCompilationOptions, SurfaceConfiguration};
|
||||
use winit::application::ApplicationHandler;
|
||||
@ -15,14 +22,13 @@ use winit::event_loop::ActiveEventLoop;
|
||||
use winit::platform::x11::WindowAttributesExtX11;
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
// Uniforms for transformations
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct Transforms {
|
||||
transform: [f32; 16], // 4x4 matrix
|
||||
width: u32,
|
||||
height: u32,
|
||||
_padding1: u32,
|
||||
orientation: u32,
|
||||
_padding2: u32,
|
||||
}
|
||||
|
||||
@ -32,6 +38,7 @@ pub(crate) struct TransformData {
|
||||
zoom: f32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
orientation: Orientation,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
@ -212,12 +219,15 @@ pub struct AppState {
|
||||
pub surface: wgpu::Surface<'static>,
|
||||
pub scale_factor: f32,
|
||||
pub egui_renderer: EguiRenderer,
|
||||
pub store: ImageStore,
|
||||
pub store: Arc<RwLock<ImageStore>>,
|
||||
pub image_texture: wgpu::Texture,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
pub render_pipeline: wgpu::RenderPipeline,
|
||||
pub transform_buffer: wgpu::Buffer,
|
||||
pub transform_data: TransformData,
|
||||
pub filters: FileFilters,
|
||||
pub selected_image: ImageData,
|
||||
pub loaded_thumbnail: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -276,12 +286,19 @@ impl AppState {
|
||||
|
||||
let egui_renderer = EguiRenderer::new(&device, surface_config.format, None, 1, window);
|
||||
|
||||
let image_store = ImageStore::new(path);
|
||||
|
||||
// TODO: verify
|
||||
let selected_image = image_store.current_image_path.clone();
|
||||
let store = Arc::new(RwLock::new(image_store));
|
||||
|
||||
let loader = ImflowEguiLoader::new(store.clone());
|
||||
|
||||
egui_renderer.context().add_image_loader(Arc::new(loader));
|
||||
|
||||
let scale_factor = 1.0;
|
||||
|
||||
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(), 8192, 8192);
|
||||
|
||||
let transform_data = TransformData {
|
||||
@ -290,6 +307,7 @@ impl AppState {
|
||||
zoom: 1.0,
|
||||
width: 10000,
|
||||
height: 10000,
|
||||
orientation: Orientation::NoTransforms,
|
||||
};
|
||||
|
||||
Self {
|
||||
@ -305,6 +323,9 @@ impl AppState {
|
||||
render_pipeline,
|
||||
transform_buffer,
|
||||
transform_data,
|
||||
filters: FileFilters::default(),
|
||||
selected_image,
|
||||
loaded_thumbnail: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,6 +334,10 @@ impl AppState {
|
||||
self.surface_config.height = height;
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
}
|
||||
|
||||
// fn get_store(&mut self) -> &ImageStore {
|
||||
// &self.store.lock().unwrap()
|
||||
// }
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@ -358,8 +383,8 @@ impl App {
|
||||
self.window.get_or_insert(window);
|
||||
self.state.get_or_insert(state);
|
||||
|
||||
self.pan_zoom(0.0, 0.0, 0.0);
|
||||
self.update_texture();
|
||||
self.reset_transform();
|
||||
self.update_texture(true);
|
||||
}
|
||||
|
||||
fn handle_resized(&mut self, width: u32, height: u32) {
|
||||
@ -369,55 +394,83 @@ impl App {
|
||||
self.pan_zoom(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
pub fn update_texture(&mut self) {
|
||||
pub fn update_texture(&mut self, force: bool) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
if !force
|
||||
{
|
||||
let mut store = state.store.write().unwrap();
|
||||
store.check_loaded_images();
|
||||
let current_image_selected = state.selected_image == store.current_image_path;
|
||||
let current_quality_loaded =
|
||||
state.loaded_thumbnail == store.get_current_image().is_none();
|
||||
println!(
|
||||
"check {} {}",
|
||||
current_quality_loaded, current_image_selected
|
||||
);
|
||||
if current_image_selected && current_quality_loaded {
|
||||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut store = state.store.write().unwrap();
|
||||
let imbuf = if let Some(full) = store.get_current_image() {
|
||||
state.loaded_thumbnail = false;
|
||||
full
|
||||
} else {
|
||||
state.loaded_thumbnail = true;
|
||||
store.get_thumbnail()
|
||||
};
|
||||
let width = imbuf.width as u32;
|
||||
let height = imbuf.height as u32;
|
||||
let buffer_u8 = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
imbuf.rgba_buffer.as_ptr() as *const u8,
|
||||
imbuf.rgba_buffer.len() * 4,
|
||||
)
|
||||
};
|
||||
|
||||
state.store.check_loaded_images();
|
||||
let imbuf = if let Some(full) = state.store.get_current_image() {
|
||||
full
|
||||
} else {
|
||||
state.store.get_thumbnail()
|
||||
};
|
||||
let width = imbuf.width as u32;
|
||||
let height = imbuf.height as u32;
|
||||
let buffer_u8 = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
imbuf.rgba_buffer.as_ptr() as *const u8,
|
||||
imbuf.rgba_buffer.len() * 4,
|
||||
)
|
||||
};
|
||||
state.transform_data.width = width;
|
||||
state.transform_data.height = height;
|
||||
state.transform_data.orientation = imbuf.orientation;
|
||||
|
||||
state.transform_data.width = width;
|
||||
state.transform_data.height = height;
|
||||
state.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &state.image_texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&buffer_u8,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width), // 4 bytes per ARGB pixel
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
state.selected_image = store.current_image_path.clone();
|
||||
}
|
||||
|
||||
state.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &state.image_texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&buffer_u8,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width), // 4 bytes per ARGB pixel
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
self.pan_zoom(0.0, 0.0, 0.0);
|
||||
self.update_transform();
|
||||
}
|
||||
|
||||
fn update_transform(&mut self) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
|
||||
let image_aspect_ratio =
|
||||
(state.transform_data.width as f32) / (state.transform_data.height as f32);
|
||||
// TODO: Remove obviously
|
||||
if state.transform_data.width < 800 {
|
||||
state.transform_data.orientation = Orientation::NoTransforms;
|
||||
}
|
||||
let (width, height) = swap_wh(
|
||||
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_aspect_ratio = window_size.width as f32 / window_size.height as f32;
|
||||
let mut scale_x = 1.0;
|
||||
@ -433,9 +486,9 @@ impl App {
|
||||
0,
|
||||
bytemuck::cast_slice(&[Transforms {
|
||||
transform,
|
||||
width: state.transform_data.width,
|
||||
height: state.transform_data.height,
|
||||
_padding1: 0,
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
orientation: state.transform_data.orientation as u32,
|
||||
_padding2: 0,
|
||||
}]),
|
||||
);
|
||||
@ -603,10 +656,29 @@ impl App {
|
||||
render_pass.draw_indexed(0..6, 0, 0..1);
|
||||
}
|
||||
|
||||
let rating = state.store.get_current_rating();
|
||||
let path = state.store.current_image_path.clone();
|
||||
let filename = path.path.file_name().unwrap();
|
||||
let window = self.window.as_ref().unwrap();
|
||||
let mut rating_filter = [false; 6];
|
||||
|
||||
// let mut file_filters;
|
||||
let rating;
|
||||
let path;
|
||||
let current_id;
|
||||
let image_count;
|
||||
let filename;
|
||||
let window;
|
||||
let filtered_images;
|
||||
let current_image;
|
||||
let mut selected_image = None;
|
||||
{
|
||||
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();
|
||||
current_image = store.current_image_path.clone();
|
||||
filtered_images = store.get_filtered_images(&state.filters);
|
||||
filename = path.path.file_name().unwrap();
|
||||
window = self.window.as_ref().unwrap();
|
||||
}
|
||||
{
|
||||
state.egui_renderer.begin_frame(window);
|
||||
|
||||
@ -617,7 +689,7 @@ impl App {
|
||||
.show(state.egui_renderer.context(), |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{:.1}", rating))
|
||||
egui::RichText::new(format!("{}", rating))
|
||||
.size(42.0)
|
||||
.strong(),
|
||||
);
|
||||
@ -629,6 +701,51 @@ impl App {
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("Thumbnails")
|
||||
.exact_height(120.0)
|
||||
.show(state.egui_renderer.context(), |panel_ui| {
|
||||
egui::ScrollArea::horizontal().show(panel_ui, |ui| {
|
||||
ui.horizontal_centered(|horizontal| {
|
||||
for image in filtered_images {
|
||||
let source = ImageSource::Bytes {
|
||||
uri: std::borrow::Cow::Owned(image.get_hash_str()),
|
||||
bytes: egui::load::Bytes::Static(&[]),
|
||||
};
|
||||
|
||||
let image_widget = horizontal.add(
|
||||
egui::Image::new(source)
|
||||
.fit_to_original_size(0.8)
|
||||
.corner_radius(10)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
if current_image == image {
|
||||
image_widget.scroll_to_me(Some(Align::Center));
|
||||
}
|
||||
if image_widget.clicked() {
|
||||
println!("{}", image.get_hash_str());
|
||||
selected_image = Some(image);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::right("Filters").show(state.egui_renderer.context(), |ui| {
|
||||
for (i, mut rating) in state.filters.rating.iter_mut().enumerate().rev() {
|
||||
ui.checkbox(&mut rating, format!("{} stars", i));
|
||||
}
|
||||
|
||||
ui.text_edit_singleline(&mut state.filters.name);
|
||||
|
||||
for (format, mut value) in state.filters.file_format.iter_mut() {
|
||||
ui.checkbox(&mut value, format!("{}", format));
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(selected_image) = selected_image {
|
||||
state.store.write().unwrap().select_image(selected_image);
|
||||
}
|
||||
|
||||
state.egui_renderer.end_frame_and_draw(
|
||||
&state.device,
|
||||
&state.queue,
|
||||
@ -641,6 +758,8 @@ impl App {
|
||||
|
||||
state.queue.submit(Some(encoder.finish()));
|
||||
surface_texture.present();
|
||||
|
||||
self.update_texture(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,64 +787,80 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.handle_redraw();
|
||||
let (events, _keys_down, pointer) = self
|
||||
let (events, _keys_down, pointer, scroll) = self
|
||||
.state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.egui_renderer
|
||||
.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(),
|
||||
)
|
||||
});
|
||||
|
||||
events.iter().for_each(|e| {
|
||||
if let Event::Key { key, pressed, .. } = e {
|
||||
if !*pressed {
|
||||
return;
|
||||
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| {
|
||||
if let Event::Key { key, pressed, .. } = e {
|
||||
if !*pressed {
|
||||
return;
|
||||
}
|
||||
match *key {
|
||||
Key::ArrowLeft => {
|
||||
store.next_image(-1);
|
||||
updated_image = true;
|
||||
}
|
||||
Key::ArrowRight => {
|
||||
store.next_image(1);
|
||||
updated_image = true;
|
||||
}
|
||||
Key::ArrowUp => {
|
||||
let rating = store.get_current_rating();
|
||||
store.set_rating(rating + 1);
|
||||
}
|
||||
Key::ArrowDown => {
|
||||
let rating = store.get_current_rating();
|
||||
store.set_rating(rating - 1);
|
||||
}
|
||||
Key::Backtick => store.set_rating(0),
|
||||
Key::Num0 => store.set_rating(0),
|
||||
Key::Num1 => store.set_rating(1),
|
||||
Key::Num2 => store.set_rating(2),
|
||||
Key::Num3 => store.set_rating(3),
|
||||
Key::Num4 => store.set_rating(4),
|
||||
Key::Num5 => store.set_rating(5),
|
||||
Key::Escape => exit(0),
|
||||
_ => {}
|
||||
}
|
||||
} else if let Event::PointerButton {
|
||||
button, pressed, ..
|
||||
} = e
|
||||
{
|
||||
if *pressed && *button == PointerButton::Secondary {
|
||||
reset_transform = true;
|
||||
}
|
||||
}
|
||||
match *key {
|
||||
Key::ArrowLeft => {
|
||||
self.state.as_mut().unwrap().store.next_image(-1);
|
||||
self.update_texture();
|
||||
}
|
||||
Key::ArrowRight => {
|
||||
self.state.as_mut().unwrap().store.next_image(1);
|
||||
self.update_texture();
|
||||
}
|
||||
Key::ArrowUp => {
|
||||
let rating =
|
||||
self.state.as_mut().unwrap().store.get_current_rating();
|
||||
self.state.as_mut().unwrap().store.set_rating(rating + 1);
|
||||
}
|
||||
Key::ArrowDown => {
|
||||
let rating =
|
||||
self.state.as_mut().unwrap().store.get_current_rating();
|
||||
self.state.as_mut().unwrap().store.set_rating(rating - 1);
|
||||
}
|
||||
Key::Backtick => self.state.as_mut().unwrap().store.set_rating(0),
|
||||
Key::Num0 => self.state.as_mut().unwrap().store.set_rating(0),
|
||||
Key::Num1 => self.state.as_mut().unwrap().store.set_rating(1),
|
||||
Key::Num2 => self.state.as_mut().unwrap().store.set_rating(2),
|
||||
Key::Num3 => self.state.as_mut().unwrap().store.set_rating(3),
|
||||
Key::Num4 => self.state.as_mut().unwrap().store.set_rating(4),
|
||||
Key::Num5 => self.state.as_mut().unwrap().store.set_rating(5),
|
||||
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 {
|
||||
button, pressed, ..
|
||||
} = e
|
||||
{
|
||||
if *pressed && *button == PointerButton::Secondary {
|
||||
self.reset_transform();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
if pointer.primary_down() && pointer.is_moving() {
|
||||
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(false);
|
||||
}
|
||||
if reset_transform {
|
||||
self.reset_transform();
|
||||
}
|
||||
self.window.as_ref().unwrap().request_redraw();
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
@ -735,3 +870,71 @@ impl ApplicationHandler for App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImflowEguiLoader {
|
||||
store: Arc<RwLock<ImageStore>>,
|
||||
// stored: Option<ImageLoadResult>,
|
||||
cache: egui::mutex::Mutex<HashMap<String, 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();
|
||||
let id = uri.to_string();
|
||||
if let Some(handle) = cache.get(&id) {
|
||||
handle.clone()
|
||||
} else {
|
||||
let imbuf = {
|
||||
let binding = self.store.read().unwrap();
|
||||
binding.get_thumbnail_hash(id.clone()).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!()
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use egui::Context;
|
||||
use egui::{vec2, Color32, Context, Rangef, Style, Visuals};
|
||||
use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView};
|
||||
use egui_wgpu::{Renderer, ScreenDescriptor, wgpu};
|
||||
use egui_winit::State;
|
||||
@ -24,6 +24,15 @@ impl EguiRenderer {
|
||||
window: &Window,
|
||||
) -> EguiRenderer {
|
||||
let egui_context = Context::default();
|
||||
egui_context.options_mut(|o| o.line_scroll_speed = 200.0);
|
||||
egui_context.style_mut(|s| s.scroll_animation.duration = Rangef::new(0.1, 5.0));
|
||||
egui_context.set_visuals_of(
|
||||
egui::Theme::Dark,
|
||||
Visuals {
|
||||
panel_fill: Color32::BLACK,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let egui_state = egui_winit::State::new(
|
||||
egui_context,
|
||||
|
409
src/image.rs
409
src/image.rs
@ -1,5 +1,6 @@
|
||||
use image::DynamicImage;
|
||||
use image::RgbaImage;
|
||||
use image::ImageBuffer;
|
||||
use image::Rgba;
|
||||
use image::imageops::FilterType;
|
||||
use image::metadata::Orientation;
|
||||
use itertools::Itertools;
|
||||
@ -8,17 +9,29 @@ use jpegxl_rs::decode::PixelFormat;
|
||||
use jpegxl_rs::decoder_builder;
|
||||
use libheif_rs::{HeifContext, LibHeif, RgbChroma};
|
||||
use rexiv2::Metadata;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use sha2::digest::consts::U32;
|
||||
use sha2::digest::generic_array::GenericArray;
|
||||
use zune_image::codecs::jpeg::JpegDecoder;
|
||||
use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
|
||||
use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
|
||||
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
// use std::fmt::Write;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::fs::read;
|
||||
use std::hash::Hash;
|
||||
use std::io::BufReader;
|
||||
use std::io::Cursor;
|
||||
use std::mem;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd)]
|
||||
@ -28,39 +41,81 @@ pub enum ImageFormat {
|
||||
Heif,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd)]
|
||||
impl Display for ImageFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ImageFormat::Jpg => f.write_str("JPG"),
|
||||
ImageFormat::Jxl => f.write_str("JXL"),
|
||||
ImageFormat::Heif => f.write_str("HEIF"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImageData {
|
||||
pub path: PathBuf,
|
||||
pub format: ImageFormat,
|
||||
pub embedded_thumbnail: bool,
|
||||
pub orientation: Orientation,
|
||||
pub hash: GenericArray<u8, U32>,
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
pub fn get_cache_path(&self) -> PathBuf {
|
||||
let home_dir = PathBuf::from_str(&env::var("HOME").unwrap()).unwrap();
|
||||
let cache_dir = home_dir.join(".cache/imflow");
|
||||
if !cache_dir.exists() {
|
||||
fs::create_dir(&cache_dir).unwrap();
|
||||
}
|
||||
let hash_hex = format!("{:x}", self.hash);
|
||||
return cache_dir.join(hash_hex).to_path_buf();
|
||||
}
|
||||
|
||||
pub fn get_hash_str(&self) -> String {
|
||||
format!("{:x}", self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ImageData {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write(self.path.to_str().unwrap().as_bytes());
|
||||
state.write(self.hash.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ImageData {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.hash.eq(&other.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ImageData {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImflowImageBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub rgba_buffer: Vec<u32>,
|
||||
pub rating: i32,
|
||||
pub orientation: Orientation,
|
||||
}
|
||||
|
||||
pub fn get_rating(image: &ImageData) -> i32 {
|
||||
let meta = Metadata::new_from_path(&image.path);
|
||||
match meta {
|
||||
Ok(meta) => {
|
||||
let rating = meta.get_tag_numeric("Xmp.xmp.Rating");
|
||||
rating
|
||||
}
|
||||
Err(e) => panic!("{:?}", e),
|
||||
if let Ok(meta) = Metadata::new_from_path(&image.path) {
|
||||
meta.get_tag_numeric("Xmp.xmp.Rating")
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_orientation(image: &ImageData) -> u8 {
|
||||
let meta = Metadata::new_from_path(&image.path);
|
||||
match meta {
|
||||
Ok(meta) => meta.get_orientation() as u8,
|
||||
Err(e) => panic!("{:?}", e),
|
||||
}
|
||||
pub fn get_orientation(path: &PathBuf) -> Orientation {
|
||||
Metadata::new_from_path(path).map_or(Orientation::NoTransforms, |meta| {
|
||||
Orientation::from_exif(meta.get_orientation() as u8).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
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 [
|
||||
Orientation::Rotate90,
|
||||
Orientation::Rotate270,
|
||||
@ -92,6 +147,7 @@ fn get_format(path: &PathBuf) -> Option<ImageFormat> {
|
||||
}
|
||||
|
||||
pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
||||
// sleep(Duration::from_millis(500));
|
||||
let total_start = Instant::now();
|
||||
|
||||
match image.format {
|
||||
@ -120,15 +176,11 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
||||
let (metadata, buffer) = decoder.decode_with::<u8>(&file).unwrap();
|
||||
let width = metadata.width as usize;
|
||||
let height = metadata.height as usize;
|
||||
// TODO: convert
|
||||
// let orientation = metadata.orientation;
|
||||
let orientation = Orientation::NoTransforms;
|
||||
|
||||
let rgba_buffer = unsafe {
|
||||
Vec::from_raw_parts(
|
||||
buffer.as_ptr() as *mut u32,
|
||||
buffer.len() / 4,
|
||||
buffer.len() / 4,
|
||||
)
|
||||
};
|
||||
std::mem::forget(buffer);
|
||||
let rgba_buffer = vec_u8_to_u32(buffer);
|
||||
|
||||
println!("Total JXL loading time: {:?}", total_start.elapsed());
|
||||
|
||||
@ -137,6 +189,7 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
||||
height,
|
||||
rgba_buffer,
|
||||
rating,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
ImageFormat::Jpg => {
|
||||
@ -155,51 +208,49 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
||||
buffer = vec![0; width * height * 4];
|
||||
decoder.decode_into(buffer.as_mut_slice()).unwrap();
|
||||
|
||||
let orientation_start = Instant::now();
|
||||
// TODO: Optimize rotation
|
||||
let orientation =
|
||||
Orientation::from_exif(get_orientation(image)).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 buffer = dynamic_image.as_rgba8().unwrap();
|
||||
let (width, height) = swap_wh(width, height, orientation);
|
||||
let orientation_time = orientation_start.elapsed();
|
||||
|
||||
// Reinterpret to avoid copying
|
||||
let rgba_buffer = unsafe {
|
||||
Vec::from_raw_parts(
|
||||
buffer.as_ptr() as *mut u32,
|
||||
buffer.len() / 4,
|
||||
buffer.len() / 4,
|
||||
)
|
||||
};
|
||||
std::mem::forget(dynamic_image);
|
||||
let total_time = total_start.elapsed();
|
||||
println!("Orientation time: {:?}", orientation_time);
|
||||
println!("Total loading time: {:?}", total_time);
|
||||
let orientation = image.orientation;
|
||||
let rgba_buffer = vec_u8_to_u32(buffer);
|
||||
println!("Total loading time: {:?}", total_start.elapsed());
|
||||
ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer,
|
||||
rating,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_to_rgba_buffer(img: DynamicImage) -> Vec<u32> {
|
||||
let flat = img.to_rgba8();
|
||||
let mut buffer = flat.to_vec();
|
||||
let vec = unsafe {
|
||||
fn vec_u8_to_u32(buffer: Vec<u8>) -> Vec<u32> {
|
||||
let rgba_buffer = unsafe {
|
||||
Vec::from_raw_parts(
|
||||
buffer.as_mut_ptr() as *mut u32,
|
||||
buffer.as_ptr() as *mut u32,
|
||||
buffer.len() / 4,
|
||||
buffer.len() / 4,
|
||||
)
|
||||
};
|
||||
mem::forget(buffer);
|
||||
vec
|
||||
std::mem::forget(buffer);
|
||||
rgba_buffer
|
||||
// bytemuck::cast_vec(buffer)
|
||||
}
|
||||
|
||||
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> {
|
||||
let flat: ImageBuffer<Rgba<u8>, Vec<u8>> = img.to_rgba8();
|
||||
vec_u8_to_u32(flat.into_vec())
|
||||
}
|
||||
|
||||
pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
||||
@ -209,7 +260,28 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
||||
.sorted()
|
||||
.filter_map(|path| {
|
||||
if let Some(format) = get_format(&path) {
|
||||
Some(ImageData { path, format })
|
||||
let meta = Metadata::new_from_path(&path)
|
||||
.expect(&format!("Image has no metadata: {:?}", path).to_string());
|
||||
let embedded_thumbnail = if format == ImageFormat::Heif {
|
||||
let ctx = HeifContext::read_from_file(path.to_str().unwrap()).unwrap();
|
||||
let binding = ctx.top_level_image_handles();
|
||||
let handle = binding.get(0).unwrap();
|
||||
handle.number_of_thumbnails() > 0
|
||||
} else {
|
||||
meta.get_preview_images().is_some()
|
||||
};
|
||||
let orientation = Orientation::from_exif(meta.get_orientation() as u8)
|
||||
.unwrap_or(Orientation::NoTransforms);
|
||||
let hash = get_file_hash(&path);
|
||||
let rating = meta.get_tag_numeric("Xmp.xmp.Rating");
|
||||
Some(ImageData {
|
||||
path,
|
||||
format,
|
||||
embedded_thumbnail,
|
||||
orientation,
|
||||
hash,
|
||||
rating,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -217,61 +289,103 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
||||
.collect::<Vec<ImageData>>()
|
||||
}
|
||||
|
||||
pub fn check_embedded_thumbnail(path: &PathBuf) -> bool {
|
||||
Metadata::new_from_path(path).map_or(false, |meta| meta.get_preview_images().is_some())
|
||||
}
|
||||
|
||||
pub fn get_embedded_thumbnail(image: &ImageData) -> Option<Vec<u8>> {
|
||||
let meta = Metadata::new_from_path(&image.path);
|
||||
match meta {
|
||||
Ok(meta) => {
|
||||
if let Some(previews) = meta.get_preview_images() {
|
||||
for preview in previews {
|
||||
return Some(preview.get_data().unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
let meta = Metadata::new_from_path(&image.path).ok()?;
|
||||
|
||||
let width = meta.get_pixel_width();
|
||||
let height = meta.get_pixel_height();
|
||||
println!("image: {}", width as f32 / height as f32);
|
||||
|
||||
meta.get_preview_images()?.first().and_then(|preview| {
|
||||
let width = preview.get_width();
|
||||
let height = preview.get_height();
|
||||
println!("thumbnail: {}", width as f32 / height as f32);
|
||||
preview.get_data().ok()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
|
||||
if path.format == ImageFormat::Heif {
|
||||
return load_heif(path, true);
|
||||
let cache_path = path.get_cache_path();
|
||||
let mut buffer: Option<Vec<u8>> = None;
|
||||
if cache_path.exists() {
|
||||
let read_bytes = fs::read(&cache_path).unwrap();
|
||||
if read_bytes.len() != 0 {
|
||||
buffer = Some(read_bytes);
|
||||
}
|
||||
}
|
||||
match load_thumbnail_exif(path) {
|
||||
Some(thumbnail) => return thumbnail,
|
||||
None => load_thumbnail_full(path),
|
||||
if let Some(bytes) = buffer {
|
||||
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 orientation =
|
||||
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 {
|
||||
Vec::from_raw_parts(
|
||||
(ptr as usize + 12) as *mut u32,
|
||||
(len - 12) / 4,
|
||||
(cap - 12) / 4,
|
||||
)
|
||||
};
|
||||
|
||||
assert_eq!(width * height, buffer_u32.len());
|
||||
|
||||
return ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer: buffer_u32,
|
||||
rating: 0,
|
||||
orientation,
|
||||
};
|
||||
}
|
||||
let thumbnail = if path.format == ImageFormat::Heif {
|
||||
load_heif(path, true)
|
||||
} else {
|
||||
load_thumbnail_exif(path).unwrap_or_else(|| load_thumbnail_full(path))
|
||||
};
|
||||
|
||||
save_thumbnail(&cache_path, thumbnail.clone());
|
||||
thumbnail
|
||||
}
|
||||
|
||||
pub fn load_thumbnail_exif(path: &ImageData) -> Option<ImflowImageBuffer> {
|
||||
match get_embedded_thumbnail(path) {
|
||||
Some(thumbnail) => {
|
||||
let decoder = image::ImageReader::new(Cursor::new(thumbnail))
|
||||
.with_guessed_format()
|
||||
.unwrap();
|
||||
let image = decoder.decode().unwrap();
|
||||
if let Some(thumbnail) = get_embedded_thumbnail(path) {
|
||||
let decoder = image::ImageReader::new(Cursor::new(thumbnail))
|
||||
.with_guessed_format()
|
||||
.unwrap();
|
||||
let image = decoder.decode().unwrap();
|
||||
let orientation = path.orientation;
|
||||
|
||||
let width: usize = image.width() as usize;
|
||||
let height: usize = image.height() as usize;
|
||||
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 width = image.width();
|
||||
let height = image.height();
|
||||
// TODO: extract from image
|
||||
let ratio_image = 1.5;
|
||||
let ratio_thumbnail = width as f32 / height as f32;
|
||||
let crop = ratio_thumbnail / ratio_image;
|
||||
let start = ((0.5 - (crop / 2.0)) * height as f32).round();
|
||||
let cropped_height = (height as f32 * crop) as u32;
|
||||
let mut image = image.crop_imm(0, start as u32, width, cropped_height);
|
||||
|
||||
let rating = get_rating(path.into());
|
||||
image.apply_orientation(orientation);
|
||||
let width: usize = image.width() as usize;
|
||||
let height: usize = image.height() as usize;
|
||||
let rgba_buffer = image_to_rgba_buffer(image);
|
||||
let rating = get_rating(path.into());
|
||||
|
||||
Some(ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer: buffer_u32,
|
||||
rating,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
Some(ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer,
|
||||
rating,
|
||||
orientation,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,51 +397,68 @@ pub fn load_thumbnail_full(path: &ImageData) -> ImflowImageBuffer {
|
||||
.unwrap()
|
||||
.decode()
|
||||
.unwrap()
|
||||
.resize(640, 480, FilterType::Nearest);
|
||||
.resize_to_fill(720, 720, FilterType::Nearest);
|
||||
let width = image.width() as usize;
|
||||
let height = image.height() as usize;
|
||||
let start = std::time::Instant::now();
|
||||
let buffer = image_to_rgba_buffer(image);
|
||||
println!("Elapsed: {:?}", start.elapsed());
|
||||
let rating = get_rating(path.into());
|
||||
let orientation = path.orientation;
|
||||
|
||||
ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer: buffer,
|
||||
rating,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
||||
let lib_heif = LibHeif::new();
|
||||
let ctx = HeifContext::read_from_file(path.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();
|
||||
let image = if resize {
|
||||
let binding = ctx.top_level_image_handles();
|
||||
let handle = binding.get(0).unwrap();
|
||||
let thumbnail_count = handle.number_of_thumbnails() as u32;
|
||||
let mut thumbnail_ids = vec![0u32, thumbnail_count];
|
||||
handle.thumbnail_ids(&mut thumbnail_ids);
|
||||
let handle = &handle.thumbnail(thumbnail_ids[0]).unwrap();
|
||||
|
||||
lib_heif
|
||||
.decode(handle, libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba), None)
|
||||
.unwrap()
|
||||
} else {
|
||||
let binding = ctx.top_level_image_handles();
|
||||
let handle = binding.get(0).unwrap();
|
||||
|
||||
lib_heif
|
||||
.decode(handle, libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba), None)
|
||||
.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;
|
||||
|
||||
// Scale the image
|
||||
// if resize {
|
||||
// const MAX: usize = 3000;
|
||||
// let scale = max(width, height) as f32 / MAX as f32;
|
||||
// width = (width as f32 / scale) as usize;
|
||||
// height = (height as f32 / scale) as usize;
|
||||
// // image = image.scale(width as u32, height as u32, None).unwrap();
|
||||
// image = image.scale(599, 300, None).unwrap();
|
||||
// width = image.width() as usize;
|
||||
// height = image.height() as usize;
|
||||
// }
|
||||
|
||||
let rating = get_rating(path);
|
||||
|
||||
// Get "pixels"
|
||||
@ -335,17 +466,49 @@ pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
||||
let interleaved_plane = planes.interleaved.unwrap();
|
||||
assert!(!interleaved_plane.data.is_empty());
|
||||
assert!(interleaved_plane.stride > 0);
|
||||
assert_eq!(interleaved_plane.storage_bits_per_pixel, 32);
|
||||
|
||||
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)
|
||||
};
|
||||
let u32_slice = slice_u8_to_u32(rgba_buffer);
|
||||
|
||||
ImflowImageBuffer {
|
||||
width,
|
||||
height,
|
||||
rgba_buffer: u32_slice.to_vec(),
|
||||
rating,
|
||||
// TODO: verify
|
||||
orientation: Orientation::NoTransforms,
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut file = File::open(path).unwrap();
|
||||
let mut buf = [0u8; 16 * 1024];
|
||||
file.read(&mut buf).unwrap();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&buf);
|
||||
hasher.update(file.metadata().unwrap().len().to_le_bytes());
|
||||
hasher.finalize()
|
||||
}
|
||||
|
||||
// TODO: optimize
|
||||
pub fn save_thumbnail(path: &PathBuf, image: ImflowImageBuffer) {
|
||||
let cache_dir = path.parent().unwrap();
|
||||
if !cache_dir.exists() {
|
||||
fs::create_dir(cache_dir).unwrap();
|
||||
}
|
||||
let mut file = File::create(path).unwrap();
|
||||
let u8_buffer = vec_u32_to_u8(image.rgba_buffer);
|
||||
file.write(&(image.width 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();
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
#![feature(vec_into_raw_parts)]
|
||||
pub mod image;
|
||||
pub mod store;
|
||||
|
@ -1,7 +1,8 @@
|
||||
struct Transforms {
|
||||
transform: mat4x4<f32>,
|
||||
width: u32,
|
||||
height: u32
|
||||
height: u32,
|
||||
orientation: u32
|
||||
};
|
||||
@group(0) @binding(2) var<uniform> transforms: Transforms;
|
||||
|
||||
@ -26,11 +27,31 @@ fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
@group(0) @binding(0) var texture: texture_2d<f32>;
|
||||
@group(0) @binding(1) var texture_sampler: sampler;
|
||||
|
||||
fn reverse(in: vec2<f32>) -> vec2<f32> {
|
||||
return vec2<f32>(in.y, in.x);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let texture_size = vec2<f32>(f32(transforms.width), f32(transforms.height));
|
||||
fn fs_main(@location(0) in: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
var texture_size = vec2<f32>(f32(transforms.width), f32(transforms.height));
|
||||
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 pixel = uv * scale;
|
||||
var pixel = uv * scale;
|
||||
|
||||
// add offset to remove bleed from uncleared buffer
|
||||
let half_texel = vec2<f32>(0.5) / out_dim;
|
||||
let min_uv = half_texel;
|
||||
let max_uv = scale - half_texel;
|
||||
pixel = clamp(pixel, min_uv, max_uv);
|
||||
|
||||
if transforms.orientation == 3 {
|
||||
pixel = reverse(pixel);
|
||||
}
|
||||
return textureSample(texture, texture_sampler, pixel);
|
||||
}
|
||||
|
125
src/store.rs
125
src/store.rs
@ -1,24 +1,45 @@
|
||||
use crate::image::{ImageData, load_thumbnail};
|
||||
use crate::image::{ImageData, ImageFormat, load_thumbnail};
|
||||
use crate::image::{ImflowImageBuffer, load_available_images, load_image};
|
||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||
use rayon::prelude::*;
|
||||
use rexiv2::Metadata;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Instant;
|
||||
use threadpool::ThreadPool;
|
||||
|
||||
const PRELOAD_NEXT_IMAGE_N: usize = 16;
|
||||
const PRELOAD_NEXT_IMAGE_N: usize = 0;
|
||||
|
||||
pub struct FileFilters {
|
||||
pub rating: [bool; 6],
|
||||
pub name: String,
|
||||
pub file_format: HashMap<ImageFormat, bool>,
|
||||
}
|
||||
|
||||
impl Default for FileFilters {
|
||||
fn default() -> Self {
|
||||
let mut formats = HashMap::new();
|
||||
formats.insert(ImageFormat::Jpg, true);
|
||||
formats.insert(ImageFormat::Jxl, true);
|
||||
formats.insert(ImageFormat::Heif, true);
|
||||
FileFilters {
|
||||
rating: [true; 6],
|
||||
name: "".to_string(),
|
||||
file_format: formats,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_thumbnails: HashMap<ImageData, ImflowImageBuffer>,
|
||||
pub(crate) available_images: Vec<ImageData>,
|
||||
pub available_images: Vec<ImageData>,
|
||||
pub current_image_path: ImageData,
|
||||
pub(crate) pool: ThreadPool,
|
||||
pub(crate) loader_rx: mpsc::Receiver<(ImageData, ImflowImageBuffer)>,
|
||||
pub(crate) loader_tx: mpsc::Sender<(ImageData, ImflowImageBuffer)>,
|
||||
pub(crate) loader_rx: Receiver<(ImageData, ImflowImageBuffer)>,
|
||||
pub(crate) loader_tx: Sender<(ImageData, ImflowImageBuffer)>,
|
||||
pub(crate) currently_loading: HashSet<ImageData>,
|
||||
}
|
||||
|
||||
@ -26,25 +47,32 @@ impl ImageStore {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
let current_image_id: usize = 0;
|
||||
let mut loaded_images: HashMap<ImageData, ImflowImageBuffer> = HashMap::new();
|
||||
let mut loaded_thumbnails: HashMap<ImageData, ImflowImageBuffer> = HashMap::new();
|
||||
let available_images = load_available_images(path);
|
||||
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 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 mut loaded = 0;
|
||||
let to_load = available_images.len();
|
||||
for path in &available_images {
|
||||
let buf = load_thumbnail(path);
|
||||
loaded_thumbnails.insert(path.clone(), buf);
|
||||
loaded += 1;
|
||||
println!("{}/{}", loaded, to_load);
|
||||
}
|
||||
let (sender, receiver) = unbounded();
|
||||
available_images
|
||||
.par_iter()
|
||||
.for_each_with(sender, |s, path| {
|
||||
// if path.embedded_thumbnail {
|
||||
let buf = load_thumbnail(path);
|
||||
s.send((path.clone(), buf)).unwrap();
|
||||
// }
|
||||
});
|
||||
let loaded_thumbnails: HashMap<_, _> = receiver.iter().collect();
|
||||
let total_time = total_start.elapsed();
|
||||
println!(
|
||||
"all thumbnails load time: {:?} for {}",
|
||||
@ -52,9 +80,9 @@ impl ImageStore {
|
||||
loaded_thumbnails.len()
|
||||
);
|
||||
|
||||
let path = available_images[0].clone();
|
||||
let image = load_image(&path.clone());
|
||||
loaded_images.insert(path, image);
|
||||
let image = load_image(&new_path.clone());
|
||||
// let (path, image) = first_image_thread.join().unwrap();
|
||||
loaded_images.insert(new_path.clone(), image);
|
||||
let mut state = Self {
|
||||
current_image_id,
|
||||
loaded_images,
|
||||
@ -94,16 +122,15 @@ impl ImageStore {
|
||||
}
|
||||
|
||||
pub fn get_current_rating(&self) -> i32 {
|
||||
let imbuf = if let Some(full) = self.get_current_image() {
|
||||
// println!("full");
|
||||
full
|
||||
} else {
|
||||
// TODO: this assumes loaded thumbnail
|
||||
self.loaded_images_thumbnails
|
||||
.get(&self.current_image_path)
|
||||
.unwrap()
|
||||
};
|
||||
imbuf.rating
|
||||
self.current_image_path.rating
|
||||
// let imbuf = if let Some(full) = self.get_current_image() {
|
||||
// // println!("full");
|
||||
// full
|
||||
// } else {
|
||||
// // TODO: this assumes loaded thumbnail
|
||||
|
||||
// };
|
||||
// imbuf.rating
|
||||
}
|
||||
|
||||
pub fn preload_next_images(&mut self, n: usize) {
|
||||
@ -151,6 +178,14 @@ impl ImageStore {
|
||||
self.preload_next_images(PRELOAD_NEXT_IMAGE_N);
|
||||
}
|
||||
|
||||
pub fn select_image(&mut self, selected_image: ImageData) {
|
||||
if !self.loaded_images.contains_key(&selected_image) {
|
||||
self.request_load(selected_image.clone());
|
||||
}
|
||||
self.current_image_path = selected_image;
|
||||
self.preload_next_images(PRELOAD_NEXT_IMAGE_N);
|
||||
}
|
||||
|
||||
pub fn get_current_image(&self) -> Option<&ImflowImageBuffer> {
|
||||
self.loaded_images.get(&self.current_image_path)
|
||||
}
|
||||
@ -159,6 +194,19 @@ impl ImageStore {
|
||||
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_hash(&self, hash: String) -> &ImflowImageBuffer {
|
||||
self.loaded_images_thumbnails
|
||||
.iter()
|
||||
.find(|f| f.0.get_hash_str() == hash)
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer {
|
||||
if self
|
||||
.loaded_images_thumbnails
|
||||
@ -178,4 +226,21 @@ impl ImageStore {
|
||||
.get(&self.current_image_path)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn get_filtered_images(&self, filter: &FileFilters) -> Vec<ImageData> {
|
||||
self.available_images
|
||||
.iter()
|
||||
.filter(|f| filter.rating[f.rating.clamp(0, 5) as usize])
|
||||
.filter(|f| filter.file_format[&f.format])
|
||||
.filter(|f| {
|
||||
f.path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains(&filter.name)
|
||||
})
|
||||
.map(|f| f.clone())
|
||||
.collect::<Vec<ImageData>>()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user