gbserver/gb-image/src/processor.rs

204 lines
5.6 KiB
Rust
Raw Normal View History

2024-12-22 20:56:52 -03:00
use gb_core::{Result, Error};
use image::{
2024-12-23 00:54:50 -03:00
DynamicImage, Rgba,
2024-12-22 20:56:52 -03:00
};
use imageproc::{
2024-12-23 00:54:50 -03:00
drawing::draw_text_mut,
2024-12-22 20:56:52 -03:00
};
use rusttype::{Font, Scale};
use std::path::Path;
2024-12-23 00:54:50 -03:00
use tracing::instrument;
use std::convert::TryInto;
pub struct ProcessingOptions {
pub crop: Option<CropParams>,
pub watermark: Option<DynamicImage>,
pub x: i32,
pub y: i32,
}
pub struct CropParams {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
2024-12-22 20:56:52 -03:00
pub struct ImageProcessor {
default_font: Font<'static>,
}
impl ImageProcessor {
pub fn new() -> Result<Self> {
let font_data = include_bytes!("../assets/DejaVuSans.ttf");
let font = Font::try_from_bytes(font_data)
2024-12-23 00:54:50 -03:00
.ok_or_else(|| Error::internal("Failed to load font"))?;
2024-12-22 20:56:52 -03:00
Ok(Self {
default_font: font,
})
}
2024-12-23 00:54:50 -03:00
pub fn process_image(&self, mut image: DynamicImage, options: &ProcessingOptions) -> Result<DynamicImage> {
if let Some(crop) = &options.crop {
let cropped = image.crop_imm(
crop.x,
crop.y,
crop.width,
crop.height
);
image = cropped;
}
if let Some(watermark) = &options.watermark {
let x: i64 = options.x.try_into().map_err(|_| Error::internal("Invalid x coordinate"))?;
let y: i64 = options.y.try_into().map_err(|_| Error::internal("Invalid y coordinate"))?;
image::imageops::overlay(&mut image, watermark, x, y);
}
Ok(image)
}
2024-12-22 20:56:52 -03:00
#[instrument(skip(self, image_data))]
pub fn load_image(&self, image_data: &[u8]) -> Result<DynamicImage> {
image::load_from_memory(image_data)
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to load image: {}", e)))
2024-12-22 20:56:52 -03:00
}
#[instrument(skip(self, image))]
pub fn save_image(&self, image: &DynamicImage, path: &Path) -> Result<()> {
image.save(path)
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to save image: {}", e)))
2024-12-22 20:56:52 -03:00
}
#[instrument(skip(self, image))]
pub fn crop(&self, image: &DynamicImage, x: u32, y: u32, width: u32, height: u32) -> Result<DynamicImage> {
2024-12-23 00:54:50 -03:00
Ok(image.crop_imm(x, y, width, height))
2024-12-22 20:56:52 -03:00
}
#[instrument(skip(self, image))]
pub fn add_text(
&self,
image: &mut DynamicImage,
text: &str,
x: i32,
y: i32,
scale: f32,
color: Rgba<u8>,
) -> Result<()> {
let scale = Scale::uniform(scale);
let mut img = image.to_rgba8();
draw_text_mut(
&mut img,
color,
x,
y,
scale,
&self.default_font,
text,
);
*image = DynamicImage::ImageRgba8(img);
Ok(())
}
#[instrument(skip(self, image))]
pub fn add_watermark(
&self,
image: &mut DynamicImage,
watermark: &DynamicImage,
x: u32,
y: u32,
) -> Result<()> {
2024-12-23 00:54:50 -03:00
let x: i64 = x.try_into().map_err(|_| Error::internal("Invalid x coordinate"))?;
let y: i64 = y.try_into().map_err(|_| Error::internal("Invalid y coordinate"))?;
2024-12-22 20:56:52 -03:00
image::imageops::overlay(image, watermark, x, y);
Ok(())
}
#[instrument(skip(self, image))]
pub fn extract_text(&self, image: &DynamicImage) -> Result<String> {
use tesseract::Tesseract;
2024-12-23 00:54:50 -03:00
let temp_file = tempfile::NamedTempFile::new()
.map_err(|e| Error::internal(format!("Failed to create temp file: {}", e)))?;
2024-12-22 20:56:52 -03:00
image.save(&temp_file)
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to save temp image: {}", e)))?;
2024-12-22 20:56:52 -03:00
2024-12-23 00:54:50 -03:00
let mut api = Tesseract::new(None, Some("eng"))
.map_err(|e| Error::internal(format!("Failed to initialize Tesseract: {}", e)))?;
2024-12-24 10:09:14 -03:00
api.set_image(temp_file.path().to_str().unwrap())
2024-12-23 00:54:50 -03:00
.map_err(|e| Error::internal(format!("Failed to set image: {}", e)))?;
2024-12-22 20:56:52 -03:00
2024-12-23 00:54:50 -03:00
api.recognize()
.map_err(|e| Error::internal(format!("Failed to recognize text: {}", e)))?;
api.get_text()
.map_err(|e| Error::internal(format!("Failed to get text: {}", e)))
2024-12-22 20:56:52 -03:00
}
}
2024-12-23 00:54:50 -03:00
2024-12-22 20:56:52 -03:00
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
use std::path::PathBuf;
#[fixture]
fn processor() -> ImageProcessor {
ImageProcessor::new().unwrap()
}
#[fixture]
fn test_image() -> DynamicImage {
DynamicImage::new_rgb8(100, 100)
}
#[rstest]
fn test_resize(processor: ImageProcessor, test_image: DynamicImage) {
let resized = processor.resize(&test_image, 50, 50);
assert_eq!(resized.width(), 50);
assert_eq!(resized.height(), 50);
}
#[rstest]
fn test_crop(processor: ImageProcessor, test_image: DynamicImage) -> Result<()> {
let cropped = processor.crop(&test_image, 25, 25, 50, 50)?;
assert_eq!(cropped.width(), 50);
assert_eq!(cropped.height(), 50);
Ok(())
}
#[rstest]
fn test_add_text(processor: ImageProcessor, mut test_image: DynamicImage) -> Result<()> {
processor.add_text(
&mut test_image,
"Test",
10,
10,
12.0,
Rgba([255, 255, 255, 255]),
)?;
Ok(())
}
#[rstest]
fn test_extract_text(processor: ImageProcessor, mut test_image: DynamicImage) -> Result<()> {
processor.add_text(
&mut test_image,
"Test OCR",
10,
10,
24.0,
Rgba([0, 0, 0, 255]),
)?;
let text = processor.extract_text(&test_image)?;
assert!(text.contains("Test OCR"));
Ok(())
}
2024-12-23 00:54:50 -03:00
}