diff --git a/src/image/image.rs b/src/image/image.rs index 2ccfb54..661b11c 100644 --- a/src/image/image.rs +++ b/src/image/image.rs @@ -7,7 +7,7 @@ use svg_metadata::Metadata; use display_info::DisplayInfo; use image::{codecs::{gif::{GifDecoder, GifEncoder}, jpeg::{JpegDecoder, JpegEncoder}, png::{PngDecoder, PngEncoder}, webp::{WebPDecoder, WebPEncoder}}, DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageResult}; -use crate::{error::{Error, Result}, notifier::NotifierAPI}; +use crate::{error::{Error, Result}, image::optimization::OptimizationProcessingMeat, notifier::NotifierAPI}; use super::{backends::ImageProcessingBackend, image_formats::ImageFormat, optimization::{self, ImageOptimization}}; @@ -113,37 +113,36 @@ impl Image { ) } - pub fn reload_image( - &mut self, - optimizations_to_apply: &[ImageOptimization], - notifier: &mut NotifierAPI, - image_processing_backend: &ImageProcessingBackend - ) -> Result<()> { - if self.optimizations.is_empty() && optimizations_to_apply.is_empty() { - return Ok(()); - } + // pub fn reload_image( + // &mut self, + // optimizations_to_apply: &[ImageOptimization], + // notifier: &mut NotifierAPI, + // image_processing_backend: &ImageProcessingBackend + // ) -> Result<()> { + // if self.optimizations.is_empty() && optimizations_to_apply.is_empty() { + // return Ok(()); + // } - notifier.set_loading_and_log(Some("Gathering required optimizations...".into())); + // notifier.set_loading_and_log(Some("Gathering required optimizations...".into())); - // what optimizations actually require to be applied / aren't applied already. - let required_optimizations = self.required_optimizations(optimizations_to_apply); + // // what optimizations actually require to be applied / aren't applied already. + // let required_optimizations = self.required_optimizations(optimizations_to_apply); - // TODO: we need to somehow figure out if we need to - // read image bytes from the file again or not judging by the required optimizations. - // - // E.g. If we are upsampling we will need the complete set of images bytes hence a re-read. - // If we are downsampling we can reuse the images bytes currently loaded in memory. + // // TODO: we need to somehow figure out if we need to + // // read image bytes from the file again or not judging by the required optimizations. + // // + // // E.g. If we are upsampling we will need the complete set of images bytes hence a re-read. + // // If we are downsampling we can reuse the images bytes currently loaded in memory. - Ok(()) - } + // Ok(()) + // } pub fn load_image( &mut self, - optimizations: &[ImageOptimization], notifier: &mut NotifierAPI, image_processing_backend: &ImageProcessingBackend ) -> Result<()> { - if optimizations.is_empty() { + if self.optimizations.is_empty() { debug!("No optimizations were set so loading with fs::read instead..."); let mut image_bytes_lock = self.image_bytes.lock().unwrap(); @@ -177,7 +176,6 @@ impl Image { let image_result = self.optimize_and_decode_image_to_buffer( image_processing_backend, image_decoder, - optimizations, &mut optimized_image_buffer, notifier ); @@ -193,7 +191,8 @@ impl Image { .toast_and_log(error.into(), egui_notify::ToastLevel::Error); // load image without optimizations - let result = self.load_image(&[], notifier, image_processing_backend); + self.optimizations.clear(); + let result = self.load_image(notifier, image_processing_backend); match result { Ok(_) => return Ok(()), @@ -237,51 +236,51 @@ impl Image { } } - fn required_optimizations(&self, optimizations: &[ImageOptimization]) -> Vec { - let optimizations_wanted = optimizations.to_owned(); + // fn required_optimizations(&self, optimizations: &[ImageOptimization]) -> Vec { + // let optimizations_wanted = optimizations.to_owned(); - let mut optimizations_necessary: Vec = Vec::new(); + // let mut optimizations_necessary: Vec = Vec::new(); - for optimization in optimizations_wanted { - if let Some(old_optimization) = self.has_optimization(&optimization) { + // for optimization in optimizations_wanted { + // if let Some(old_optimization) = self.has_optimization(&optimization) { - // NOTE: ignore the warning. - // TODO: We might need to introduce "ImageOptimization::Upsample". - if let ( - ImageOptimization::Downsample(width, height), - ImageOptimization::Downsample(old_width, old_height) - ) = (&optimization, old_optimization) { - // We don't want to apply a downsample optimization if the change - // in resolution isn't that big but we do want to upsample no matter what. + // // NOTE: ignore the warning. + // // TODO: We might need to introduce "ImageOptimization::Upsample". + // if let ( + // ImageOptimization::Downsample(width, height), + // ImageOptimization::Downsample(old_width, old_height) + // ) = (&optimization, old_optimization) { + // // We don't want to apply a downsample optimization if the change + // // in resolution isn't that big but we do want to upsample no matter what. - // the scale difference between the old downsample and new. - let scale_width = *width as f32 / *old_width as f32; - let scale_height = *height as f32 / *old_height as f32; + // // the scale difference between the old downsample and new. + // let scale_width = *width as f32 / *old_width as f32; + // let scale_height = *height as f32 / *old_height as f32; - let is_upsample = scale_width > 1.0 || scale_height > 1.0; + // let is_upsample = scale_width > 1.0 || scale_height > 1.0; - match is_upsample { - false => { - // downsample difference must be - // greater than this to allow the optimization. - let allowed_downsample_diff: f32 = 1.2; + // match is_upsample { + // false => { + // // downsample difference must be + // // greater than this to allow the optimization. + // let allowed_downsample_diff: f32 = 1.2; - if scale_width > allowed_downsample_diff && scale_height > allowed_downsample_diff { - optimizations_necessary.push(optimization); - } - }, - true => { - optimizations_necessary.push(optimization); - continue; - }, - } - } + // if scale_width > allowed_downsample_diff && scale_height > allowed_downsample_diff { + // optimizations_necessary.push(optimization); + // } + // }, + // true => { + // optimizations_necessary.push(optimization); + // continue; + // }, + // } + // } - } - } + // } + // } - optimizations_necessary - } + // optimizations_necessary + // } /// Checks if the image has this TYPE of optimization applied, not the exact /// optimization itself. Then it returns a reference to the exact optimization found. @@ -299,13 +298,13 @@ impl Image { &self, image_processing_backend: &ImageProcessingBackend, image_decoder: Box, - optimizations: &[ImageOptimization], optimized_image_buffer: &mut Vec, notifier: &mut NotifierAPI, ) -> ImageResult<()> { let image_colour_type = image_decoder.color_type(); - // mutable width and height + // mutable width and height because some optimizations + // modify the image size hence we need to keep track of that. let (mut actual_width, mut actual_height) = ( self.image_size.width as u32, self.image_size.height as u32 ); @@ -319,16 +318,15 @@ impl Image { let has_alpha = image_colour_type.has_alpha(); - for optimization in optimizations { - notifier.set_loading( - Some(format!("Applying {:#} optimization...", optimization)) - ); - debug!("Applying '{:?}' optimization to image...", optimization); - - (pixels, (actual_width, actual_height)) = optimization.apply_roseate( - pixels, &(self.image_size.width as u32, self.image_size.height as u32), has_alpha - ); - } + // TODO: handle result and errors + self.apply_optimizations( + notifier, + OptimizationProcessingMeat::Roseate( + &mut pixels, + &mut (actual_width, actual_height), + has_alpha + ) + ); notifier.set_loading( Some("Encoding optimized image...".into()) @@ -385,14 +383,11 @@ impl Image { match result { Ok(mut dynamic_image) => { - for optimization in optimizations { - notifier.set_loading( - Some(format!("Applying {:#} optimization...", optimization)) - ); - debug!("Applying '{:?}' optimization to image...", optimization); - - dynamic_image = optimization.apply_dynamic_image(dynamic_image); - } + // TODO: handle result and errors + self.apply_optimizations( + notifier, + OptimizationProcessingMeat::ImageRS(&mut dynamic_image) + ); notifier.set_loading( Some("Encoding optimized image...".into()) diff --git a/src/image/optimization.rs b/src/image/optimization.rs index 3da2b87..b5531c4 100644 --- a/src/image/optimization.rs +++ b/src/image/optimization.rs @@ -5,16 +5,23 @@ use log::debug; use imagesize::ImageSize; use display_info::DisplayInfo; -use super::{fast_downsample::fast_downsample, image::ImageSizeT}; +use crate::{error::Result, notifier::NotifierAPI}; -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +use super::{fast_downsample::fast_downsample, image::{Image, ImageSizeT}}; + +pub enum OptimizationProcessingMeat<'a> { + ImageRS(&'a mut DynamicImage), + Roseate(&'a mut Vec, &'a mut ImageSizeT, bool) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ImageOptimization { - /// Downsamples the image to this width and height. + /// Downsamples the image roughly to the resolution of your monitor. /// - /// Images don't always have to be displayed at it's native resolution, - /// especially when the image is significantly bigger than our monitor - /// can even display so to save memory we downsample the image. Downsampling - /// decreases the memory usage of the image at the cost of time wasted actually + /// Images don't always have to be displayed at their native resolution, + /// especially when the image is significantly bigger than your monitor + /// can even display, so to save memory we downsample the image. Downsampling + /// decreases the amount of memory used by rose of the image at the cost of time wasted actually /// resizing the image. The bigger the image the more time it will take to downsample /// but we think memory savings are more valuable. You can enable or disable downsampling /// in the config if you do not wish for such memory savings. Setting the overall optimization @@ -22,37 +29,24 @@ pub enum ImageOptimization { /// /// NOTE: "The image's aspect ratio is preserved. The image is scaled to the maximum /// possible size that fits within the bounds specified by the width and height." ~ Image Crate - Downsample(u32, u32), - Upsample(u32, u32) + MonitorDownsampling(u32), + /// Basically `MonitorDownsampling` but the image is + /// dynamically downsampled when full detail is no longer required + /// (for example, when the user zooms back out from a zoom that triggered `DynamicUpsampling`). + DynamicDownsampling, + /// `DynamicDownsampling` but the other way round. The image is upsampled relative to the + /// amount you zoomed into the image (zoom factor) all the way up to the image's native resolution. + /// + /// The point of dynamic upsampling is to give back the detail that was lost from initially downsampling the image. + DynamicUpsampling, } impl ImageOptimization { - pub fn apply_dynamic_image(&self, image: DynamicImage) -> DynamicImage { - match self { - ImageOptimization::Downsample(width, height) => { - image.resize( - *width, - *height, - image::imageops::FilterType::Lanczos3 - ) - }, - _ => image - } - } - - pub fn apply_roseate(&self, pixels: Vec, image_size: &ImageSizeT, has_alpha: bool) -> (Vec, ImageSizeT) { - match self { - ImageOptimization::Downsample(width, height) => { - fast_downsample(pixels, image_size, (*width, *height), has_alpha) - }, - _ => (pixels, *image_size) - } - } - pub fn id(&self) -> &str { match self { - ImageOptimization::Downsample(_, _) => "downsample", - ImageOptimization::Upsample(_, _) => "upsample", + ImageOptimization::MonitorDownsampling(_) => "monitor-downsampling", + ImageOptimization::DynamicDownsampling => "dynamic-downsampling", + ImageOptimization::DynamicUpsampling => "dynamic-upsampling", } } } @@ -60,51 +54,133 @@ impl ImageOptimization { impl Display for ImageOptimization { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ImageOptimization::Downsample(_, _) => write!(f, "Downsample"), - ImageOptimization::Upsample(_, _) => write!(f, "Upsample"), + ImageOptimization::MonitorDownsampling(_) => write!(f, "Monitor Downsampling"), + ImageOptimization::DynamicDownsampling => write!(f, "Dynamic Downsampling"), + ImageOptimization::DynamicUpsampling => write!(f, "Dynamic Upsampling"), } } } -pub fn apply_image_optimizations(mut optimizations: Vec, image_size: &ImageSize) -> Vec { - let all_display_infos = DisplayInfo::all().expect( - "Failed to get information about your display monitor!" - ); +impl Image { + // TODO: Return actual error instead of "()". + pub fn apply_optimizations(&self, notifier: &mut NotifierAPI, meat: OptimizationProcessingMeat) -> Result<(), ()> { + match meat { + OptimizationProcessingMeat::ImageRS(dynamic_image) => { - // NOTE: I don't think the first monitor is always the primary and - // if that is the case then we're gonna have a problem. (i.e images overly downsampled or not at all) - let primary_display_maybe = all_display_infos.first().expect( - "Uhhhhh, you don't have a monitor. WHAT!" - ); + for optimization in self.optimizations.clone() { + notifier.set_loading( + Some(format!("Applying {:#} optimization...", optimization)) + ); + debug!("Applying '{:?}' optimization to image...", optimization); - let marginal_allowance: f32 = 1.3; - // TODO: Make this adjustable in the config too as down sample strength. - // I'm still thinking about this so leave it out for now. ~ Goldy + *dynamic_image = match optimization { + ImageOptimization::MonitorDownsampling(marginal_allowance) => { + let (monitor_width, monitor_height) = get_monitor_size_before_egui_window()?; - let (width, height) = ( - primary_display_maybe.width as f32 * marginal_allowance, - primary_display_maybe.height as f32 * marginal_allowance - ); + let marginal_allowance_scale = marginal_allowance as f32 / 100.0; - debug!( - "Display Size: {} x {}", - primary_display_maybe.width, - primary_display_maybe.height - ); - debug!( - "Display Size + Downsample Marginal Allowance: {} x {}", width, height - ); - debug!( - "Image Size: {} x {}", - image_size.width, - image_size.height - ); + let (width, height) = ( + monitor_width as f32 * marginal_allowance_scale, monitor_height as f32 * marginal_allowance_scale + ); + + dynamic_image.resize( + width as u32, + height as u32, + image::imageops::FilterType::Lanczos3 + ) + }, + _ => todo!() + }; + } - // If the image is a lot bigger than the user's monitor - // then apply the downsample optimization for this image. - if image_size.width > width as usize && image_size.height > height as usize { - optimizations.push(ImageOptimization::Downsample(width as u32, height as u32)); + }, + OptimizationProcessingMeat::Roseate(pixels, image_size, has_alpha) => { + + for optimization in self.optimizations.clone() { + (*pixels, *image_size) = match optimization { + ImageOptimization::MonitorDownsampling(marginal_allowance) => { + let (monitor_width, monitor_height) = get_monitor_size_before_egui_window()?; + + let marginal_allowance_scale = marginal_allowance as f32 / 100.0; + + let (width, height) = ( + monitor_width as f32 * marginal_allowance_scale, monitor_height as f32 * marginal_allowance_scale + ); + + fast_downsample( + pixels.to_vec(), // this is pretty bad, .to_vec() will clone all these pixels. + // We need to find a way to avoid this for memory spiking and performance sake. + &image_size, + (width as u32, height as u32), + has_alpha + ) + }, + _ => todo!() + } + } + + }, + } + + Ok(()) } +} + +// pub fn apply_image_optimizations(mut optimizations: Vec, image_size: &ImageSize) -> Vec { +// let all_display_infos = DisplayInfo::all().expect( +// "Failed to get information about your display monitor!" +// ); + +// // NOTE: I don't think the first monitor is always the primary and +// // if that is the case then we're gonna have a problem. (i.e images overly downsampled or not at all) +// let primary_display_maybe = all_display_infos.first().expect( +// "Uhhhhh, you don't have a monitor. WHAT!" +// ); - optimizations +// let marginal_allowance: f32 = 1.3; +// // TODO: Make this adjustable in the config too as down sample strength. +// // I'm still thinking about this so leave it out for now. ~ Goldy + +// let (width, height) = ( +// primary_display_maybe.width as f32 * marginal_allowance, +// primary_display_maybe.height as f32 * marginal_allowance +// ); + +// debug!( +// "Display Size: {} x {}", +// primary_display_maybe.width, +// primary_display_maybe.height +// ); +// debug!( +// "Display Size + Downsample Marginal Allowance: {} x {}", width, height +// ); +// debug!( +// "Image Size: {} x {}", +// image_size.width, +// image_size.height +// ); + +// // If the image is a lot bigger than the user's monitor +// // then apply the downsample optimization for this image. +// if image_size.width > width as usize && image_size.height > height as usize { +// optimizations.push(ImageOptimization::Downsample(width as u32, height as u32)); +// } + +// optimizations +// } + +// TODO: Return actual error instead of "()". +fn get_monitor_size_before_egui_window() -> Result<(u32, u32), ()> { + let all_display_infos = DisplayInfo::all().expect( + "Failed to get information about your display monitor!" + ); + + // NOTE: I don't think the first monitor is always the primary and + // if that is the case then we're gonna have a problem. (i.e images overly downsampled or not at all) + match all_display_infos.first() { + Some(primary_monitor_maybe) => { + Ok((primary_monitor_maybe.width, primary_monitor_maybe.height)) + }, + None => Err(()), + } } \ No newline at end of file diff --git a/src/image_loader.rs b/src/image_loader.rs index 987f41b..67e00ea 100644 --- a/src/image_loader.rs +++ b/src/image_loader.rs @@ -2,7 +2,7 @@ use std::{sync::{Arc, Mutex}, thread, time::{Duration, Instant}}; use log::{debug, info, warn}; -use crate::{image::{backends::ImageProcessingBackend, image::Image, optimization::apply_image_optimizations}, notifier::NotifierAPI}; +use crate::{image::{backends::ImageProcessingBackend, image::Image, optimization::{self, ImageOptimization}}, notifier::NotifierAPI}; /// Struct that handles all the image loading logic in a thread safe /// manner to allow features such as background image loading / lazy loading. @@ -48,12 +48,13 @@ impl ImageLoader { ); let mut image = image.clone(); - let mut optimizations = Vec::new(); notifier.set_loading( Some("Applying image optimizations...".into()) ); - optimizations = apply_image_optimizations(optimizations, &image.image_size); + // optimizations = apply_image_optimizations(optimizations, &image.image_size); + + let mut optimizations = self.get_image_optimisations(); // Our svg implementation is very experimental. Let's warn the user. if image.image_path.extension().unwrap_or_default() == "svg" { @@ -70,6 +71,8 @@ impl ImageLoader { optimizations.clear(); } + image.optimizations.extend(optimizations); + let image_loaded_arc = self.image_loaded_arc.clone(); let mut notifier_arc = notifier.clone(); @@ -82,7 +85,6 @@ impl ImageLoader { notifier_arc.set_loading(Some("Loading image...".into())); let now = Instant::now(); let result = image.load_image( - &optimizations, &mut notifier_arc, &backend ); @@ -110,4 +112,16 @@ impl ImageLoader { loading_logic() } } + + // TODO: Make it apply optimizations following + // the user's config and other various factors. + fn get_image_optimisations(&self) -> Vec { + let mut optimizations = Vec::new(); + + // NOTE: wip, so just returning some basic optimizations for testing sake + optimizations.push(ImageOptimization::MonitorDownsampling(130)); + + optimizations + } + } \ No newline at end of file