diff --git a/Cargo.lock b/Cargo.lock index 1089c705..7ddd4c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" dependencies = [ "bitflags 2.6.0", - "libloading 0.8.5", + "libloading 0.7.4", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index b835db3c..f894287f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,6 @@ opt-level = 3 panic = "abort" [profile.dev] -debug = false +debug = true incremental = true -opt-level = 1 +opt-level = 0 diff --git a/src/appstate.rs b/src/appstate.rs index b639a455..2c04be77 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -2,8 +2,10 @@ use crate::{ image_editing::EditState, scrubber::Scrubber, settings::{PersistentSettings, VolatileSettings}, + texture_wrapper::TextureWrapperManager, utils::{ExtendedImageInfo, Frame, Player}, }; + use egui_notify::Toasts; use image::RgbaImage; use nalgebra::Vector2; @@ -65,7 +67,8 @@ pub struct OculanteState { pub extended_info_loading: bool, /// The Player, responsible for loading and sending Frames pub player: Player, - pub current_texture: Option, + //pub current_texture: Option, + pub current_texture: TextureWrapperManager, pub current_path: Option, pub current_image: Option, pub settings_enabled: bool, @@ -92,7 +95,7 @@ pub struct OculanteState { pub filebrowser_id: Option, } -impl OculanteState { +impl<'b> OculanteState { pub fn send_message_info(&self, msg: &str) { _ = self.message_channel.0.send(Message::info(msg)); } @@ -106,7 +109,7 @@ impl OculanteState { } } -impl Default for OculanteState { +impl<'b> Default for OculanteState { fn default() -> OculanteState { let tx_channel = mpsc::channel(); OculanteState { @@ -122,7 +125,7 @@ impl Default for OculanteState { cursor: Default::default(), cursor_relative: Default::default(), sampled_color: [0., 0., 0., 0.], - player: Player::new(tx_channel.0.clone(), 20, 16384), + player: Player::new(tx_channel.0.clone(), 20), texture_channel: tx_channel, message_channel: mpsc::channel(), load_channel: mpsc::channel(), diff --git a/src/image_loader.rs b/src/image_loader.rs index 9ba0bd8e..bd5566fd 100644 --- a/src/image_loader.rs +++ b/src/image_loader.rs @@ -446,8 +446,8 @@ pub fn open_image( let mut decoder = PngDecoder::new(ZCursor::new(contents)); decoder.set_options( DecoderOptions::new_fast() - .set_max_height(50000) - .set_max_width(50000), + .set_max_height(128000) + .set_max_width(128000), ); //animation diff --git a/src/main.rs b/src/main.rs index 6c7968ab..6b44a7f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ mod image_loader; use appstate::*; #[cfg(not(feature = "file_open"))] mod filebrowser; +mod texture_wrapper; pub mod ktx2_loader; // mod events; @@ -207,7 +208,6 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt state.player = Player::new( state.texture_channel.0.clone(), state.persistent_settings.max_cache, - gfx.limits().max_texture_size, ); debug!("matches {:?}", matches); @@ -759,7 +759,7 @@ fn update(app: &mut App, state: &mut OculanteState) { state.toasts.error(e); state.current_image = None; state.is_loaded = true; - state.current_texture = None; + state.current_texture.clear(); } Message::Info(m) => { state @@ -866,7 +866,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O } } // always reset if first image - if state.current_texture.is_none() { + if state.current_texture.get().is_none() { state.reset_image = true; } @@ -923,7 +923,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O debug!("Received compare result"); // always reset if first image - if state.current_texture.is_none() { + if state.current_texture.get().is_none() { state.reset_image = true; } @@ -931,29 +931,36 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O } } - if let Some(tex) = &mut state.current_texture { + if let Some(tex) = &mut state.current_texture.get() { if tex.width() as u32 == img.width() && tex.height() as u32 == img.height() { - img.update_texture(gfx, tex); + img.update_texture_with_texwrap(gfx, tex); } else { - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state + .current_texture + .set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } else { debug!("Setting texture"); - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state + .current_texture + .set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } match &state.persistent_settings.current_channel { // Unpremultiply the image - ColorChannel::Rgb => { - state.current_texture = unpremult(&img).to_texture(gfx, &state.persistent_settings) - } + ColorChannel::Rgb => state.current_texture.set( + unpremult(&img).to_texture_with_texwrap(gfx, &state.persistent_settings), + gfx, + ), // Do nuttin' ColorChannel::Rgba => (), // Display the channel _ => { - state.current_texture = + state.current_texture.set( solo_channel(&img, state.persistent_settings.current_channel as usize) - .to_texture(gfx, &state.persistent_settings) + .to_texture_with_texwrap(gfx, &state.persistent_settings), + gfx, + ); } } state.current_image = Some(img); @@ -1097,7 +1104,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O settings_ui(app, ctx, state, gfx); }); - if let Some(texture) = &state.current_texture { + if let Some(texture) = &state.current_texture.get() { // align to pixel to prevent distortion let aligned_offset_x = state.image_geometry.offset.x.trunc(); let aligned_offset_y = state.image_geometry.offset.y.trunc(); @@ -1114,18 +1121,31 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O } } if state.tiling < 2 { - draw.image(texture) - .blend_mode(BlendMode::NORMAL) - .scale(state.image_geometry.scale, state.image_geometry.scale) - .translate(aligned_offset_x, aligned_offset_y); + texture.draw_textures( + &mut draw, + aligned_offset_x, + aligned_offset_y, + state.image_geometry.scale, + ); } else { - draw.pattern(texture) - .scale(state.image_geometry.scale, state.image_geometry.scale) - .translate(aligned_offset_x, aligned_offset_y) - .size( - texture.width() * state.tiling as f32, - texture.height() * state.tiling as f32, - ); + for yi in 0..state.tiling { + for xi in 0..state.tiling { + //The "old" version used only a static offset, is this correct? + let translate_x = (xi as f32 * texture.width() * state.image_geometry.scale + + state.image_geometry.offset.x) + .trunc(); + let translate_y = (yi as f32 * texture.height() * state.image_geometry.scale + + state.image_geometry.offset.y) + .trunc(); + + texture.draw_textures( + &mut draw, + translate_x, + translate_y, + state.image_geometry.scale, + ); + } + } } if state.persistent_settings.show_frame { @@ -1152,10 +1172,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O > app.window().size().0 as f32; if show_minimap { - draw.image(texture) - .blend_mode(BlendMode::NORMAL) - .translate(offset_x, 100.) - .scale(scale, scale); + texture.draw_textures(&mut draw, offset_x, 100., scale); } } diff --git a/src/texture_wrapper.rs b/src/texture_wrapper.rs new file mode 100644 index 00000000..56492368 --- /dev/null +++ b/src/texture_wrapper.rs @@ -0,0 +1,360 @@ +use log::warn; +use notan::draw::*; +use notan::egui::EguiRegisterTexture; +use notan::egui::SizedTexture; + +use crate::settings::PersistentSettings; +use crate::Draw; +use image::imageops; + +use image::RgbaImage; + +use log::error; +use notan::prelude::{BlendMode, Graphics, Texture, TextureFilter}; + +pub struct TexWrap { + texture_array: Vec, + pub col_count: u32, + pub row_count: u32, + pub col_translation: u32, + pub row_translation: u32, + pub size_vec: (f32, f32), // The whole Texture Array size + pub texture_count: usize, +} + +#[derive(Default)] +pub struct TextureWrapperManager { + current_texture: Option, +} + +impl TextureWrapperManager { + pub fn set(&mut self, tex: Option, gfx: &mut Graphics) { + if let Some(texture) = &mut self.current_texture { + texture.unregister_textures(gfx); + } + self.current_texture = tex; + } + + pub fn get(&mut self) -> &mut Option { + &mut self.current_texture + } + + //TODO: Extend for clearing textures + pub fn clear(&mut self /*, gfx: &mut Graphics */) { + /*if let Some(texture) = &mut self.current_texture { + texture.unregister_textures(gfx); + }*/ + self.current_texture = None; + } +} + +pub struct TextureResponse<'a> { + pub texture: &'a TexturePair, + + pub x_offset_texture: i32, + pub y_offset_texture: i32, + + pub texture_width: i32, + pub texture_height: i32, + pub offset_width: i32, + pub offset_height: i32, + + pub x_tex_left_global: i32, + pub y_tex_top_global: i32, + pub x_tex_right_global: i32, + pub y_tex_bottom_global: i32, +} + +pub struct TexturePair { + pub texture: Texture, + pub texture_egui: SizedTexture, +} + +impl TexWrap { + pub fn from_rgbaimage( + gfx: &mut Graphics, + settings: &PersistentSettings, + image: &RgbaImage, + ) -> Option { + Self::gen_from_rgbaimage(gfx, settings, image, Self::gen_texture_standard) + } + + pub fn from_rgbaimage_premult( + gfx: &mut Graphics, + settings: &PersistentSettings, + image: &RgbaImage, + ) -> Option { + Self::gen_from_rgbaimage(gfx, settings, image, Self::gen_texture_premult) + } + + fn gen_texture_standard( + gfx: &mut Graphics, + bytes: &[u8], + width: u32, + height: u32, + settings: &PersistentSettings, + size_ok: bool, + ) -> Option { + gfx.create_texture() + .from_bytes(bytes, width, height) + .with_mipmaps(settings.use_mipmaps && size_ok) + // .with_format(notan::prelude::TextureFormat::SRgba8) + // .with_premultiplied_alpha() + .with_filter( + if settings.linear_min_filter { + TextureFilter::Linear + } else { + TextureFilter::Nearest + }, + if settings.linear_mag_filter { + TextureFilter::Linear + } else { + TextureFilter::Nearest + }, + ) + // .with_wrap(TextureWrap::Clamp, TextureWrap::Clamp) + .build() + .ok() + } + + fn gen_texture_premult( + gfx: &mut Graphics, + bytes: &[u8], + width: u32, + height: u32, + settings: &PersistentSettings, + size_ok: bool, + ) -> Option { + gfx.create_texture() + .from_bytes(bytes, width, height) + .with_premultiplied_alpha() + .with_mipmaps(settings.use_mipmaps && size_ok) + // .with_format(notan::prelude::TextureFormat::SRgba8) + // .with_premultiplied_alpha() + .with_filter( + if settings.linear_min_filter { + TextureFilter::Linear + } else { + TextureFilter::Nearest + }, + if settings.linear_mag_filter { + TextureFilter::Linear + } else { + TextureFilter::Nearest + }, + ) + // .with_wrap(TextureWrap::Clamp, TextureWrap::Clamp) + .build() + .ok() + } + + fn gen_from_rgbaimage( + gfx: &mut Graphics, + settings: &PersistentSettings, + image: &RgbaImage, + texture_generator_function: fn( + &mut Graphics, + &[u8], + u32, + u32, + &PersistentSettings, + bool, + ) -> Option, + ) -> Option { + const MAX_PIXEL_COUNT: usize = 8192 * 8192; + + let im_w = image.width(); + let im_h = image.height(); + let im_pixel_count = (im_w * im_h) as usize; + let allow_mipmap = im_pixel_count < MAX_PIXEL_COUNT; + + if !allow_mipmap { + warn!( + "Image with {0} pixels too large (max {1} pixels), disabling mipmaps", + im_pixel_count, MAX_PIXEL_COUNT + ); + } + + let im_size = (im_w as f32, im_h as f32); + let max_texture_size = gfx.limits().max_texture_size; + let col_count = (im_w as f32 / max_texture_size as f32).ceil() as u32; + let row_count = (im_h as f32 / max_texture_size as f32).ceil() as u32; + + let mut texture_vec: Vec = Vec::new(); + let row_increment = std::cmp::min(max_texture_size, im_h); + let col_increment = std::cmp::min(max_texture_size, im_w); + let mut fine = true; + + for row_index in 0..row_count { + let tex_start_y = row_index * row_increment; + let tex_height = std::cmp::min(row_increment, im_h - tex_start_y); + for col_index in 0..col_count { + let tex_start_x = col_index * col_increment; + let tex_width = std::cmp::min(col_increment, im_w - tex_start_x); + + let sub_img = + imageops::crop_imm(image, tex_start_x, tex_start_y, tex_width, tex_height); + let my_img = sub_img.to_image(); + let tex = texture_generator_function( + gfx, + my_img.as_ref(), + my_img.width(), + my_img.height(), + settings, + allow_mipmap, + ); + + if let Some(t) = tex { + let egt = gfx.egui_register_texture(&t); + let te = TexturePair { + texture: t, + texture_egui: egt, + }; + texture_vec.push(te); + } else { + //On error + texture_vec.clear(); + fine = false; + break; + } + } + if !fine { + //early exit if we failed + break; + } + } + + if fine { + let texture_count = texture_vec.len(); + Some(TexWrap { + size_vec: im_size, + col_count: col_count, + row_count: row_count, + texture_array: texture_vec, + col_translation: col_increment, + row_translation: row_increment, + texture_count, + }) + } else { + None + } + } + + pub fn draw_textures( + &self, + draw: &mut Draw, + translation_x: f32, + translation_y: f32, + scale: f32, + ) { + let mut tex_idx = 0; + for row_idx in 0..self.row_count { + let translate_y = + translation_y as f64 + scale as f64 * row_idx as f64 * self.row_translation as f64; + for col_idx in 0..self.col_count { + let translate_x = translation_x as f64 + + scale as f64 * col_idx as f64 * self.col_translation as f64; + draw.image(&self.texture_array[tex_idx].texture) + .blend_mode(BlendMode::NORMAL) + .scale(scale, scale) + .translate(translate_x as f32, translate_y as f32); + tex_idx += 1; + } + } + } + + pub fn unregister_textures(&mut self, gfx: &mut Graphics) { + for text in &self.texture_array { + gfx.egui_remove_texture(text.texture_egui.id); + } + } + + pub fn update_textures(&mut self, gfx: &mut Graphics, image: &RgbaImage) { + if self.col_count == 1 && self.row_count == 1 { + if let Err(e) = gfx + .update_texture(&mut self.texture_array[0].texture) + .with_data(image) + .update() + { + error!("{e}"); + } + } else { + let mut tex_index = 0; + for row_index in 0..self.row_count { + let tex_start_y = row_index * self.row_translation; + let tex_height = std::cmp::min(self.row_translation, image.height() - tex_start_y); + for col_index in 0..self.col_count { + let tex_start_x = col_index * self.col_translation; + let tex_width = + std::cmp::min(self.col_translation, image.width() - tex_start_x); + + let sub_img = + imageops::crop_imm(image, tex_start_x, tex_start_y, tex_width, tex_height); + let my_img = sub_img.to_image(); + if let Err(e) = gfx + .update_texture(&mut self.texture_array[tex_index].texture) + .with_data(my_img.as_ref()) + .update() + { + error!("{e}"); + } + tex_index += 1; + } + } + } + } + + pub fn get_texture_at_xy(&self, xa: i32, ya: i32) -> TextureResponse { + let x = xa.max(0).min(self.width() as i32 - 1); + let y = ya.max(0).min(self.height() as i32 - 1); + + let x_idx = x / self.col_translation as i32; + let y_idx = y / self.row_translation as i32; + let tex_idx = + (y_idx * self.col_count as i32 + x_idx).min(self.texture_array.len() as i32 - 1); + let my_tex_pair = &self.texture_array[tex_idx as usize]; + let my_tex = &my_tex_pair.texture; + let width = my_tex.width() as i32; + let height = my_tex.height() as i32; + + let tex_left = x_idx * self.col_translation as i32; + let tex_top = y_idx * self.row_translation as i32; + let tex_right = tex_left + width - 1; + let tex_bottom = tex_top + height - 1; + + let x_offset_texture = xa - tex_left; + let y_offset_texture = ya - tex_top; + + let remaining_width = width - x_offset_texture; + let remaining_height = height - y_offset_texture; + + TextureResponse { + texture: my_tex_pair, + x_offset_texture: x_offset_texture, + y_offset_texture: y_offset_texture, + + texture_width: width, + texture_height: height, + + x_tex_left_global: tex_left, + y_tex_top_global: tex_top, + x_tex_right_global: tex_right, + y_tex_bottom_global: tex_bottom, + + offset_width: remaining_width, + offset_height: remaining_height, + } + } + + pub fn size(&self) -> (f32, f32) { + return self.size_vec; + } + + pub fn width(&self) -> f32 { + return self.size_vec.0; + } + + pub fn height(&self) -> f32 { + return self.size_vec.1; + } +} diff --git a/src/ui.rs b/src/ui.rs index 1972513d..c9e26de7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -455,8 +455,8 @@ impl EguiExt for Ui { /// Proof-of-concept funtion to draw texture completely with egui #[allow(unused)] pub fn image_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { - if let Some(texture) = &state.current_texture { - let tex_id = gfx.egui_register_texture(texture); + if let Some(texture) = &state.current_texture.get() { + //let tex_id = gfx.egui_register_texture(&texture.texture_array[0]); //TODO: Adapt if needed let image_rect = Rect::from_center_size( Pos2::new( @@ -471,12 +471,12 @@ pub fn image_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { ) * state.image_geometry.scale, ); - egui::Painter::new(ctx.clone(), LayerId::background(), ctx.available_rect()).image( + /*egui::Painter::new(ctx.clone(), LayerId::background(), ctx.available_rect()).image( tex_id.id, image_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE, - ); + );*/ } // state.image_geometry.scale; @@ -519,17 +519,15 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { egui::ScrollArea::vertical().auto_shrink([false,true]) .show(ui, |ui| { - if let Some(texture) = &state.current_texture { - let tex_id = gfx.egui_register_texture(texture); + if let Some(texture) = &state.current_texture.get() { - // width of image widget - let desired_width = PANEL_WIDTH - PANEL_WIDGET_OFFSET; + let desired_width = PANEL_WIDTH as f64 - PANEL_WIDGET_OFFSET as f64; - let scale = (desired_width / 8.) / texture.size().0; + let scale = (desired_width / 8.) / texture.size().0 as f64; let uv_center = ( - state.cursor_relative.x / state.image_geometry.dimensions.0 as f32, - (state.cursor_relative.y / state.image_geometry.dimensions.1 as f32), + state.cursor_relative.x as f64 / state.image_geometry.dimensions.0 as f64, + (state.cursor_relative.y as f64 / state.image_geometry.dimensions.1 as f64), ); egui::Grid::new("info") @@ -587,25 +585,39 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { }); // make sure aspect ratio is compensated for the square preview - let ratio = texture.size().0 / texture.size().1; + let ratio = texture.size().0 as f64 / texture.size().1 as f64; let uv_size = (scale, scale * ratio); + let bbox_tl: Pos2; + let bbox_br: Pos2; + if texture.texture_count==1 { + let texture_resonse = texture.get_texture_at_xy(0,0); + ui.add_space(10.); + let preview_rect = ui + .add( + egui::Image::new(texture_resonse.texture.texture_egui) + .maintain_aspect_ratio(false) + .fit_to_exact_size(egui::Vec2::splat(desired_width as f32)) + .rounding(ROUNDING) + .uv(egui::Rect::from_x_y_ranges( + (uv_center.0 - uv_size.0) as f32..=(uv_center.0 + uv_size.0) as f32, + (uv_center.1 - uv_size.1) as f32..=(uv_center.1 + uv_size.1) as f32, + )), + ) + .rect; + bbox_tl = preview_rect.left_top(); + bbox_br = preview_rect.right_bottom(); + } + else{ + (bbox_tl, bbox_br) = render_info_image_tiled(ui, uv_center,uv_size, desired_width, texture); + } + + let bg_color = Color32::BLACK.linear_multiply(0.5); + let preview_rect = egui::Rect::from_min_max(bbox_tl, bbox_br); + ui.advance_cursor_after_rect(preview_rect); - ui.add_space(10.); - let preview_rect = ui - .add( - egui::Image::new(tex_id) - .maintain_aspect_ratio(false) - .fit_to_exact_size(egui::Vec2::splat(desired_width)) - .rounding(ROUNDING) - .uv(egui::Rect::from_x_y_ranges( - uv_center.0 - uv_size.0..=uv_center.0 + uv_size.0, - uv_center.1 - uv_size.1..=uv_center.1 + uv_size.1, - )), - ) - .rect; let stroke_color = Color32::from_white_alpha(240); - let bg_color = Color32::BLACK.linear_multiply(0.5); + ui.painter_at(preview_rect).line_segment( [preview_rect.center_bottom(), preview_rect.center_top()], Stroke::new(4., bg_color), @@ -702,7 +714,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { }); }); - if state.current_texture.is_some() { + if state.current_texture.get().is_some() { ui.styled_collapsing("Alpha tools", |ui| { ui.vertical_centered_justified(|ui| { egui::Frame::none() @@ -717,7 +729,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { .on_hover_text("Highlight pixels with zero alpha and color information") .clicked() { - state.current_texture = highlight_bleed(img).to_texture(gfx, &state.persistent_settings); + state.current_texture.set(highlight_bleed(img).to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } if ui .button("Show semi-transparent pixels") @@ -726,10 +738,11 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { ) .clicked() { - state.current_texture = highlight_semitrans(img).to_texture(gfx, &state.persistent_settings); + + state.current_texture.set(highlight_semitrans(img).to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } if ui.button("Reset image").clicked() { - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } }); @@ -737,7 +750,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { }); } - if state.current_texture.is_some() { + if state.current_texture.get().is_some() { ui.horizontal(|ui| { ui.label("Tiling"); ui.style_mut().spacing.slider_width = ui.available_width() - 16.; @@ -749,6 +762,138 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { }); } +fn render_info_image_tiled( + ui: &mut Ui, + uv_center: (f64, f64), + uv_size: (f64, f64), + desired_width: f64, + texture: &crate::texture_wrapper::TexWrap, +) -> (Pos2, Pos2) { + let xy_size = ( + (texture.width() as f64 * uv_size.0) as i32, + (texture.height() as f64 * uv_size.1) as i32, + ); + + let xy_center = ( + (texture.width() as f64 * uv_center.0) as i32, + (texture.height() as f64 * uv_center.1) as i32, + ); //(16384,1024);// + let sc1 = ( + 2.0 * xy_size.0 as f64 / desired_width, + 2.0 * xy_size.1 as f64 / desired_width, + ); + + //coordinates of the image-view + let mut bbox_tl = egui::pos2(f32::MAX, f32::MAX); + let mut bbox_br = egui::pos2(f32::MIN, f32::MIN); + + //Ui position to start at + let base_ui_curs = nalgebra::Vector2::new(ui.cursor().min.x as f64, ui.cursor().min.y as f64); + let mut curr_ui_curs = base_ui_curs; + //our start position + + //Loop control variables, start end end coordinates of interest + let x_coordinate_end = (xy_center.0 + xy_size.0) as i32; + let mut y_coordinate = xy_center.1 - xy_size.1; + let y_coordinate_end = (xy_center.1 + xy_size.1) as i32; + + while y_coordinate <= y_coordinate_end { + let mut y_coordinate_increment = 0; //increment for y coordinate after x loop + let mut x_coordinate = xy_center.0 - xy_size.0; + curr_ui_curs.x = base_ui_curs.x; + let mut last_display_size_y: f64 = 0.0; + while x_coordinate <= x_coordinate_end { + //get texture tile + let curr_tex_response = + texture.get_texture_at_xy(x_coordinate as i32, y_coordinate as i32); + + //increment coordinates by usable width/height + y_coordinate_increment = curr_tex_response.offset_height; + x_coordinate += curr_tex_response.offset_width; + + //Handling last texture in a row or col + let mut curr_tex_end = nalgebra::Vector2::new( + i32::min(curr_tex_response.x_tex_right_global, x_coordinate_end), + i32::min(curr_tex_response.y_tex_bottom_global, y_coordinate_end), + ); + + //Handling positive coordinate overflow + if curr_tex_response.x_tex_right_global as f32 >= texture.width() - 1.0f32 { + x_coordinate = x_coordinate_end + 1; + curr_tex_end.x += (x_coordinate_end - curr_tex_response.x_tex_right_global).max(0); + } + + if curr_tex_response.y_tex_bottom_global as f32 >= texture.height() - 1.0f32 { + y_coordinate_increment = y_coordinate_end - y_coordinate + 1; + curr_tex_end.y += (y_coordinate_end - curr_tex_response.y_tex_bottom_global).max(0); + } + + //Usable tile size, depending on offsets + let tile_size = nalgebra::Vector2::new( + curr_tex_end.x + - curr_tex_response.x_offset_texture + - curr_tex_response.x_tex_left_global + + 1, + curr_tex_end.y + - curr_tex_response.y_offset_texture + - curr_tex_response.y_tex_top_global + + 1, + ); + + //Display size - tile size scaled + let display_size = + nalgebra::Vector2::new(tile_size.x as f64 / sc1.0, tile_size.y as f64 / sc1.1); + + //Texture display range + let uv_start = nalgebra::Vector2::new( + curr_tex_response.x_offset_texture as f64 / curr_tex_response.texture_width as f64, + curr_tex_response.y_offset_texture as f64 / curr_tex_response.texture_height as f64, + ); + + let uv_end = nalgebra::Vector2::new( + (curr_tex_end.x - curr_tex_response.x_tex_left_global + 1) as f64 + / curr_tex_response.texture_width as f64, + (curr_tex_end.y - curr_tex_response.y_tex_top_global + 1) as f64 + / curr_tex_response.texture_height as f64, + ); + + //let tex_id2 = gfx.egui_register_texture(curr_tex_response.texture); + let draw_tl_32 = Pos2::new(curr_ui_curs.x as f32, curr_ui_curs.y as f32); + let draw_br_32 = Pos2::new( + (curr_ui_curs.x + display_size.x) as f32, + (curr_ui_curs.y + display_size.y) as f32, + ); + let r_ret = egui::Rect::from_min_max(draw_tl_32, draw_br_32); + + egui::Image::new(curr_tex_response.texture.texture_egui) + .maintain_aspect_ratio(false) + .fit_to_exact_size(egui::Vec2::new( + display_size.x as f32, + display_size.y as f32, + )) + .uv(egui::Rect::from_x_y_ranges( + uv_start.x as f32..=uv_end.x as f32, + uv_start.y as f32..=uv_end.y as f32, + )) + .paint_at(ui, r_ret); + + //Update display cursor + curr_ui_curs.x += display_size.x; + last_display_size_y = display_size.y; + + //Update coordinates for preview rectangle + bbox_tl.x = bbox_tl.x.min(r_ret.left()); + bbox_tl.y = bbox_tl.y.min(r_ret.top()); + bbox_br.x = bbox_br.x.max(r_ret.right()); + bbox_br.y = bbox_br.y.max(r_ret.bottom()); + } + //Update y coordinates + y_coordinate += y_coordinate_increment; + curr_ui_curs.y += last_display_size_y; + } + (bbox_tl, bbox_br) +} + pub fn settings_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mut Graphics) { let mut settings_enabled = state.settings_enabled; egui::Window::new("Preferences") @@ -873,18 +1018,18 @@ pub fn settings_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: if ui.styled_checkbox(&mut state.persistent_settings.linear_mag_filter, "Interpolate when zooming in").on_hover_text("When zooming in, do you prefer to see individual pixels or an interpolation?").changed(){ if let Some(img) = &state.current_image { if state.edit_state.result_image_op.is_empty() { - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } else { - state.current_texture = state.edit_state.result_pixel_op.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(state.edit_state.result_pixel_op.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } } if ui.styled_checkbox(&mut state.persistent_settings.linear_min_filter, "Interpolate when zooming out").on_hover_text("When zooming out, do you prefer crisper or smoother pixels?").changed(){ if let Some(img) = &state.current_image { if state.edit_state.result_image_op.is_empty() { - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } else { - state.current_texture = state.edit_state.result_pixel_op.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(state.edit_state.result_pixel_op.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } } @@ -893,9 +1038,9 @@ pub fn settings_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: if ui.styled_checkbox(&mut state.persistent_settings.use_mipmaps, "Use mipmaps").on_hover_text("When zooming out, less memory will be used. Faster performance, but blurry.").changed(){ if let Some(img) = &state.current_image { if state.edit_state.result_image_op.is_empty() { - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } else { - state.current_texture = state.edit_state.result_pixel_op.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(state.edit_state.result_pixel_op.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } } @@ -1181,7 +1326,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu { if let Some(img) = &state.current_image { state.image_geometry.dimensions = img.dimensions(); - state.current_texture = img.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } if ui @@ -1448,15 +1593,14 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu } // Update the texture - if let Some(tex) = &mut state.current_texture { + if let Some(tex) = &mut state.current_texture.get() { if let Some(img) = &state.current_image { if tex.width() as u32 == state.edit_state.result_pixel_op.width() && state.edit_state.result_pixel_op.height() == img.height() { - state.edit_state.result_pixel_op.update_texture(gfx, tex); + state.edit_state.result_pixel_op.update_texture_with_texwrap(gfx, tex); } else { - state.current_texture = - state.edit_state.result_pixel_op.to_texture(gfx, &state.persistent_settings); + state.current_texture.set(state.edit_state.result_pixel_op.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } } } @@ -2242,16 +2386,22 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu if let Some(img) = &state.current_image { match &state.persistent_settings.current_channel { ColorChannel::Rgb => { - state.current_texture = - unpremult(img).to_texture(gfx, &state.persistent_settings) + state.current_texture.set( + unpremult(img).to_texture_with_texwrap(gfx, &state.persistent_settings), + gfx, + ); } ColorChannel::Rgba => { - state.current_texture = img.to_texture(gfx, &state.persistent_settings) + state + .current_texture + .set(img.to_texture_with_texwrap(gfx, &state.persistent_settings), gfx); } _ => { - state.current_texture = + state.current_texture.set( solo_channel(img, state.persistent_settings.current_channel as usize) - .to_texture(gfx, &state.persistent_settings) + .to_texture_with_texwrap(gfx, &state.persistent_settings), + gfx, + ); } } } @@ -2379,7 +2529,7 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu } } - if state.current_texture.is_some() && window_x > ui.cursor().left() + 80. { + if state.current_texture.get().is_some() && window_x > ui.cursor().left() + 80. { if tooltip( unframed_button(PLACEHOLDER, ui), "Clear image", diff --git a/src/utils.rs b/src/utils.rs index cf977d4c..f4a90c50 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,7 +4,7 @@ use img_parts::{Bytes, DynImage, ImageEXIF}; use log::{debug, error}; use nalgebra::{clamp, Vector2}; use notan::graphics::Texture; -use notan::prelude::{App, Graphics, TextureFilter}; +use notan::prelude::{App, Graphics}; use rayon::prelude::ParallelIterator; use rayon::slice::ParallelSliceMut; use serde::{Deserialize, Serialize}; @@ -30,6 +30,7 @@ use crate::image_editing::{self, ImageOperation}; use crate::image_loader::{open_image, rotate_rgbaimage}; use crate::settings::PersistentSettings; use crate::shortcuts::{lookup, InputEvent, Shortcuts}; +use crate::texture_wrapper::TexWrap; pub const SUPPORTED_EXTENSIONS: &[&str] = &[ "bmp", @@ -97,9 +98,9 @@ pub struct ExtendedImageInfo { pub num_pixels: usize, pub num_transparent_pixels: usize, pub num_colors: usize, - pub red_histogram: Vec<(i32, i32)>, - pub green_histogram: Vec<(i32, i32)>, - pub blue_histogram: Vec<(i32, i32)>, + pub red_histogram: Vec<(i32, u64)>, + pub green_histogram: Vec<(i32, u64)>, + pub blue_histogram: Vec<(i32, u64)>, pub exif: HashMap, pub raw_exif: Option, pub name: String, @@ -162,9 +163,9 @@ impl ExtendedImageInfo { } pub fn from_image(img: &RgbaImage) -> Self { - let mut hist_r: [u32; 256] = [0; 256]; - let mut hist_g: [u32; 256] = [0; 256]; - let mut hist_b: [u32; 256] = [0; 256]; + let mut hist_r: [u64; 256] = [0; 256]; + let mut hist_g: [u64; 256] = [0; 256]; + let mut hist_b: [u64; 256] = [0; 256]; let num_pixels = img.width() as usize * img.height() as usize; let mut num_transparent_pixels = 0; @@ -197,22 +198,22 @@ impl ExtendedImageInfo { full_colors += intensity.count_ones(); } - let green_histogram: Vec<(i32, i32)> = hist_g + let green_histogram: Vec<(i32, u64)> = hist_g .iter() .enumerate() - .map(|(k, v)| (k as i32, *v as i32)) + .map(|(k, v)| (k as i32, *v)) .collect(); - let red_histogram: Vec<(i32, i32)> = hist_r + let red_histogram: Vec<(i32, u64)> = hist_r .iter() .enumerate() - .map(|(k, v)| (k as i32, *v as i32)) + .map(|(k, v)| (k as i32, *v)) .collect(); - let blue_histogram: Vec<(i32, i32)> = hist_b + let blue_histogram: Vec<(i32, u64)> = hist_b .iter() .enumerate() - .map(|(k, v)| (k as i32, *v as i32)) + .map(|(k, v)| (k as i32, *v)) .collect(); Self { @@ -234,13 +235,12 @@ pub struct Player { pub image_sender: Sender, pub stop_sender: Sender<()>, pub cache: Cache, - pub max_texture_size: u32, watcher: HashMap, } impl Player { /// Create a new Player - pub fn new(image_sender: Sender, cache_size: usize, max_texture_size: u32) -> Player { + pub fn new(image_sender: Sender, cache_size: usize) -> Player { let (stop_sender, _): (Sender<()>, Receiver<()>) = mpsc::channel(); Player { image_sender, @@ -249,7 +249,6 @@ impl Player { data: Default::default(), cache_size, }, - max_texture_size, watcher: Default::default(), } } @@ -302,8 +301,6 @@ impl Player { self.image_sender.clone(), message_sender, stop_receiver, - self.max_texture_size, - forced_frame_source, ); if let Ok(meta) = std::fs::metadata(img_location) { @@ -327,8 +324,6 @@ pub fn send_image_threaded( texture_sender: Sender, message_sender: Sender, stop_receiver: Receiver<()>, - max_texture_size: u32, - forced_frame_source: Option, ) { let loc = img_location.to_owned(); @@ -353,44 +348,11 @@ pub fn send_image_threaded( // a "normal image (no animation)" if f.source == FrameSource::Still { debug!("Received image in {:?}", timer.elapsed()); - if let Ok(rotated_img) = rotate_rgbaimage(&f.buffer, &path) { debug!("Image has been rotated."); f.buffer = rotated_img; } - - let largest_side = f.buffer.dimensions().0.max(f.buffer.dimensions().1); - - // Check if texture is too large to fit on the texture - if largest_side > max_texture_size { - _ = message_sender.send(Message::warn("This image exceeded the maximum resolution and will be be scaled down.")); - let scale_factor = max_texture_size as f32 / largest_side as f32; - let new_dimensions = ( - (f.buffer.dimensions().0 as f32 * scale_factor) - .min(max_texture_size as f32) - as u32, - (f.buffer.dimensions().1 as f32 * scale_factor) - .min(max_texture_size as f32) - as u32, - ); - - if let Some(ref fs) = forced_frame_source { - f.source = fs.clone(); - } - - let mut frame = f; - - let op = ImageOperation::Resize { - dimensions: new_dimensions, - aspect: true, - filter: image_editing::ScaleFilter::Box, - }; - _ = op.process_image(&mut frame.buffer); - let _ = texture_sender.send(frame); - } else { - let _ = texture_sender.send(f); - } - + let _ = texture_sender.send(f); return; } if f.source == FrameSource::Animation { @@ -677,7 +639,7 @@ pub trait ImageExt { unimplemented!() } - fn to_texture(&self, _: &mut Graphics, _settings: &PersistentSettings) -> Option { + fn to_texture_with_texwrap(&self, _: &mut Graphics, _settings: &PersistentSettings) -> Option { unimplemented!() } @@ -689,6 +651,10 @@ pub trait ImageExt { unimplemented!() } + fn update_texture_with_texwrap(&self, _: &mut Graphics, _: &mut TexWrap) { + unimplemented!() + } + #[allow(unused)] fn to_image(&self, _: &mut Graphics) -> Option { unimplemented!() @@ -700,28 +666,8 @@ impl ImageExt for RgbaImage { Vector2::new(self.width() as f32, self.height() as f32) } - fn to_texture(&self, gfx: &mut Graphics, settings: &PersistentSettings) -> Option { - gfx.clean(); - gfx.create_texture() - .from_bytes(self, self.width(), self.height()) - .with_mipmaps(settings.use_mipmaps) - // .with_format(notan::prelude::TextureFormat::SRgba8) - // .with_premultiplied_alpha() - .with_filter( - if settings.linear_min_filter { - TextureFilter::Linear - } else { - TextureFilter::Nearest - }, - if settings.linear_mag_filter { - TextureFilter::Linear - } else { - TextureFilter::Nearest - }, - ) - // .with_wrap(TextureWrap::Clamp, TextureWrap::Clamp) - .build() - .ok() + fn to_texture_with_texwrap(&self, gfx: &mut Graphics, settings: &PersistentSettings) -> Option { + TexWrap::from_rgbaimage(gfx, settings, self) } fn to_texture_premult(&self, gfx: &mut Graphics) -> Option { @@ -741,6 +687,10 @@ impl ImageExt for RgbaImage { error!("{e}"); } } + + fn update_texture_with_texwrap(&self, gfx: &mut Graphics, texture: &mut TexWrap) { + texture.update_textures(gfx, self); + } } impl ImageExt for (i32, i32) { @@ -812,7 +762,7 @@ pub fn clear_image(state: &mut OculanteState) { debug!("Clearing image. Next is {}", next_img.display()); if state.scrubber.entries.len() == 0 { state.current_image = None; - state.current_texture = None; + state.current_texture.clear(); state.current_path = None; state.image_info = None; return; diff --git a/tests/ultrahigh.png b/tests/ultrahigh.png new file mode 100644 index 00000000..9cb5ff66 Binary files /dev/null and b/tests/ultrahigh.png differ diff --git a/tests/ultrasize.png b/tests/ultrasize.png new file mode 100644 index 00000000..1615803f Binary files /dev/null and b/tests/ultrasize.png differ diff --git a/tests/ultrawide.png b/tests/ultrawide.png new file mode 100644 index 00000000..b531085e Binary files /dev/null and b/tests/ultrawide.png differ