use crate::utils::Point2D;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextResult {
pub text: String,
pub confidence: f32,
pub bounding_box: BoundingBox,
pub characters: Vec<CharacterResult>,
pub words: Vec<WordResult>,
pub lines: Vec<LineResult>,
pub language: Option<String>,
pub metadata: HashMap<String, String>,
}
impl TextResult {
pub fn new(text: String, confidence: f32, bounding_box: BoundingBox) -> Self {
Self {
text,
confidence,
bounding_box,
characters: Vec::new(),
words: Vec::new(),
lines: Vec::new(),
language: None,
metadata: HashMap::new(),
}
}
pub fn average_character_confidence(&self) -> f32 {
if self.characters.is_empty() {
self.confidence
} else {
let sum: f32 = self.characters.iter().map(|c| c.confidence).sum();
sum / self.characters.len() as f32
}
}
pub fn character_count(&self) -> usize {
self.characters.len()
}
pub fn word_count(&self) -> usize {
self.words.len()
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CharacterResult {
pub character: char,
pub confidence: f32,
pub bounding_box: BoundingBox,
pub code_point: u32,
pub properties: CharacterProperties,
}
impl CharacterResult {
pub fn new(character: char, confidence: f32, bounding_box: BoundingBox) -> Self {
Self {
character,
confidence,
bounding_box,
code_point: character as u32,
properties: CharacterProperties::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WordResult {
pub text: String,
pub confidence: f32,
pub bounding_box: BoundingBox,
pub characters: Vec<CharacterResult>,
pub properties: WordProperties,
}
impl WordResult {
pub fn new(text: String, confidence: f32, bounding_box: BoundingBox) -> Self {
Self {
text,
confidence,
bounding_box,
characters: Vec::new(),
properties: WordProperties::default(),
}
}
pub fn average_character_confidence(&self) -> f32 {
if self.characters.is_empty() {
self.confidence
} else {
let sum: f32 = self.characters.iter().map(|c| c.confidence).sum();
sum / self.characters.len() as f32
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineResult {
pub text: String,
pub confidence: f32,
pub bounding_box: BoundingBox,
pub words: Vec<WordResult>,
pub properties: LineProperties,
}
impl LineResult {
pub fn new(text: String, confidence: f32, bounding_box: BoundingBox) -> Self {
Self {
text,
confidence,
bounding_box,
words: Vec::new(),
properties: LineProperties::default(),
}
}
pub fn average_word_confidence(&self) -> f32 {
if self.words.is_empty() {
self.confidence
} else {
let sum: f32 = self.words.iter().map(|w| w.confidence).sum();
sum / self.words.len() as f32
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BoundingBox {
pub left: u32,
pub top: u32,
pub right: u32,
pub bottom: u32,
}
impl BoundingBox {
pub fn new(left: u32, top: u32, right: u32, bottom: u32) -> Self {
Self {
left,
top,
right,
bottom,
}
}
pub fn from_center(center: Point2D, width: u32, height: u32) -> Self {
let half_width = width as f32 / 2.0;
let half_height = height as f32 / 2.0;
Self {
left: (center.x - half_width) as u32,
top: (center.y - half_height) as u32,
right: (center.x + half_width) as u32,
bottom: (center.y + half_height) as u32,
}
}
pub fn width(&self) -> u32 {
self.right.saturating_sub(self.left)
}
pub fn height(&self) -> u32 {
self.bottom.saturating_sub(self.top)
}
pub fn area(&self) -> u32 {
self.width() * self.height()
}
pub fn center(&self) -> Point2D {
Point2D::new(
(self.left + self.right) as f32 / 2.0,
(self.top + self.bottom) as f32 / 2.0,
)
}
pub fn contains(&self, x: u32, y: u32) -> bool {
x >= self.left && x < self.right && y >= self.top && y < self.bottom
}
pub fn intersects(&self, other: &BoundingBox) -> bool {
!(self.right <= other.left
|| other.right <= self.left
|| self.bottom <= other.top
|| other.bottom <= self.top)
}
pub fn intersection_area(&self, other: &BoundingBox) -> u32 {
if !self.intersects(other) {
return 0;
}
let left = self.left.max(other.left);
let top = self.top.max(other.top);
let right = self.right.min(other.right);
let bottom = self.bottom.min(other.bottom);
(right - left) * (bottom - top)
}
pub fn union(&self, other: &BoundingBox) -> BoundingBox {
BoundingBox {
left: self.left.min(other.left),
top: self.top.min(other.top),
right: self.right.max(other.right),
bottom: self.bottom.max(other.bottom),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CharacterProperties {
pub is_digit: bool,
pub is_letter: bool,
pub is_whitespace: bool,
pub is_punctuation: bool,
pub width: u32,
pub height: u32,
pub font_size: f32,
}
impl Default for CharacterProperties {
fn default() -> Self {
Self {
is_digit: false,
is_letter: false,
is_whitespace: false,
is_punctuation: false,
width: 0,
height: 0,
font_size: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WordProperties {
pub is_dictionary_word: bool,
pub length: usize,
pub average_character_width: f32,
pub average_character_height: f32,
pub font_size: f32,
pub direction: TextDirection,
pub is_bold: bool,
pub is_italic: bool,
pub is_monospace: bool,
}
impl Default for WordProperties {
fn default() -> Self {
Self {
is_dictionary_word: false,
length: 0,
average_character_width: 0.0,
average_character_height: 0.0,
font_size: 0.0,
direction: TextDirection::LeftToRight,
is_bold: false,
is_italic: false,
is_monospace: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineProperties {
pub height: u32,
pub baseline: u32,
pub line_spacing: f32,
pub alignment: TextAlignment,
pub reading_order: ReadingOrder,
pub is_vertical: bool,
}
impl Default for LineProperties {
fn default() -> Self {
Self {
height: 0,
baseline: 0,
line_spacing: 0.0,
alignment: TextAlignment::Left,
reading_order: ReadingOrder::TopToBottom,
is_vertical: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextDirection {
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextAlignment {
Left,
Right,
Center,
Justified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadingOrder {
TopToBottom,
LeftToRight,
RightToLeft,
BottomToTop,
}
use crate::core::geometry::{ICoord, TBox};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WordFlag {
Segmented, Italic, Bold, Bol, Eol, StartOfLine, EndOfLine, Normalized, ScriptHasXHeight, ScriptIsLatin, DontChop, RepChar, FuzzySp, FuzzyNon, Inverse, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DisplayFlag {
Box, Text, Polygonal, EdgeStep, BnPolygonal, Blamer, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BlobChoiceClassifier {
StaticClassifier, AdaptedClassifier, SpeckleClassifier, Ambiguity, Fake, }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlobChoice {
pub unichar_id: u32,
pub rating: f32,
pub certainty: f32,
pub fontinfo_id: i16,
pub fontinfo_id2: i16,
pub script_id: i16,
pub min_xheight: f32,
pub max_xheight: f32,
pub yshift: f32,
pub classifier: BlobChoiceClassifier,
}
impl BlobChoice {
pub fn new(
unichar_id: u32,
rating: f32,
certainty: f32,
script_id: i16,
min_xheight: f32,
max_xheight: f32,
yshift: f32,
classifier: BlobChoiceClassifier,
) -> Self {
Self {
unichar_id,
rating,
certainty,
fontinfo_id: -1,
fontinfo_id2: -1,
script_id,
min_xheight,
max_xheight,
yshift,
classifier,
}
}
pub fn default() -> Self {
Self {
unichar_id: 32, rating: 10.0,
certainty: -1.0,
fontinfo_id: -1,
fontinfo_id2: -1,
script_id: -1,
min_xheight: 0.0,
max_xheight: 0.0,
yshift: 0.0,
classifier: BlobChoiceClassifier::Fake,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WordChoice {
pub blob_choices: Vec<Vec<BlobChoice>>,
pub choices: Vec<Vec<BlobChoice>>,
pub rating: f32,
pub certainty: f32,
pub blanks: u8,
pub script_id: i16,
pub language_model_state: Option<String>,
}
impl WordChoice {
pub fn new() -> Self {
Self {
blob_choices: Vec::new(),
choices: Vec::new(),
rating: 0.0,
certainty: 0.0,
blanks: 0,
script_id: -1,
language_model_state: None,
}
}
pub fn add_choice(&mut self, choices: Vec<BlobChoice>) {
self.blob_choices.push(choices);
}
pub fn best_choice(&self, position: usize) -> Option<&BlobChoice> {
self.blob_choices.get(position)?.first()
}
pub fn text(&self) -> String {
self.blob_choices
.iter()
.filter_map(|choices| choices.first())
.map(|choice| char::from_u32(choice.unichar_id).unwrap_or('?'))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Word {
pub blanks: u8,
pub flags: std::collections::HashSet<WordFlag>,
pub display_flags: std::collections::HashSet<DisplayFlag>,
pub script_id: i16,
pub correct_text: String,
pub choices: Vec<WordChoice>,
pub bounding_box: TBox,
pub characters: Vec<CharacterResult>,
}
impl Word {
pub fn new() -> Self {
Self {
blanks: 0,
flags: std::collections::HashSet::new(),
display_flags: std::collections::HashSet::new(),
script_id: 0,
correct_text: String::new(),
choices: Vec::new(),
bounding_box: TBox::null(),
characters: Vec::new(),
}
}
pub fn has_flag(&self, flag: WordFlag) -> bool {
self.flags.contains(&flag)
}
pub fn set_flag(&mut self, flag: WordFlag, value: bool) {
if value {
self.flags.insert(flag);
} else {
self.flags.remove(&flag);
}
}
pub fn has_display_flag(&self, flag: DisplayFlag) -> bool {
self.display_flags.contains(&flag)
}
pub fn set_display_flag(&mut self, flag: DisplayFlag, value: bool) {
if value {
self.display_flags.insert(flag);
} else {
self.display_flags.remove(&flag);
}
}
pub fn best_choice(&self) -> Option<&WordChoice> {
self.choices.first()
}
pub fn text(&self) -> String {
self.best_choice()
.map(|choice| choice.text())
.unwrap_or_else(|| self.correct_text.clone())
}
pub fn move_by(&mut self, vec: ICoord) {
self.bounding_box.move_by(vec);
for character in &mut self.characters {
character.bounding_box = BoundingBox::new(
character.bounding_box.left + vec.x as u32,
character.bounding_box.top + vec.y as u32,
character.bounding_box.right + vec.x as u32,
character.bounding_box.bottom + vec.y as u32,
);
}
}
}
#[cfg(test)]
mod tesseract_tests {
use super::*;
#[test]
fn test_blob_choice_creation() {
let choice = BlobChoice::new(
65, 0.1,
0.9,
0, 10.0,
20.0,
0.0,
BlobChoiceClassifier::StaticClassifier,
);
assert_eq!(choice.unichar_id, 65);
assert_eq!(choice.rating, 0.1);
assert_eq!(choice.certainty, 0.9);
}
#[test]
fn test_word_choice_creation() {
let mut word_choice = WordChoice::new();
word_choice.add_choice(vec![BlobChoice::new(
65,
0.1,
0.9,
0,
10.0,
20.0,
0.0,
BlobChoiceClassifier::StaticClassifier,
)]);
word_choice.add_choice(vec![BlobChoice::new(
66,
0.2,
0.8,
0,
10.0,
20.0,
0.0,
BlobChoiceClassifier::StaticClassifier,
)]);
assert_eq!(word_choice.text(), "AB");
}
#[test]
fn test_word_creation() {
let mut word = Word::new();
word.set_flag(WordFlag::Bold, true);
word.set_flag(WordFlag::Italic, false);
word.correct_text = "Hello".to_string();
assert!(word.has_flag(WordFlag::Bold));
assert!(!word.has_flag(WordFlag::Italic));
assert_eq!(word.correct_text, "Hello");
}
}
impl TextResult {
pub fn to_plain_text(&self) -> String {
self.text.clone()
}
pub fn to_hocr(&self, image_width: u32, image_height: u32) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n");
html.push_str(
"<html xmlns='http://www.w3.org/1999/xhtml' \
xmlns:hocr='http://www.w3.org/1999/04/hocr' \
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' \
xsi:schemaLocation='http://www.w3.org/1999/04/hocr http://www.w3.org/2001/04/hocr/hocr.xsd'>\n",
);
html.push_str("<head><meta charset='UTF-8' />\n");
html.push_str(&format!("<title>OCR Output</title>\n</head>\n<body>\n",));
html.push_str(&format!(
"<div class='ocr_page' id='page_1' title='image' \
style='width:{}px;height:{}px'>\n",
image_width, image_height
));
for (line_idx, line) in self.lines.iter().enumerate() {
html.push_str(&format!(
" <span class='ocr_line' id='line_{}' \
title='line {}' \
confidence='{}'>\n",
line_idx + 1,
line_idx + 1,
format_confidence(line.confidence),
));
for (word_idx, word) in line.words.iter().enumerate() {
let bbox = &word.bounding_box;
let title = format!(
"word {}; x {}; y {}; w {}; h {}; \
confidence {}",
word_idx,
bbox.left,
bbox.top,
bbox.width(),
bbox.height(),
format_confidence(word.confidence),
);
html.push_str(&format!(
" <span class='ocrx_word' id='word_{}_{}' \
title='{}'>{}</span>\n",
line_idx + 1,
word_idx + 1,
title,
word.text,
));
}
html.push_str(" </span>\n");
}
html.push_str("</div>\n</body>\n</html>");
html
}
pub fn to_json(&self) -> String {
#[derive(Serialize)]
struct JsonOutput<'a> {
text: &'a str,
confidence: f32,
language: &'a Option<String>,
num_lines: usize,
num_words: usize,
num_characters: usize,
lines: Vec<JsonLine<'a>>,
}
#[derive(Serialize)]
struct JsonLine<'a> {
text: &'a str,
confidence: f32,
bounding_box: Option<JsonBbox>,
words: Vec<JsonWord<'a>>,
}
#[derive(Serialize)]
struct JsonWord<'a> {
text: &'a str,
confidence: f32,
bounding_box: Option<JsonBbox>,
characters: Vec<JsonChar>,
}
#[derive(Serialize)]
struct JsonChar {
character: char,
confidence: f32,
bounding_box: Option<JsonBbox>,
}
#[derive(Serialize)]
struct JsonBbox {
left: u32,
top: u32,
right: u32,
bottom: u32,
}
impl<'a> From<&'a crate::core::text::BoundingBox> for JsonBbox {
fn from(bbox: &'a crate::core::text::BoundingBox) -> Self {
JsonBbox {
left: bbox.left,
top: bbox.top,
right: bbox.right,
bottom: bbox.bottom,
}
}
}
let json_output = JsonOutput {
text: &self.text,
confidence: self.confidence,
language: &self.language,
num_lines: self.lines.len(),
num_words: self.words.len(),
num_characters: self.characters.len(),
lines: self
.lines
.iter()
.map(|line| JsonLine {
text: &line.text,
confidence: line.confidence,
bounding_box: Some((&line.bounding_box).into()),
words: line
.words
.iter()
.map(|word| JsonWord {
text: &word.text,
confidence: word.confidence,
bounding_box: Some((&word.bounding_box).into()),
characters: word
.characters
.iter()
.map(|c| JsonChar {
character: c.character,
confidence: c.confidence,
bounding_box: Some((&c.bounding_box).into()),
})
.collect(),
})
.collect(),
})
.collect(),
};
serde_json::to_string_pretty(&json_output).unwrap_or_default()
}
}
fn format_confidence(confidence: f32) -> String {
format!("{:.2}", (confidence * 100.0).min(100.0))
}