use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{OcrError, Result};
const RGB_CHANNELS: usize = 3;
pub(crate) const QUAD_CORNERS: usize = 4;
#[derive(Debug, Clone, PartialEq)]
pub struct Image {
width: u32,
height: u32,
rgb8: Vec<u8>,
}
impl Image {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let decoded = image::load_from_memory(bytes).map_err(|error| OcrError::Image {
message: "failed to decode image bytes".to_string(),
source: Some(Box::new(error)),
})?;
let rgb = decoded.to_rgb8();
let (width, height) = rgb.dimensions();
Ok(Self {
width,
height,
rgb8: rgb.into_raw(),
})
}
pub fn from_rgb8(width: u32, height: u32, rgb8: Vec<u8>) -> Result<Self> {
let expected = (width as usize)
.checked_mul(height as usize)
.and_then(|pixels| pixels.checked_mul(RGB_CHANNELS));
match expected {
Some(expected) if expected == rgb8.len() => Ok(Self { width, height, rgb8 }),
_ => Err(OcrError::image(format!(
"RGB8 buffer length {} does not match width {} * height {} * {} channels",
rgb8.len(),
width,
height,
RGB_CHANNELS
))),
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn as_rgb8(&self) -> &[u8] {
&self.rgb8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct BBox {
pub x_min: f32,
pub y_min: f32,
pub x_max: f32,
pub y_max: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Quad {
pub points: [Point; 4],
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextLine {
pub quad: Quad,
pub text: String,
pub confidence: f32,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct OcrResult {
pub lines: Vec<TextLine>,
}
#[cfg(test)]
mod format_tests {
use super::Image;
use image::{DynamicImage, ImageFormat, RgbImage};
fn assert_round_trips(format: ImageFormat) {
let pixels: Vec<u8> = (0..3 * 2 * 3).map(|value| value as u8).collect();
let rgb = RgbImage::from_raw(3, 2, pixels).expect("3x2 rgb buffer");
let mut encoded = std::io::Cursor::new(Vec::new());
DynamicImage::ImageRgb8(rgb)
.write_to(&mut encoded, format)
.unwrap_or_else(|error| panic!("encoding {format:?}: {error}"));
let decoded = Image::from_bytes(&encoded.into_inner())
.unwrap_or_else(|error| panic!("decoding {format:?} back through Image::from_bytes: {error}"));
assert_eq!(
(decoded.width(), decoded.height()),
(3, 2),
"{format:?} decodes to the original size"
);
}
#[test]
fn should_decode_newly_enabled_raster_formats() {
assert_round_trips(ImageFormat::Bmp);
assert_round_trips(ImageFormat::Tiff);
assert_round_trips(ImageFormat::Gif);
assert_round_trips(ImageFormat::Pnm);
}
}