Compare commits

...

7 Commits
main ... dev

Author SHA1 Message Date
Dawid Pietrykowski
2d73b89df0 Load fixes 2025-04-13 17:22:53 +02:00
Dawid Pietrykowski
ec879c31b1 Remove thumbnail letterboxing, fix scroll 2025-04-13 16:06:29 +02:00
Dawid Pietrykowski
b00bbbadc0 Added filters, scroll thumbnails 2025-04-12 23:19:02 +02:00
Dawid Pietrykowski
b41554c608 Fix bleed, fix thumbnail orientation 2025-04-11 00:33:09 +02:00
Dawid Pietrykowski
207878928f Optimizations, thumbnail strip, store RwLock 2025-04-10 01:07:50 +02:00
Dawid Pietrykowski
da998ccbf1 Add thumbnail cache 2025-04-08 23:54:13 +02:00
Dawid Pietrykowski
2c5cd88eb4 Expand metadata 2025-04-08 00:35:17 +02:00
9 changed files with 837 additions and 275 deletions

78
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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!()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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