use eframe::egui::{Color32, Pos2, Rect, Vec2};
use image::RgbaImage;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Tool {
Select,
Pen,
Line,
Arrow,
Rect,
Ellipse,
Highlight,
Pixelate,
Text,
Marker,
}
impl Tool {
pub fn label(self) -> &'static str {
match self {
Tool::Select => "Select",
Tool::Pen => "Pen",
Tool::Line => "Line",
Tool::Arrow => "Arrow",
Tool::Rect => "Box",
Tool::Ellipse => "Ellipse",
Tool::Highlight => "Highlight",
Tool::Pixelate => "Blur",
Tool::Text => "Text",
Tool::Marker => "Marker",
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Style {
pub color: Color32,
pub width: f32,
pub font_size: f32,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Shape {
Pen { points: Vec<Pos2> },
Line { a: Pos2, b: Pos2 },
Arrow { a: Pos2, b: Pos2 },
Rect { rect: Rect },
Ellipse { rect: Rect },
Highlight { rect: Rect },
Pixelate { rect: Rect },
Text { pos: Pos2, text: String },
Marker { pos: Pos2, number: u32, target: Option<Pos2> },
}
#[derive(Clone, PartialEq, Debug)]
pub struct Annotation {
pub shape: Shape,
pub style: Style,
pub rotation: f32,
}
impl Annotation {
pub fn new(shape: Shape, style: Style) -> Self {
Self { shape, style, rotation: 0.0 }
}
}
pub fn highlight_color(color: Color32) -> Color32 {
Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 70)
}
pub struct ArrowGeometry {
pub shaft_end: Pos2,
pub head: [Pos2; 3],
}
pub fn arrow_geometry(a: Pos2, b: Pos2, stroke_width: f32) -> ArrowGeometry {
let dir = b - a;
let len = dir.length().max(0.001);
let dir = dir / len;
let head_len = (stroke_width * 6.0).max(14.0).min(len);
let head_width = head_len * 0.7;
let base = b - dir * head_len;
let normal = Vec2::new(-dir.y, dir.x);
ArrowGeometry {
shaft_end: b - dir * (head_len * 0.6),
head: [b, base + normal * head_width * 0.5, base - normal * head_width * 0.5],
}
}
pub fn marker_radius(style: &Style) -> f32 {
style.font_size * 0.9
}
pub fn marker_target(pos: Pos2, target: Option<Pos2>, style: &Style) -> Option<Pos2> {
let min_len = marker_radius(style) * 1.6;
target.filter(|t| (*t - pos).length() >= min_len)
}
pub fn pixelate_block(img_w: u32, img_h: u32) -> u32 {
(img_w.min(img_h) / 120).clamp(8, 48)
}
pub fn clamp_px(v: f32, limit: u32) -> u32 {
if v <= 0.0 {
return 0;
}
let limit_f = limit.min(1 << 24);
if v >= limit_f as f32 { limit_f } else { v as u32 }
}
pub fn apply_pixelate(img: &mut RgbaImage, rect: Rect, block: u32) {
let (w, h) = img.dimensions();
let x0 = clamp_px(rect.min.x.floor(), w);
let y0 = clamp_px(rect.min.y.floor(), h);
let x1 = clamp_px(rect.max.x.ceil(), w);
let y1 = clamp_px(rect.max.y.ceil(), h);
if x1 <= x0 || y1 <= y0 {
return;
}
let mut by = y0;
while by < y1 {
let bh = block.min(y1 - by);
let mut bx = x0;
while bx < x1 {
let bw = block.min(x1 - bx);
let mut sum = [0u64; 3];
for y in by..by + bh {
for x in bx..bx + bw {
let p = img.get_pixel(x, y).0;
sum[0] += u64::from(p[0]);
sum[1] += u64::from(p[1]);
sum[2] += u64::from(p[2]);
}
}
let n = u64::from(bw) * u64::from(bh);
let channel = |s: u64| u8::try_from(s / n).unwrap_or(u8::MAX);
let avg = image::Rgba([channel(sum[0]), channel(sum[1]), channel(sum[2]), 255]);
for y in by..by + bh {
for x in bx..bx + bw {
img.put_pixel(x, y, avg);
}
}
bx += bw;
}
by += bh;
}
}
pub fn composite_pixelates<'a>(
img: &mut RgbaImage,
annotations: impl IntoIterator<Item = &'a Annotation>,
) {
let (w, h) = img.dimensions();
let block = pixelate_block(w, h);
for ann in annotations {
if let Shape::Pixelate { rect } = &ann.shape {
apply_pixelate(img, *rect, block);
}
}
}
pub fn pixelate_rects<'a>(annotations: impl IntoIterator<Item = &'a Annotation>) -> Vec<Rect> {
annotations
.into_iter()
.filter_map(|a| match &a.shape {
Shape::Pixelate { rect } => Some(*rect),
_ => None,
})
.collect()
}