From 0b3bbb16d05949c0ee373792bc6f737621655e86 Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 18 Jan 2025 23:37:06 +0100 Subject: [PATCH] simplify metadata and add slice filter for DICOM --- README.md | 1 + src/appstate.rs | 10 ++- src/image_editing.rs | 39 ++++++++++-- src/image_loader.rs | 30 +-------- src/main.rs | 42 ++++--------- src/thumbnails.rs | 2 +- src/ui/edit_ui.rs | 16 ++--- src/ui/info_ui.rs | 27 +++++++- src/ui/top_bar.rs | 5 -- src/utils.rs | 146 +++++++++++++++++++++++++++---------------- 10 files changed, 181 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 07b0a38..7e162d6 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Uninstalling Oculante is a quick process, just delete the executable and delete - webp (via `libwebp-sys` - `image` had _very_ limited format support) - farbfeld - DDS (DXT1-5, via `dds-rs`) +- DICOM (via dicom-rs) - Some metadata supported, too. - psd (via `psd`) - svg (via `resvg`) - exr (via `exr-rs`), tonemapped diff --git a/src/appstate.rs b/src/appstate.rs index 1b5ac58..7e85c6f 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -73,7 +73,7 @@ pub struct OculanteState { pub current_path: Option, pub current_image: Option, pub settings_enabled: bool, - pub image_info: Option, + pub image_metadata: Option, pub tiling: usize, pub mouse_grab: bool, pub key_grab: bool, @@ -133,7 +133,11 @@ impl<'b> Default for OculanteState { cursor: Default::default(), cursor_relative: Default::default(), sampled_color: [0., 0., 0., 0.], - player: Player::new(tx_channel.0.clone(), 20, meta_channel.0.clone(), msg_channel.0.clone()), + player: Player::new( + tx_channel.0.clone(), + 20, + msg_channel.0.clone(), + ), texture_channel: tx_channel, message_channel: msg_channel, load_channel: mpsc::channel(), @@ -144,7 +148,7 @@ impl<'b> Default for OculanteState { current_image: Default::default(), current_path: Default::default(), settings_enabled: Default::default(), - image_info: Default::default(), + image_metadata: Default::default(), tiling: 1, mouse_grab: Default::default(), key_grab: Default::default(), diff --git a/src/image_editing.rs b/src/image_editing.rs index 0e39d67..396c41b 100644 --- a/src/image_editing.rs +++ b/src/image_editing.rs @@ -161,6 +161,8 @@ impl fmt::Display for ImgOpItem { pub enum ImageOperation { ColorConverter(ColorTypeExt), Brightness(i32), + /// discard pixels around a threshold: position and range, bool for mode. + Slice(u8, u8, bool), Expression(String), Desaturate(u8), Posterize(u8), @@ -241,6 +243,7 @@ impl fmt::Display for ImageOperation { match *self { Self::ColorConverter(_) => write!(f, "Color Type"), Self::Brightness(_) => write!(f, "Brightness"), + Self::Slice(..) => write!(f, "Slice"), Self::Noise { .. } => write!(f, "Noise"), Self::Desaturate(_) => write!(f, "Desaturate"), Self::Posterize(_) => write!(f, "Posterize"), @@ -317,6 +320,21 @@ impl ImageOperation { x } Self::Brightness(val) => ui.styled_slider(val, -255..=255), + Self::Slice(position, range, hard) => { + let mut x = ui.allocate_response(vec2(0.0, 0.0), Sense::click_and_drag()); + ui.label("Position"); + if ui.styled_slider(position, 0..=255).changed() { + x.mark_changed(); + } + ui.label("Range"); + if ui.styled_slider(range, 0..=255).changed() { + x.mark_changed(); + } + if ui.styled_checkbox(hard, "Smooth").changed() { + x.mark_changed(); + } + x + } Self::Exposure(val) => ui.styled_slider(val, -100..=100), Self::ChromaticAberration(val) => ui.styled_slider(val, 0..=255), Self::Filter3x3(val) => { @@ -1465,9 +1483,6 @@ impl ImageOperation { } Self::Exposure(amt) => { let amt = (*amt as f32 / 100.) * 4.; - - // *p = *p * Vector4::new(2., 2., 2., 2.).; - p[0] = p[0] * (2_f32).powf(amt); p[1] = p[1] * (2_f32).powf(amt); p[2] = p[2] * (2_f32).powf(amt); @@ -1481,7 +1496,23 @@ impl ImageOperation { p[1] = egui::lerp(bounds.0..=bounds.1, p[1]); p[2] = egui::lerp(bounds.0..=bounds.1, p[2]); } - + Self::Slice(position, range, smooth) => { + let normalized_pos = (*position as f32) / 255.; + let normalized_range = (*range as f32) / 255.; + for i in 0..=2 { + if *smooth { + let distance = 1.0 - (normalized_pos - p[i]).abs(); + p[i] *= distance + normalized_range; + } else { + if p[i] > normalized_pos + normalized_range { + p[i] = 0.; + } + if p[i] < (normalized_pos - normalized_range) { + p[i] = 0.; + } + } + } + } Self::GradientMap(col) => { let brightness = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]; // let res = interpolate_spline(col, brightness); diff --git a/src/image_loader.rs b/src/image_loader.rs index e87a500..c34f48c 100644 --- a/src/image_loader.rs +++ b/src/image_loader.rs @@ -1,5 +1,5 @@ use crate::ktx2_loader::CompressedImageFormats; -use crate::utils::{fit, ExtendedImageInfo, Frame}; +use crate::utils::{fit, Frame}; use crate::{appstate::Message, ktx2_loader, FONT}; use log::{debug, error, info}; use psd::Psd; @@ -29,7 +29,6 @@ use zune_png::zune_core::result::DecodingResult; pub fn open_image( img_location: &Path, message_sender: Option>, - metadata_sender: Option>, ) -> Result> { let (sender, receiver): (Sender, Receiver) = channel(); let img_location = (*img_location).to_owned(); @@ -96,33 +95,6 @@ pub fn open_image( "dcm" | "ima" => { use dicom_pixeldata::PixelDecoder; let obj = dicom_object::open_file(img_location)?; - - - // WIP: Find out interesting items to display - for name in &[ - "StudyDate", - "ModalitiesInStudy", - "Modality", - "SourceType", - "ImageType", - "Manufacturer", - "InstitutionName", - "PrivateDataElement", - "PrivateDataElementName", - "OperatorsName", - "ManufacturerModelName", - "PatientName", - "PatientBirthDate", - "PatientAge", - "PixelSpacing", - ] { - if let Ok(e) = obj.element_by_name(name) { - if let Ok(s) = e.to_str() { - info!("{name}: {s}"); - } - } - } - let image = obj.decode_pixel_data()?; let dynamic_image = image.to_dynamic_image(0)?; _ = sender.send(Frame::new_still(dynamic_image)); diff --git a/src/main.rs b/src/main.rs index e90750d..abc4ee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,9 +197,7 @@ 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, - state.extended_info_channel.0.clone(), - state.message_channel.0.clone() - + state.message_channel.0.clone(), ); debug!("matches {:?}", matches); @@ -224,16 +222,12 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt if let Ok(first_img_location) = find_first_image_in_directory(location) { state.is_loaded = false; state.current_path = Some(first_img_location.clone()); - state - .player - .load(&first_img_location); + state.player.load(&first_img_location); } } else { state.is_loaded = false; state.current_path = Some(location.clone().clone()); - state - .player - .load(&location); + state.player.load(&location); }; } @@ -257,7 +251,6 @@ fn init(_app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins) -> OculanteSt state.player.load_advanced( &location, Some(Frame::ImageCollectionMember(Default::default())), - ); }; state.scrubber.entries = paths_to_open.clone(); @@ -560,11 +553,6 @@ fn process_events(app: &mut App, state: &mut OculanteState, evt: Event) { } if key_pressed(app, state, InfoMode) { state.persistent_settings.info_enabled = !state.persistent_settings.info_enabled; - send_extended_info( - &state.current_image, - &state.current_path, - &state.extended_info_channel, - ); } if key_pressed(app, state, EditMode) { state.persistent_settings.edit_enabled = !state.persistent_settings.edit_enabled; @@ -727,9 +715,7 @@ fn update(app: &mut App, state: &mut OculanteState) { let t = app.timer.elapsed_f32() % 0.8; if t <= 0.05 { trace!("chk mod {}", t); - state - .player - .check_modified(p); + state.player.check_modified(p); } } @@ -777,14 +763,14 @@ fn update(app: &mut App, state: &mut OculanteState) { } // redraw if extended info is missing so we make sure it's promply displayed - if state.persistent_settings.info_enabled && state.image_info.is_none() { + if state.persistent_settings.info_enabled && state.image_metadata.is_none() { app.window().request_frame(); } // check extended info has been sent if let Ok(info) = state.extended_info_channel.1.try_recv() { debug!("Received extended image info for {}", info.name); - state.image_info = Some(info); + state.image_metadata = Some(info); app.window().request_frame(); } @@ -959,7 +945,7 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O } } state.redraw = false; - state.image_info = None; + // state.image_info = None; } Frame::EditResult(_) => { state.redraw = false; @@ -1046,14 +1032,12 @@ fn drawe(app: &mut App, gfx: &mut Graphics, plugins: &mut Plugins, state: &mut O // In those cases, we want the image to stay as it is. // TODO: PERF: This copies the image buffer. This should also maybe not run for animation frames // although it looks cool. - if state.persistent_settings.info_enabled { - debug!("Sending extended info"); - send_extended_info( - &state.current_image, - &state.current_path, - &state.extended_info_channel, - ); - } + state.image_metadata = None; + send_extended_info( + &state.current_image, + &state.current_path, + &state.extended_info_channel, + ); } if state.redraw { diff --git a/src/thumbnails.rs b/src/thumbnails.rs index 9d196e8..cf5cc73 100644 --- a/src/thumbnails.rs +++ b/src/thumbnails.rs @@ -94,7 +94,7 @@ pub fn generate>(source_path: P) -> Result<()> { source_path.as_ref().display(), dest_path.display() ); - let f = open_image(source_path.as_ref(), None, None)?; + let f = open_image(source_path.as_ref(), None)?; let i = f.recv()?.get_image().context("Can't get buffer")?; debug!("\tOpened {}", source_path.as_ref().display()); diff --git a/src/ui/edit_ui.rs b/src/ui/edit_ui.rs index 1fccbca..8a81208 100644 --- a/src/ui/edit_ui.rs +++ b/src/ui/edit_ui.rs @@ -43,6 +43,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu ImgOpItem::new(ImageOperation::Add([0, 0, 0])), ImgOpItem::new(ImageOperation::Mult([255, 255, 255])), ImgOpItem::new(ImageOperation::Fill([255, 255, 255, 255])), + ImgOpItem::new(ImageOperation::Slice(128, 20, false)), // Colour Mapping and Conversion ImgOpItem::new(ImageOperation::LUT("Lomography Redscale 100".into())), ImgOpItem::new(ImageOperation::GradientMap(vec![ @@ -458,7 +459,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu key_slice.as_slice(), &mut state.volatile_settings, |p| { - _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &encoders); + _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &encoders); }, ctx, ); @@ -470,14 +471,14 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu if let Some(p) = &state.current_path { let text = if p.exists() { "Overwrite" } else { "Save"}; let modal = show_modal(ui.ctx(), "Overwrite?", |_|{ - _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}"))); + _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}"))); }, "overwrite"); if ui.button(text).on_hover_text("Saves the image. This will create a new file or overwrite an existing one.").clicked() { if p.exists() { modal.open(); } else { - _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_info, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}"))); + _ = save_with_encoding(&state.edit_state.result_pixel_op, p, &state.image_metadata, &state.volatile_settings.encoding_options).map(|_| state.send_message_info("Saved")).map_err(|e| state.send_message_err(&format!("Error: {e}"))); } } @@ -629,14 +630,7 @@ pub fn edit_ui(app: &mut App, ctx: &Context, state: &mut OculanteState, gfx: &mu state.image_geometry.dimensions = state.edit_state.result_pixel_op.dimensions(); - if pixels_changed && state.persistent_settings.info_enabled { - state.image_info = None; - send_extended_info( - &Some(state.edit_state.result_pixel_op.clone()), - &state.current_path, - &state.extended_info_channel, - ); - } + }); } diff --git a/src/ui/info_ui.rs b/src/ui/info_ui.rs index 91b6b73..d18d117 100644 --- a/src/ui/info_ui.rs +++ b/src/ui/info_ui.rs @@ -194,7 +194,6 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) -> ui.ctx().request_repaint(); ui.ctx().request_repaint_after(Duration::from_millis(500)); state.current_path = Some(path); - state.image_info = None; } }); }); @@ -269,6 +268,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) -> ui.styled_slider(&mut state.tiling, 1..=10); }); } + advanced_ui(ui, state); }); @@ -277,7 +277,7 @@ pub fn info_ui(ctx: &Context, state: &mut OculanteState, _gfx: &mut Graphics) -> } fn advanced_ui(ui: &mut Ui, state: &mut OculanteState) { - if let Some(info) = &state.image_info { + if let Some(info) = &state.image_metadata { egui::Grid::new("extended").num_columns(2).show(ui, |ui| { ui.label("Number of colors"); ui.label_right(format!("{}", info.num_colors)); @@ -317,6 +317,29 @@ fn advanced_ui(ui: &mut Ui, state: &mut OculanteState) { }); } + if let Some(dicom) = &info.dicom { + ui.styled_collapsing("DICOM", |ui| { + dark_panel(ui, |ui| { + for (key, val) in &dicom.dicom_data { + ui.scope(|ui| { + ui.style_mut().override_font_id = + Some(FontId::new(14., FontFamily::Name("bold".into()))); + ui.colored_label( + if ui.style().visuals.dark_mode { + Color32::from_gray(200) + } else { + Color32::from_gray(20) + }, + key, + ); + }); + ui.label(val); + ui.separator(); + } + }); + }); + } + let red_vals = Line::new( info.red_histogram .iter() diff --git a/src/ui/top_bar.rs b/src/ui/top_bar.rs index b66d2e0..82ccb70 100644 --- a/src/ui/top_bar.rs +++ b/src/ui/top_bar.rs @@ -173,11 +173,6 @@ pub fn main_menu(ui: &mut Ui, state: &mut OculanteState, app: &mut App, gfx: &mu .clicked() { state.persistent_settings.info_enabled = !state.persistent_settings.info_enabled; - send_extended_info( - &state.current_image, - &state.current_path, - &state.extended_info_channel, - ); } if window_x > ui.cursor().left() + 80. { if tooltip( diff --git a/src/utils.rs b/src/utils.rs index 11a5951..52b4c40 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,7 @@ use arboard::Clipboard; use img_parts::{Bytes, DynImage, ImageEXIF}; -use log::{debug, error}; +use log::{debug, error, info}; use nalgebra::{clamp, Vector2}; use notan::graphics::Texture; use notan::prelude::{App, Graphics}; @@ -93,7 +93,13 @@ fn is_pixel_fully_transparent(p: &Rgba) -> bool { p.0 == [0, 0, 0, 0] } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] +pub struct DicomData { + pub physical_size: (f32, f32), + pub dicom_data: HashMap, +} + +#[derive(Debug, Clone, Default)] pub struct ExtendedImageInfo { pub num_pixels: usize, pub num_transparent_pixels: usize, @@ -102,33 +108,11 @@ pub struct ExtendedImageInfo { pub green_histogram: Vec<(i32, u64)>, pub blue_histogram: Vec<(i32, u64)>, pub exif: HashMap, + pub dicom: Option, pub raw_exif: Option, pub name: String, } -pub fn delete_file(state: &mut OculanteState) { - if let Some(p) = &state.current_path { - #[cfg(not(any(target_os = "netbsd", target_os = "freebsd")))] - { - _ = trash::delete(p); - } - #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] - { - _ = std::fs::remove_file(p) - } - - state.send_message_info(&format!( - "Deleted {}", - p.file_name() - .map(|f| f.to_string_lossy().to_string()) - .unwrap_or_default() - )); - // remove from cache so we don't suceed to load it agaim - state.player.cache.data.remove(p); - } - clear_image(state); -} - impl ExtendedImageInfo { pub fn with_exif(&mut self, image_path: &Path) -> Result<()> { self.name = image_path.to_string_lossy().to_string(); @@ -162,6 +146,48 @@ impl ExtendedImageInfo { Ok(()) } + pub fn with_dicom(&mut self, image_path: &Path) -> Result<()> { + self.name = image_path.to_string_lossy().to_string(); + if image_path.extension() != Some(OsStr::new("dcm")) + || image_path.extension() != Some(OsStr::new("ima")) + { + let obj = dicom_object::open_file(image_path)?; + let mut dicom_data = HashMap::new(); + + // WIP: Find out interesting items to display + for name in &[ + "StudyDate", + "ModalitiesInStudy", + "Modality", + "SourceType", + "ImageType", + "Manufacturer", + "InstitutionName", + "PrivateDataElement", + "PrivateDataElementName", + "OperatorsName", + "ManufacturerModelName", + "PatientName", + "PatientBirthDate", + "PatientAge", + "PixelSpacing", + ] { + if let Ok(e) = obj.element_by_name(name) { + if let Ok(s) = e.to_str() { + info!("{name}: {s}"); + dicom_data.insert(name.to_string(), s.to_string()); + } + } + } + self.dicom = Some(DicomData { + physical_size: (0.0, 0.0), + dicom_data, + }) + } + + Ok(()) + } + pub fn from_image(img: &RgbaImage) -> Self { let mut hist_r: [u64; 256] = [0; 256]; let mut hist_g: [u64; 256] = [0; 256]; @@ -226,6 +252,7 @@ impl ExtendedImageInfo { raw_exif: Default::default(), name: Default::default(), exif: Default::default(), + dicom: Default::default(), } } } @@ -234,7 +261,6 @@ impl ExtendedImageInfo { pub struct Player { pub image_sender: Sender, pub stop_sender: Sender<()>, - pub metadata_sender: Sender, pub message_sender: Sender, pub cache: Cache, watcher: HashMap, @@ -242,13 +268,16 @@ pub struct Player { impl Player { /// Create a new Player - pub fn new(image_sender: Sender, cache_size: usize, metadata_sender: Sender, message_sender: Sender) -> Player { + pub fn new( + image_sender: Sender, + cache_size: usize, + message_sender: Sender, + ) -> Player { let (stop_sender, _): (Sender<()>, Receiver<()>) = mpsc::channel(); Player { image_sender, stop_sender, message_sender, - metadata_sender, cache: Cache { data: Default::default(), cache_size, @@ -275,11 +304,8 @@ impl Player { } } - pub fn load_advanced( - &mut self, - img_location: &Path, - forced_frame_source: Option, - ) { + /// The main loading function of the player + pub fn load_advanced(&mut self, img_location: &Path, forced_frame_source: Option) { debug!("Stopping player on load"); self.stop(); let (stop_sender, stop_receiver): (Sender<()>, Receiver<()>) = mpsc::channel(); @@ -304,7 +330,6 @@ impl Player { img_location, self.image_sender.clone(), self.message_sender.clone(), - self.metadata_sender.clone(), stop_receiver, forced_frame_source, ); @@ -329,7 +354,6 @@ pub fn send_image_threaded( img_location: &Path, texture_sender: Sender, message_sender: Sender, - metadata_sender: Sender, stop_receiver: Receiver<()>, forced_frame_source: Option, ) { @@ -340,7 +364,7 @@ pub fn send_image_threaded( let mut framecache = vec![]; let mut timer = std::time::Instant::now(); - match open_image(&loc, Some(message_sender.clone()), Some(metadata_sender.clone())) { + match open_image(&loc, Some(message_sender.clone())) { Ok(frame_receiver) => { debug!("Got a frame receiver from opening image"); @@ -532,6 +556,29 @@ pub fn zoomratio(i: f32, s: f32) -> f32 { i * s * 0.1 } +pub fn delete_file(state: &mut OculanteState) { + if let Some(p) = &state.current_path { + #[cfg(not(any(target_os = "netbsd", target_os = "freebsd")))] + { + _ = trash::delete(p); + } + #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] + { + _ = std::fs::remove_file(p) + } + + state.send_message_info(&format!( + "Deleted {}", + p.file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default() + )); + // remove from cache so we don't suceed to load it agaim + state.player.cache.data.remove(p); + } + clear_image(state); +} + /// Display RGBA values nicely pub fn disp_col(col: [f32; 4]) -> String { format!("{:.0},{:.0},{:.0},{:.0}", col[0], col[1], col[2], col[3]) @@ -671,7 +718,9 @@ pub fn send_extended_info( let mut e_info = ExtendedImageInfo::from_image(&copied_img); if let Some(p) = current_path { _ = e_info.with_exif(&p); + _ = e_info.with_dicom(&p); } + debug!("Sending extended info"); _ = sender.send(e_info); }); } @@ -791,9 +840,7 @@ pub fn last_image(state: &mut OculanteState) { if &next_img != img_location { state.is_loaded = false; *img_location = next_img; - state - .player - .load(img_location); + state.player.load(img_location); } } } @@ -805,9 +852,7 @@ pub fn first_image(state: &mut OculanteState) { if &next_img != img_location { state.is_loaded = false; *img_location = next_img; - state - .player - .load(img_location); + state.player.load(img_location); } } } @@ -820,16 +865,14 @@ pub fn clear_image(state: &mut OculanteState) { state.current_image = None; state.current_texture.clear(); state.current_path = None; - state.image_info = None; + state.image_metadata = None; return; } // prevent reload if at last or first if Some(&next_img) != state.current_path.as_ref() { state.is_loaded = false; state.current_path = Some(next_img.clone()); - state - .player - .load(&next_img); + state.player.load(&next_img); } } @@ -839,9 +882,7 @@ pub fn next_image(state: &mut OculanteState) { if Some(&next_img) != state.current_path.as_ref() { state.is_loaded = false; state.current_path = Some(next_img.clone()); - state - .player - .load(&next_img); + state.player.load(&next_img); } } @@ -851,9 +892,7 @@ pub fn prev_image(state: &mut OculanteState) { if Some(&prev_img) != state.current_path.as_ref() { state.is_loaded = false; state.current_path = Some(prev_img.clone()); - state - .player - .load(&prev_img); + state.player.load(&prev_img); } } @@ -911,7 +950,8 @@ pub fn compare_next(state: &mut OculanteState) { state.current_image = None; state.player.load_advanced( path, - Some(Frame::CompareResult(Default::default(), geo.clone()))); + Some(Frame::CompareResult(Default::default(), geo.clone())), + ); state.current_path = Some(path.clone()); } }