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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
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]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -581,6 +590,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@ -626,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"
|
||||||
@ -657,6 +684,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
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]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -750,6 +787,16 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1021,6 +1068,16 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629"
|
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]]
|
[[package]]
|
||||||
name = "gethostname"
|
name = "gethostname"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -1385,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",
|
||||||
@ -1393,9 +1451,12 @@ dependencies = [
|
|||||||
"jpegxl-rs",
|
"jpegxl-rs",
|
||||||
"libheif-rs",
|
"libheif-rs",
|
||||||
"pollster",
|
"pollster",
|
||||||
|
"rayon",
|
||||||
"rexiv2",
|
"rexiv2",
|
||||||
|
"sha2",
|
||||||
"threadpool",
|
"threadpool",
|
||||||
"winit",
|
"winit",
|
||||||
|
"zerocopy 0.8.24",
|
||||||
"zune-image",
|
"zune-image",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2804,6 +2865,17 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -3181,6 +3253,12 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
@ -21,6 +21,10 @@ itertools = "0.12"
|
|||||||
rexiv2 = "0.10.0"
|
rexiv2 = "0.10.0"
|
||||||
threadpool = "1.8.1"
|
threadpool = "1.8.1"
|
||||||
bytemuck = "1.22.0"
|
bytemuck = "1.22.0"
|
||||||
|
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
|
||||||
|
@ -14,8 +14,8 @@ use image::codecs::jpeg::JpegDecoder;
|
|||||||
use image::metadata::Orientation;
|
use image::metadata::Orientation;
|
||||||
use image::{DynamicImage, ImageResult, RgbaImage};
|
use image::{DynamicImage, ImageResult, RgbaImage};
|
||||||
use imflow::image::{
|
use imflow::image::{
|
||||||
ImflowImageBuffer, get_orientation, get_rating, image_to_rgba_buffer, load_available_images,
|
ImflowImageBuffer, check_embedded_thumbnail, get_orientation, get_rating, image_to_rgba_buffer,
|
||||||
load_image, load_thumbnail_exif, load_thumbnail_full,
|
load_available_images, load_image, load_thumbnail_exif, load_thumbnail_full,
|
||||||
};
|
};
|
||||||
use jpegxl_rs::Endianness;
|
use jpegxl_rs::Endianness;
|
||||||
use jpegxl_rs::decode::{Data, PixelFormat, Pixels};
|
use jpegxl_rs::decode::{Data, PixelFormat, Pixels};
|
||||||
@ -105,7 +105,7 @@ fn load_a(path: &PathBuf) -> ImflowImageBuffer {
|
|||||||
// let total_time = total_start.elapsed();
|
// let total_time = total_start.elapsed();
|
||||||
// println!("Total loading time: {:?}", total_time);
|
// println!("Total loading time: {:?}", total_time);
|
||||||
|
|
||||||
let rating = get_rating(path);
|
let rating = 0;
|
||||||
|
|
||||||
ImflowImageBuffer {
|
ImflowImageBuffer {
|
||||||
width,
|
width,
|
||||||
@ -130,11 +130,11 @@ fn load_b(path: &PathBuf) -> ImflowImageBuffer {
|
|||||||
decoder.decode_into(buffer.as_mut_slice()).unwrap();
|
decoder.decode_into(buffer.as_mut_slice()).unwrap();
|
||||||
|
|
||||||
let image = RgbaImage::from_raw(width as u32, height as u32, buffer).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);
|
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 mut buffer = dynamic_image.to_rgba8();
|
||||||
let buffer_u32 = unsafe {
|
let buffer_u32 = unsafe {
|
||||||
@ -254,12 +254,12 @@ pub fn file_load_benchmark(c: &mut Criterion) {
|
|||||||
let images = load_available_images(PATH.into());
|
let images = load_available_images(PATH.into());
|
||||||
group.bench_function("zune_jpeg", |b| {
|
group.bench_function("zune_jpeg", |b| {
|
||||||
for image in images.iter().take(10) {
|
for image in images.iter().take(10) {
|
||||||
b.iter(|| load_a(image));
|
b.iter(|| load_a(&image.path));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
group.bench_function("image_rs", |b| {
|
group.bench_function("image_rs", |b| {
|
||||||
for image in images.iter().take(10) {
|
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());
|
let images = load_available_images("./test_images/jxl".into());
|
||||||
group.bench_function("single", |b| {
|
group.bench_function("single", |b| {
|
||||||
for image in images.iter().take(10) {
|
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| {
|
group.bench_function("multi", |b| {
|
||||||
for image in images.iter().take(10) {
|
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, thumbnail_load_benchmark);
|
||||||
// criterion_group!(benches, file_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);
|
criterion_main!(benches);
|
||||||
|
303
src/app.rs
303
src/app.rs
@ -1,11 +1,18 @@
|
|||||||
use crate::egui_tools::EguiRenderer;
|
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::wgpu::SurfaceError;
|
||||||
use egui_wgpu::{ScreenDescriptor, wgpu};
|
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::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 +22,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 +38,7 @@ pub(crate) struct TransformData {
|
|||||||
zoom: f32,
|
zoom: f32,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
orientation: Orientation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
@ -212,12 +219,15 @@ 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,
|
||||||
pub transform_buffer: wgpu::Buffer,
|
pub transform_buffer: wgpu::Buffer,
|
||||||
pub transform_data: TransformData,
|
pub transform_data: TransformData,
|
||||||
|
pub filters: FileFilters,
|
||||||
|
pub selected_image: ImageData,
|
||||||
|
pub loaded_thumbnail: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@ -276,12 +286,19 @@ 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 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 scale_factor = 1.0;
|
||||||
|
|
||||||
let store = ImageStore::new(path);
|
|
||||||
|
|
||||||
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(), 8192, 8192);
|
setup_texture(&device, surface_config.clone(), 8192, 8192);
|
||||||
|
|
||||||
let transform_data = TransformData {
|
let transform_data = TransformData {
|
||||||
@ -290,6 +307,7 @@ impl AppState {
|
|||||||
zoom: 1.0,
|
zoom: 1.0,
|
||||||
width: 10000,
|
width: 10000,
|
||||||
height: 10000,
|
height: 10000,
|
||||||
|
orientation: Orientation::NoTransforms,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -305,6 +323,9 @@ impl AppState {
|
|||||||
render_pipeline,
|
render_pipeline,
|
||||||
transform_buffer,
|
transform_buffer,
|
||||||
transform_data,
|
transform_data,
|
||||||
|
filters: FileFilters::default(),
|
||||||
|
selected_image,
|
||||||
|
loaded_thumbnail: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,6 +334,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,8 +383,8 @@ 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(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_resized(&mut self, width: u32, height: u32) {
|
fn handle_resized(&mut self, width: u32, height: u32) {
|
||||||
@ -369,14 +394,31 @@ impl App {
|
|||||||
self.pan_zoom(0.0, 0.0, 0.0);
|
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();
|
let state = self.state.as_mut().unwrap();
|
||||||
|
if !force
|
||||||
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 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
|
full
|
||||||
} else {
|
} else {
|
||||||
state.store.get_thumbnail()
|
state.loaded_thumbnail = true;
|
||||||
|
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 +431,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 +452,25 @@ impl App {
|
|||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
state.selected_image = store.current_image_path.clone();
|
||||||
|
}
|
||||||
|
|
||||||
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 =
|
// TODO: Remove obviously
|
||||||
(state.transform_data.width as f32) / (state.transform_data.height as f32);
|
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_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 +486,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 +656,29 @@ 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 mut rating_filter = [false; 6];
|
||||||
let path = state.store.current_image_path.clone();
|
|
||||||
let filename = path.path.file_name().unwrap();
|
// let mut file_filters;
|
||||||
let window = self.window.as_ref().unwrap();
|
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);
|
state.egui_renderer.begin_frame(window);
|
||||||
|
|
||||||
@ -617,7 +689,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(),
|
||||||
);
|
);
|
||||||
@ -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.egui_renderer.end_frame_and_draw(
|
||||||
&state.device,
|
&state.device,
|
||||||
&state.queue,
|
&state.queue,
|
||||||
@ -641,6 +758,8 @@ impl App {
|
|||||||
|
|
||||||
state.queue.submit(Some(encoder.finish()));
|
state.queue.submit(Some(encoder.finish()));
|
||||||
surface_texture.present();
|
surface_texture.present();
|
||||||
|
|
||||||
|
self.update_texture(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,14 +787,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 +813,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(false);
|
||||||
|
}
|
||||||
|
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 +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::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView};
|
||||||
use egui_wgpu::{Renderer, ScreenDescriptor, wgpu};
|
use egui_wgpu::{Renderer, ScreenDescriptor, wgpu};
|
||||||
use egui_winit::State;
|
use egui_winit::State;
|
||||||
@ -24,6 +24,15 @@ impl EguiRenderer {
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
) -> EguiRenderer {
|
) -> EguiRenderer {
|
||||||
let egui_context = Context::default();
|
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(
|
let egui_state = egui_winit::State::new(
|
||||||
egui_context,
|
egui_context,
|
||||||
|
393
src/image.rs
393
src/image.rs
@ -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;
|
||||||
@ -8,17 +9,29 @@ use jpegxl_rs::decode::PixelFormat;
|
|||||||
use jpegxl_rs::decoder_builder;
|
use jpegxl_rs::decoder_builder;
|
||||||
use libheif_rs::{HeifContext, LibHeif, RgbChroma};
|
use libheif_rs::{HeifContext, LibHeif, RgbChroma};
|
||||||
use rexiv2::Metadata;
|
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::jpeg::JpegDecoder;
|
||||||
use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
|
use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
|
||||||
use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
|
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;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::fs::read;
|
use std::fs::read;
|
||||||
|
use std::hash::Hash;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::mem;
|
use std::io::Read;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd)]
|
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd)]
|
||||||
@ -28,39 +41,81 @@ pub enum ImageFormat {
|
|||||||
Heif,
|
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 struct ImageData {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub format: ImageFormat,
|
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 struct ImflowImageBuffer {
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
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(image: &ImageData) -> u8 {
|
pub fn get_orientation(path: &PathBuf) -> Orientation {
|
||||||
let meta = Metadata::new_from_path(&image.path);
|
Metadata::new_from_path(path).map_or(Orientation::NoTransforms, |meta| {
|
||||||
match meta {
|
Orientation::from_exif(meta.get_orientation() as u8).unwrap()
|
||||||
Ok(meta) => meta.get_orientation() as u8,
|
})
|
||||||
Err(e) => panic!("{:?}", e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@ -92,6 +147,7 @@ fn get_format(path: &PathBuf) -> Option<ImageFormat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
||||||
|
// sleep(Duration::from_millis(500));
|
||||||
let total_start = Instant::now();
|
let total_start = Instant::now();
|
||||||
|
|
||||||
match image.format {
|
match image.format {
|
||||||
@ -120,15 +176,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());
|
||||||
|
|
||||||
@ -137,6 +189,7 @@ pub fn load_image(image: &ImageData) -> ImflowImageBuffer {
|
|||||||
height,
|
height,
|
||||||
rgba_buffer,
|
rgba_buffer,
|
||||||
rating,
|
rating,
|
||||||
|
orientation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImageFormat::Jpg => {
|
ImageFormat::Jpg => {
|
||||||
@ -155,18 +208,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();
|
let orientation = image.orientation;
|
||||||
// TODO: Optimize rotation
|
let rgba_buffer = vec_u8_to_u32(buffer);
|
||||||
let orientation =
|
println!("Total loading time: {:?}", total_start.elapsed());
|
||||||
Orientation::from_exif(get_orientation(image)).unwrap_or(Orientation::NoTransforms);
|
ImflowImageBuffer {
|
||||||
let image = RgbaImage::from_raw(width as u32, height as u32, buffer).unwrap();
|
width,
|
||||||
let mut dynamic_image = DynamicImage::from(image);
|
height,
|
||||||
dynamic_image.apply_orientation(orientation);
|
rgba_buffer,
|
||||||
let buffer = dynamic_image.as_rgba8().unwrap();
|
rating,
|
||||||
let (width, height) = swap_wh(width, height, orientation);
|
orientation,
|
||||||
let orientation_time = orientation_start.elapsed();
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
@ -174,32 +230,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> {
|
||||||
@ -209,7 +260,28 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
|||||||
.sorted()
|
.sorted()
|
||||||
.filter_map(|path| {
|
.filter_map(|path| {
|
||||||
if let Some(format) = get_format(&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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -217,61 +289,103 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
|
|||||||
.collect::<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>> {
|
pub fn get_embedded_thumbnail(image: &ImageData) -> Option<Vec<u8>> {
|
||||||
let meta = Metadata::new_from_path(&image.path);
|
let meta = Metadata::new_from_path(&image.path).ok()?;
|
||||||
match meta {
|
|
||||||
Ok(meta) => {
|
let width = meta.get_pixel_width();
|
||||||
if let Some(previews) = meta.get_preview_images() {
|
let height = meta.get_pixel_height();
|
||||||
for preview in previews {
|
println!("image: {}", width as f32 / height as f32);
|
||||||
return Some(preview.get_data().unwrap());
|
|
||||||
}
|
meta.get_preview_images()?.first().and_then(|preview| {
|
||||||
}
|
let width = preview.get_width();
|
||||||
None
|
let height = preview.get_height();
|
||||||
}
|
println!("thumbnail: {}", width as f32 / height as f32);
|
||||||
Err(_) => None,
|
preview.get_data().ok()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
|
pub fn load_thumbnail(path: &ImageData) -> ImflowImageBuffer {
|
||||||
if path.format == ImageFormat::Heif {
|
let cache_path = path.get_cache_path();
|
||||||
return load_heif(path, true);
|
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> {
|
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 image = decoder.decode().unwrap();
|
let image = decoder.decode().unwrap();
|
||||||
|
let orientation = path.orientation;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
image.apply_orientation(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,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,51 +397,68 @@ pub fn load_thumbnail_full(path: &ImageData) -> ImflowImageBuffer {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.decode()
|
.decode()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.resize(640, 480, FilterType::Nearest);
|
.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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
||||||
let lib_heif = LibHeif::new();
|
let lib_heif = LibHeif::new();
|
||||||
let ctx = HeifContext::read_from_file(path.path.to_str().unwrap()).unwrap();
|
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 image = if resize {
|
||||||
// let mut meta_ids: Vec<ItemId> = vec![0; 1];
|
let binding = ctx.top_level_image_handles();
|
||||||
// let count = handle.metadata_block_ids(&mut meta_ids, b"Exif");
|
let handle = binding.get(0).unwrap();
|
||||||
// assert_eq!(count, 1);
|
let thumbnail_count = handle.number_of_thumbnails() as u32;
|
||||||
// let exif: Vec<u8> = handle.metadata(meta_ids[0]).unwrap();
|
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!(
|
assert_eq!(
|
||||||
image.color_space(),
|
image.color_space(),
|
||||||
Some(libheif_rs::ColorSpace::Rgb(RgbChroma::Rgba)),
|
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 width = image.width() as usize;
|
||||||
let height = image.height() 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);
|
let rating = get_rating(path);
|
||||||
|
|
||||||
// Get "pixels"
|
// Get "pixels"
|
||||||
@ -335,17 +466,49 @@ pub fn load_heif(path: &ImageData, resize: bool) -> ImflowImageBuffer {
|
|||||||
let interleaved_plane = planes.interleaved.unwrap();
|
let interleaved_plane = planes.interleaved.unwrap();
|
||||||
assert!(!interleaved_plane.data.is_empty());
|
assert!(!interleaved_plane.data.is_empty());
|
||||||
assert!(interleaved_plane.stride > 0);
|
assert!(interleaved_plane.stride > 0);
|
||||||
|
assert_eq!(interleaved_plane.storage_bits_per_pixel, 32);
|
||||||
|
|
||||||
let rgba_buffer = interleaved_plane.data;
|
let rgba_buffer = interleaved_plane.data;
|
||||||
// Create a slice of u32 from the u8 slice
|
let u32_slice = slice_u8_to_u32(rgba_buffer);
|
||||||
let u32_slice = unsafe {
|
|
||||||
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: 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 image;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
@ -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,31 @@ 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;
|
||||||
|
|
||||||
|
// 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);
|
return textureSample(texture, texture_sampler, pixel);
|
||||||
}
|
}
|
||||||
|
123
src/store.rs
123
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 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 = 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 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,25 +47,32 @@ 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 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 {
|
||||||
let buf = load_thumbnail(path);
|
let buf = load_thumbnail(path);
|
||||||
loaded_thumbnails.insert(path.clone(), buf);
|
s.send((path.clone(), buf)).unwrap();
|
||||||
loaded += 1;
|
// }
|
||||||
println!("{}/{}", loaded, to_load);
|
});
|
||||||
}
|
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 {}",
|
||||||
@ -52,9 +80,9 @@ impl ImageStore {
|
|||||||
loaded_thumbnails.len()
|
loaded_thumbnails.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let path = available_images[0].clone();
|
let image = load_image(&new_path.clone());
|
||||||
let image = load_image(&path.clone());
|
// let (path, image) = first_image_thread.join().unwrap();
|
||||||
loaded_images.insert(path, image);
|
loaded_images.insert(new_path.clone(), image);
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
current_image_id,
|
current_image_id,
|
||||||
loaded_images,
|
loaded_images,
|
||||||
@ -94,16 +122,15 @@ impl ImageStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_rating(&self) -> i32 {
|
pub fn get_current_rating(&self) -> i32 {
|
||||||
let imbuf = if let Some(full) = self.get_current_image() {
|
self.current_image_path.rating
|
||||||
// println!("full");
|
// let imbuf = if let Some(full) = self.get_current_image() {
|
||||||
full
|
// // println!("full");
|
||||||
} else {
|
// full
|
||||||
// TODO: this assumes loaded thumbnail
|
// } else {
|
||||||
self.loaded_images_thumbnails
|
// // TODO: this assumes loaded thumbnail
|
||||||
.get(&self.current_image_path)
|
|
||||||
.unwrap()
|
// };
|
||||||
};
|
// imbuf.rating
|
||||||
imbuf.rating
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preload_next_images(&mut self, n: usize) {
|
pub fn preload_next_images(&mut self, n: usize) {
|
||||||
@ -151,6 +178,14 @@ impl ImageStore {
|
|||||||
self.preload_next_images(PRELOAD_NEXT_IMAGE_N);
|
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> {
|
pub fn get_current_image(&self) -> Option<&ImflowImageBuffer> {
|
||||||
self.loaded_images.get(&self.current_image_path)
|
self.loaded_images.get(&self.current_image_path)
|
||||||
}
|
}
|
||||||
@ -159,6 +194,19 @@ 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_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 {
|
pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer {
|
||||||
if self
|
if self
|
||||||
.loaded_images_thumbnails
|
.loaded_images_thumbnails
|
||||||
@ -178,4 +226,21 @@ impl ImageStore {
|
|||||||
.get(&self.current_image_path)
|
.get(&self.current_image_path)
|
||||||
.unwrap();
|
.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