use bladvak::eframe::egui;
use bladvak::egui_extras::{Column, TableBuilder};
use bladvak::errors::{AppError, ErrorManager};
use image::{ColorType, DynamicImage, GenericImage, GenericImageView, Pixel};
use std::sync::Arc;
use crate::TarsierApp;
#[derive(Debug)]
pub(crate) struct Others {
pub(crate) convert_to: ColorType,
}
impl Default for Others {
fn default() -> Self {
Self {
convert_to: ColorType::Rgba8,
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct ImageOperations {
pub(crate) blur: f32,
pub(crate) hue_rotation: i32,
pub(crate) brighten: i32,
pub(crate) contrast: f32,
#[serde(skip)]
pub(crate) other: Others,
}
impl Default for ImageOperations {
fn default() -> Self {
Self {
blur: 10.0,
hue_rotation: 50,
brighten: 50,
contrast: 1.0,
other: Others {
convert_to: ColorType::Rgba8,
},
}
}
}
impl TarsierApp {
pub(crate) fn image_info(
&mut self,
ui: &mut egui::Ui,
error_manager: &mut bladvak::ErrorManager,
) {
let Some(document) = self.documents.get_current_doc_mut() else {
return;
};
ui.heading("Image Info");
ui.label(format!(
"Size: {}x{}",
document.img.width(),
document.img.height()
));
ui.label(format!("Format: {:?}", document.img.color()));
match &document.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");
}
}
if let Some(document) = self.documents.get_current_doc_mut()
&& ui.button("Copy image").clicked()
&& let Err(e) = bladvak::utils::set_image_in_clipboard(
ui.ctx(),
document.img.width() as usize,
document.img.height() as usize,
document.img.to_rgba8().as_flat_samples().as_slice(),
)
{
error_manager.add_error(e);
}
}
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) {
let Some(document) = self.documents.get_current_doc_mut() else {
return;
};
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 => document.img.to_luma8().into(),
ColorType::L16 => document.img.to_luma16().into(),
ColorType::La8 => document.img.to_luma_alpha8().into(),
ColorType::La16 => document.img.to_luma_alpha16().into(),
ColorType::Rgb8 => document.img.to_rgb8().into(),
ColorType::Rgb16 => document.img.to_rgb16().into(),
ColorType::Rgb32F => document.img.to_rgb32f().into(),
ColorType::Rgba16 => document.img.to_rgba16().into(),
ColorType::Rgba32F => document.img.to_rgba32f().into(),
ColorType::Rgba8 | _ => document.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.quick_operations(ui, error_manager);
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();
let Some(document) = self.documents.get_current_doc_mut() else {
return;
};
if ui.button("Grayscale").clicked() {
let color = document.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 Some(document) = self.documents.get_current_doc_mut() else {
return;
};
let current_selection = document.selection.rectangle.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 = document.img.crop(
selection.0,
selection.1,
selection.2 - selection.0,
selection.3 - selection.1,
);
let inner = func(&cropped_img);
if let Err(e) = document.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(&document.img);
self.update_image(new_img);
}
}
#[allow(clippy::similar_names)]
pub(crate) 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(crate) fn draw_point(&mut self, x_center: i32, y_center: i32) {
let Some(document) = self.documents.get_current_doc_mut() else {
return;
};
let drawing = self.mode.drawing;
let radius = i32::try_from(drawing.pen_radius).unwrap_or(10);
let color = image::Rgba(drawing.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(document.img.width()).unwrap_or(i32::MAX)
&& y < i32::try_from(document.img.height()).unwrap_or(i32::MAX)
{
if drawing.drawing_blend {
let mut current_pixel = document.img.get_pixel(x as u32, y as u32);
current_pixel.blend(&color);
document.img.put_pixel(x as u32, y as u32, current_pixel);
} else {
document.img.put_pixel(x as u32, y as u32, color);
}
}
}
}
}
self.updated_image();
}
}