use std::fmt;
use std::fmt::Write;
use rten_imageproc::{bounding_rect, min_area_rect, Point, Rect, RotatedRect, Vec2};
pub trait TextItem {
fn chars(&self) -> &[TextChar];
fn bounding_rect(&self) -> Rect {
bounding_rect(self.chars().iter().map(|c| &c.rect)).expect("expected valid rect")
}
fn rotated_rect(&self) -> RotatedRect {
let points: Vec<_> = self
.chars()
.iter()
.flat_map(|c| c.rect.corners())
.map(Point::to_f32)
.collect();
let rect = min_area_rect(&points).expect("expected valid rect");
rect.orient_towards(Vec2::from_yx(-1., 0.))
}
}
fn fmt_text_item<TI: TextItem>(item: &TI, f: &mut fmt::Formatter) -> fmt::Result {
for c in item.chars().iter().map(|c| c.char) {
f.write_char(c)?;
}
Ok(())
}
impl fmt::Display for TextLine {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt_text_item(self, f)
}
}
#[derive(Clone)]
pub struct TextChar {
pub char: char,
pub rect: Rect,
}
#[derive(Clone)]
pub struct TextLine {
chars: Vec<TextChar>,
}
impl TextLine {
pub fn new(chars: Vec<TextChar>) -> TextLine {
assert!(!chars.is_empty(), "Text lines must not be empty");
TextLine { chars }
}
pub fn words(&self) -> impl Iterator<Item = TextWord<'_>> {
self.chars()
.split(|c| c.char == ' ')
.filter(|chars| !chars.is_empty())
.map(TextWord::new)
}
}
impl TextItem for TextLine {
fn chars(&self) -> &[TextChar] {
&self.chars
}
}
pub struct TextWord<'a> {
chars: &'a [TextChar],
}
impl<'a> TextWord<'a> {
fn new(chars: &'a [TextChar]) -> TextWord<'a> {
assert!(!chars.is_empty(), "Text words must not be empty");
TextWord { chars }
}
}
impl TextItem for TextWord<'_> {
fn chars(&self) -> &[TextChar] {
self.chars
}
}
impl fmt::Display for TextWord<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt_text_item(self, f)
}
}
#[cfg(test)]
mod tests {
use rten_imageproc::{BoundingRect, Point, Rect, Vec2};
use super::{TextChar, TextItem, TextLine, TextWord};
fn gen_text_chars(text: &str, width: i32) -> Vec<TextChar> {
text.chars()
.enumerate()
.map(|(i, char)| TextChar {
char,
rect: Rect::from_tlhw(0, i as i32 * width, 25, width),
})
.collect()
}
#[test]
fn test_item_display() {
let chars = gen_text_chars("foo bar baz", 10 );
let line = TextLine::new(chars);
assert_eq!(line.to_string(), "foo bar baz");
}
#[test]
fn test_item_rotated_rect() {
let char_width = 10;
let chars = gen_text_chars("foo", char_width);
let word = TextWord::new(&chars);
assert_eq!(
word.bounding_rect(),
Rect::from_tlhw(0, 0, 25, char_width * 3)
);
let rot_rect = word.rotated_rect();
assert_eq!(rot_rect.bounding_rect(), word.bounding_rect().to_f32());
assert_eq!(rot_rect.up_axis(), Vec2::from_yx(-1., 0.));
assert_eq!(
word.rotated_rect().corners(),
[(25, 30), (25, 0), (0, 0), (0, 30)].map(|(y, x)| Point::from_yx(y as f32, x as f32))
);
}
#[test]
fn test_line_words() {
let char_width = 10;
let chars = gen_text_chars("foo bar baz ", char_width);
let line = TextLine::new(chars);
let words: Vec<_> = line.words().collect();
assert_eq!(words.len(), 3);
assert_eq!(words[0].to_string(), "foo");
assert_eq!(
words[0].bounding_rect(),
Rect::from_tlhw(0, 0, 25, char_width * 3)
);
assert_eq!(words[1].to_string(), "bar");
assert_eq!(
words[1].bounding_rect(),
Rect::from_tlhw(0, char_width * 4, 25, char_width * 3)
);
assert_eq!(words[2].to_string(), "baz");
assert_eq!(
words[2].bounding_rect(),
Rect::from_tlhw(0, char_width * 9, 25, char_width * 3)
);
}
}