use image::{DynamicImage, ImageBuffer, Luma, Rgba};
use imageproc::{drawing, edges};
use rusttype::{Font, Scale};
use crate::draw;
#[derive(Clone, Debug)]
pub struct Annotation {
pub text: String,
pub position: Position,
}
#[derive(Debug, Copy, Clone)]
pub enum Position {
Top,
Middle,
Bottom,
}
impl Annotation {
pub fn top(text: impl Into<String>) -> Annotation {
Annotation {
text: text.into(),
position: Position::Top,
}
}
pub fn middle(text: impl Into<String>) -> Annotation {
Annotation {
text: text.into(),
position: Position::Middle,
}
}
pub fn bottom(text: impl Into<String>) -> Annotation {
Annotation {
text: text.into(),
position: Position::Bottom,
}
}
fn position(&self, width: u32, height: u32, text_width: u32, text_height: u32) -> (u32, u32) {
mod position {
pub fn top(width: u32, _height: u32, text_width: u32, text_height: u32) -> (u32, u32) {
let x = (width / 2).saturating_sub(text_width / 2);
let y = {
let text_height = text_height as f32;
(text_height * 0.2) as u32
};
(x, y)
}
pub fn middle(
width: u32,
height: u32,
text_width: u32,
text_height: u32,
) -> (u32, u32) {
let x = (width / 2).saturating_sub(text_width / 2);
let y = (height / 2) - (text_height / 2);
(x, y)
}
pub fn bottom(
width: u32,
height: u32,
text_width: u32,
text_height: u32,
) -> (u32, u32) {
let x = (width / 2).saturating_sub(text_width / 2);
let y = {
let height = height as f32;
let text_height = text_height as f32;
(height - (text_height * 1.2)) as u32
};
(x, y)
}
}
match self.position {
Position::Top => position::top(width, height, text_width, text_height),
Position::Middle => position::middle(width, height, text_width, text_height),
Position::Bottom => position::bottom(width, height, text_width, text_height),
}
}
pub fn render_text(
&self,
pixels: &mut DynamicImage,
font: &Font,
scale_factor: f32,
c_width: u32,
c_height: u32,
) {
let scale = Scale::uniform(scale_factor);
let text_width = calculate_text_width(&self.text, font, scale);
let font_height = font_height(font, scale);
if (text_width as f32 * 1.2) as u32 > c_width && self.text.contains(' ') {
let (left, right) = split_text(&self.text);
let line_offset = font_height as i32;
match self.position {
Position::Top => {
let text_width = calculate_text_width(left, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
left,
0,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
let text_width = calculate_text_width(right, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
right,
line_offset,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
}
Position::Middle => {
let text_width = calculate_text_width(left, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
left,
-(line_offset / 2),
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
let text_width = calculate_text_width(right, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
right,
line_offset / 2,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
}
Position::Bottom => {
let text_width = calculate_text_width(left, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
left,
-line_offset,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
let text_width = calculate_text_width(right, font, scale);
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
right,
0,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
}
}
} else {
let position = self.position(c_width, c_height, text_width, font_height);
render_line(
&self.text,
0,
position,
(text_width, font_height),
scale_factor,
font,
pixels,
);
}
}
}
fn render_line(
text: &str,
y_offset: i32,
root_position: (u32, u32),
text_dimensions: (u32, u32),
scale_factor: f32,
font: &Font,
pixels: &mut DynamicImage,
) {
use crate::{AA_FACTOR, AA_FACTOR_FLOAT};
const WHITE_PIXEL: Rgba<u8> = Rgba([255, 255, 255, 255]);
const BLACK_PIXEL: Rgba<u8> = Rgba([0, 0, 0, 255]);
let (text_width, text_height) = text_dimensions;
let scale = Scale::uniform(scale_factor * AA_FACTOR_FLOAT);
let (x, y) = root_position;
let x = x * AA_FACTOR;
let y = (y as i32 + y_offset) as u32 * AA_FACTOR;
let edge_canvas_width = text_width * AA_FACTOR;
let mut edge_rendering =
ImageBuffer::from_pixel(edge_canvas_width, text_height * AA_FACTOR, Luma([0u8]));
draw::text(&mut edge_rendering, Luma([255u8]), 0, 0, scale, font, text);
let edge_rendering = edges::canny(&edge_rendering, 255.0, 255.0);
let edge_pixels = edge_rendering
.pixels()
.enumerate()
.filter(|px| *px.1 == Luma([255u8]))
.map(|(idx, _)| {
let idx = idx as u32;
let x = idx % edge_canvas_width + x;
let y = idx / edge_canvas_width + y;
(x, y)
});
let radius = (0.09 * scale_factor) as i32;
for (x, y) in edge_pixels {
drawing::draw_hollow_circle_mut(pixels, (x as i32, y as i32), radius, BLACK_PIXEL);
}
draw::text(pixels, WHITE_PIXEL, x, y, scale, font, text);
}
fn font_height(font: &Font, scale: Scale) -> u32 {
use rusttype::VMetrics;
let VMetrics {
ascent, descent, ..
} = font.v_metrics(scale);
((ascent - descent) * 1.1) as u32
}
fn calculate_text_width(s: &str, font: &Font, scale: Scale) -> u32 {
2 + font
.glyphs_for(s.chars())
.map(|glyph| glyph.scaled(scale).h_metrics().advance_width)
.sum::<f32>() as u32
}
fn split_text(s: &str) -> (&str, &str) {
let middle_index = s.len() / 2;
let space_indexen = s.char_indices().filter(|idx| idx.1 == ' ').map(|idx| idx.0);
let mut split_index = None;
for idx in space_indexen {
match split_index {
None => split_index = Some(idx),
Some(s_idx) => {
if (middle_index as i32 - s_idx as i32).abs()
> (middle_index as i32 - idx as i32).abs()
{
split_index = Some(idx);
} else {
break;
}
}
}
}
let split_index = split_index
.expect("Wtf, bro? You weren't supposed to call this function if you didn't have a space.");
(&s[..split_index], &s[(split_index + 1)..])
}
#[cfg(test)]
mod tests {
#[test]
fn split_text() {
let input = "text to be split";
let expected = ("text to", "be split");
assert_eq!(expected, super::split_text(input));
}
}