use std::{fmt::Display, sync::Arc};
use bladvak::eframe::egui;
use bladvak::egui_extras::{Column, TableBuilder};
use bladvak::errors::{AppError, ErrorManager};
use image::{ColorType, DynamicImage, GenericImage, GenericImageView, Pixel};
use crate::TarsierApp;
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug, Clone, Copy)]
pub enum EditMode {
Selection,
Drawing,
}
impl Display for EditMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditMode::Selection => write!(f, "Selection"),
EditMode::Drawing => write!(f, "Drawing"),
}
}
}
#[derive(Debug)]
pub struct Others {
pub convert_to: ColorType,
}
impl Default for Others {
fn default() -> Self {
Self {
convert_to: ColorType::Rgba8,
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ImageOperations {
pub blur: f32,
pub hue_rotation: i32,
pub brighten: i32,
pub contrast: f32,
pub pen_radius: u32,
pub pen_color: [u8; 4],
pub mode: EditMode,
pub drawing_blend: bool,
pub drawing_continuous_line: bool,
#[serde(skip)]
pub other: Others,
}
impl Default for ImageOperations {
fn default() -> Self {
Self {
blur: 10.0,
hue_rotation: 50,
brighten: 50,
contrast: 1.0,
pen_radius: 1,
pen_color: [0, 0, 0, 255],
mode: EditMode::Selection,
drawing_blend: false,
drawing_continuous_line: true,
other: Others {
convert_to: ColorType::Rgba8,
},
}
}
}
impl TarsierApp {
pub(crate) fn image_info(&mut self, ui: &mut egui::Ui) {
ui.heading("Image Info");
ui.label(format!("Size: {}x{}", self.img.width(), self.img.height()));
ui.label(format!("Format: {:?}", self.img.color()));
match &self.exif {
Some(exif) => {
ui.collapsing("Exif info", |ui| {
TableBuilder::new(ui)
.max_scroll_height(100.0)
.striped(true)
.column(Column::auto())
.column(Column::auto())
.column(Column::remainder())
.header(20.0, |mut header| {
header.col(|ui| {
ui.label("Exif tag");
});
header.col(|ui| {
ui.label("IFD idx");
});
header.col(|ui| {
ui.label("exif value");
});
})
.body(|mut body| {
for field in exif.fields() {
body.row(30.0, |mut row| {
row.col(|ui| {
ui.label(format!("{}", field.tag));
});
row.col(|ui| {
ui.label(format!("{}", field.ifd_num));
});
row.col(|ui| {
ui.label(format!(
"{}",
field.display_value().with_unit(exif)
));
});
});
}
});
});
}
None => {
ui.label("No exif detected");
}
}
}
pub(crate) fn combo_box_color_type(ui: &mut egui::Ui, value: &mut ColorType) {
egui::ComboBox::from_id_salt("convert_box")
.selected_text(format!("{value:?}"))
.show_ui(ui, |ui| {
ui.selectable_value(value, ColorType::L8, format!("{:?}", ColorType::L8));
ui.selectable_value(value, ColorType::L16, format!("{:?}", ColorType::L16));
ui.selectable_value(value, ColorType::La8, format!("{:?}", ColorType::La8));
ui.selectable_value(value, ColorType::La16, format!("{:?}", ColorType::La16));
ui.selectable_value(value, ColorType::Rgb8, format!("{:?}", ColorType::Rgb8));
ui.selectable_value(value, ColorType::Rgb16, format!("{:?}", ColorType::Rgb16));
ui.selectable_value(value, ColorType::Rgb32F, format!("{:?}", ColorType::Rgb32F));
ui.selectable_value(value, ColorType::Rgba8, format!("{:?}", ColorType::Rgba8));
ui.selectable_value(value, ColorType::Rgba16, format!("{:?}", ColorType::Rgba16));
ui.selectable_value(
value,
ColorType::Rgba32F,
format!("{:?}", ColorType::Rgba32F),
);
});
}
fn button_convert(&mut self, ui: &mut egui::Ui) {
ui.label("Convert");
Self::combo_box_color_type(ui, &mut self.image_operations.other.convert_to);
if ui.button("Convert").clicked() {
let new_img = match self.image_operations.other.convert_to {
ColorType::L8 => self.img.to_luma8().into(),
ColorType::L16 => self.img.to_luma16().into(),
ColorType::La8 => self.img.to_luma_alpha8().into(),
ColorType::La16 => self.img.to_luma_alpha16().into(),
ColorType::Rgb8 => self.img.to_rgb8().into(),
ColorType::Rgb16 => self.img.to_rgb16().into(),
ColorType::Rgb32F => self.img.to_rgb32f().into(),
ColorType::Rgba16 => self.img.to_rgba16().into(),
ColorType::Rgba32F => self.img.to_rgba32f().into(),
ColorType::Rgba8 | _ => self.img.to_rgba8().into(),
};
self.update_image(new_img);
}
}
pub(crate) fn image_operations(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
self.button_convert(ui);
ui.separator();
self.button_outline(ui, error_manager);
ui.separator();
if ui.button("edge detection").clicked() {
self.apply_op(
|img| {
img.filter3x3(&[
0.0, -1.0, 0.0, -1.0, 4.0, -1.0, 0.0, -1.0, 0.0, ])
},
error_manager,
);
}
ui.separator();
if ui.button("Grayscale").clicked() {
let color = self.img.color();
self.apply_op(
|img| {
let inner = img.grayscale();
match color {
ColorType::L8 => inner.to_luma8().into(),
ColorType::L16 => inner.to_luma16().into(),
ColorType::La8 => inner.to_luma_alpha8().into(),
ColorType::La16 => inner.to_luma_alpha16().into(),
ColorType::Rgb8 => inner.to_rgb8().into(),
ColorType::Rgb16 => inner.to_rgb16().into(),
ColorType::Rgba16 => inner.to_rgba16().into(),
ColorType::Rgba8 | _ => inner.to_rgba8().into(),
}
},
error_manager,
);
}
ui.separator();
if ui.button("invert").clicked() {
self.apply_op(
|img| {
let mut copied_img = img.clone();
copied_img.invert();
copied_img
},
error_manager,
);
}
ui.separator();
ui.add(egui::Slider::new(
&mut self.image_operations.blur,
0.0..=100.0,
));
if ui.button("Blur").clicked() {
let blur = self.image_operations.blur;
self.apply_op(|img| img.blur(blur), error_manager);
}
ui.separator();
ui.add(egui::Slider::new(
&mut self.image_operations.hue_rotation,
0..=360,
));
if ui.button("hue rotate").clicked() {
let hue_rotation = self.image_operations.hue_rotation;
self.apply_op(|img| img.huerotate(hue_rotation), error_manager);
}
ui.separator();
ui.add(egui::Slider::new(
&mut self.image_operations.brighten,
-100..=100,
));
if ui.button("brighten").clicked() {
let brighten = self.image_operations.brighten;
self.apply_op(|img| img.brighten(brighten), error_manager);
}
ui.separator();
ui.add(egui::Slider::new(
&mut self.image_operations.contrast,
-50.0..=50.0,
));
if ui.button("contrast").clicked() {
let contrast = self.image_operations.contrast;
self.apply_op(|img| img.adjust_contrast(contrast), error_manager);
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub(crate) fn apply_op<F>(&mut self, func: F, error_manager: &mut ErrorManager)
where
F: Fn(&DynamicImage) -> DynamicImage,
{
let current_selection = self.cursor_info.selection.map(|selection| {
(
selection.min.x as u32,
selection.min.y as u32,
selection.max.x as u32,
selection.max.y as u32,
)
});
if let Some(selection) = current_selection {
let cropped_img = self.img.crop(
selection.0,
selection.1,
selection.2 - selection.0,
selection.3 - selection.1,
);
let inner = func(&cropped_img);
if let Err(e) = self.img.copy_from(&inner, selection.0, selection.1) {
error_manager.add_error(AppError::new_with_source(
"Cannot update selected image part",
Arc::new(e),
));
}
self.updated_image();
} else {
let new_img = func(&self.img);
self.update_image(new_img);
}
}
pub fn button_drawing(&mut self, ui: &mut egui::Ui) {
let max_radius = self.img.width().max(self.img.height());
ui.add(egui::Slider::new(
&mut self.image_operations.pen_radius,
1..=max_radius / 4,
))
.on_hover_text("Pen radius");
let [r, g, b, a] = self.image_operations.pen_color;
let mut color = egui::Color32::from_rgba_premultiplied(r, g, b, a);
egui::color_picker::color_edit_button_srgba(
ui,
&mut color,
egui::color_picker::Alpha::OnlyBlend,
)
.on_hover_text("Pen color");
self.image_operations.pen_color = [color.r(), color.g(), color.b(), color.a()];
ui.checkbox(&mut self.image_operations.drawing_blend, "Blend");
ui.checkbox(
&mut self.image_operations.drawing_continuous_line,
"Continuous line",
);
}
#[allow(clippy::similar_names)]
pub fn button_outline(&mut self, ui: &mut egui::Ui, error_manager: &mut ErrorManager) {
if ui.button("sobel outline").clicked() {
self.apply_op(
|img| {
let mut img = img.clone();
let sobel_x = img.filter3x3(&[
-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0, ]);
let sobel_x2 = img.filter3x3(&[
1.0, 0.0, -1.0, 2.0, 0.0, -2.0, 1.0, 0.0, -1.0, ]);
let sobel_y = img.filter3x3(&[
-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0, ]);
let sobel_y2 = img.filter3x3(&[
1.0, 2.0, 1.0, 0.0, 0.0, 0.0, -1.0, -2.0, -1.0, ]);
for y in 0..img.height() {
for x in 0..img.width() {
let mut pixel = sobel_x.get_pixel(x, y);
let pixel_y = sobel_y.get_pixel(x, y);
pixel.blend(&pixel_y);
let pixel_x2 = sobel_x2.get_pixel(x, y);
pixel.blend(&pixel_x2);
let pixel_y2 = sobel_y2.get_pixel(x, y);
pixel.blend(&pixel_y2);
img.put_pixel(x, y, pixel);
}
}
img
},
error_manager,
);
}
}
#[allow(clippy::cast_sign_loss)]
pub fn draw_point(&mut self, x_center: i32, y_center: i32) {
let radius = i32::try_from(self.image_operations.pen_radius).unwrap_or(10);
let color = image::Rgba(self.image_operations.pen_color);
for y in (y_center - radius)..=(y_center + radius) {
for x in (x_center - radius)..=(x_center + radius) {
if (x - x_center).pow(2) + (y - y_center).pow(2) <= radius.pow(2) {
if x >= 0
&& y >= 0
&& x < i32::try_from(self.img.width()).unwrap_or(i32::MAX)
&& y < i32::try_from(self.img.height()).unwrap_or(i32::MAX)
{
if self.image_operations.drawing_blend {
let mut current_pixel = self.img.get_pixel(x as u32, y as u32);
current_pixel.blend(&color);
self.img.put_pixel(x as u32, y as u32, current_pixel);
} else {
self.img.put_pixel(x as u32, y as u32, color);
}
}
}
}
}
self.updated_image();
}
}