use image::{DynamicImage, ImageBuffer, Luma, GenericImageView, Pixel};
use base64::{Engine as _, engine::general_purpose};
use exif::{In, Reader, Tag};
use std::io::Cursor;
pub struct ImageProcessor;
impl ImageProcessor {
const THRESHOLD: u8 = 127;
fn apply_exif_orientation(img: DynamicImage, image_bytes: &[u8]) -> DynamicImage {
let mut cursor = Cursor::new(image_bytes);
let orientation = Reader::new()
.read_from_container(&mut cursor)
.ok()
.and_then(|exif| exif.get_field(Tag::Orientation, In::PRIMARY).cloned())
.and_then(|field| field.value.get_uint(0));
match orientation.unwrap_or(1) {
2 => img.fliph(),
3 => img.rotate180(),
4 => img.flipv(),
5 => img.rotate90().fliph(),
6 => img.rotate90(),
7 => img.rotate270().fliph(),
8 => img.rotate270(),
_ => img,
}
}
pub fn base64_to_image(base64_string: &str) -> Result<DynamicImage, String> {
let image_data = if base64_string.contains(',') {
base64_string.split(',').nth(1).unwrap_or(base64_string)
} else {
base64_string
};
let image_bytes = general_purpose::STANDARD
.decode(image_data)
.map_err(|e| format!("Error decoding base64: {}", e))?;
image::load_from_memory(&image_bytes)
.map(|img| Self::apply_exif_orientation(img, &image_bytes))
.map_err(|e| format!("Error loading image: {}", e))
}
pub fn resize_image(img: &DynamicImage, max_width: u32) -> DynamicImage {
let (width, height) = img.dimensions();
if width <= max_width {
return img.clone();
}
let new_width = max_width;
let new_height = ((height as f64) * (max_width as f64) / (width as f64)) as u32;
if img.color().has_alpha() {
let mut rgba_img = img.to_rgba8();
for pixel in rgba_img.pixels_mut() {
let alpha = pixel[3];
if alpha < 255 {
let alpha_f = alpha as f32 / 255.0;
pixel[0] = ((pixel[0] as f32 * alpha_f) + (255.0 * (1.0 - alpha_f))) as u8;
pixel[1] = ((pixel[1] as f32 * alpha_f) + (255.0 * (1.0 - alpha_f))) as u8;
pixel[2] = ((pixel[2] as f32 * alpha_f) + (255.0 * (1.0 - alpha_f))) as u8;
pixel[3] = 255;
}
}
let img_no_alpha = DynamicImage::ImageRgba8(rgba_img);
img_no_alpha.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
} else {
img.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
}
}
pub fn to_grayscale(img: &DynamicImage) -> ImageBuffer<Luma<u8>, Vec<u8>> {
let (width, height) = img.dimensions();
let mut grayscale = ImageBuffer::new(width, height);
if img.color().has_alpha() {
for y in 0..height {
for x in 0..width {
let pixel = img.get_pixel(x, y);
let channels = pixel.channels();
let (r, g, b, a) = if channels.len() >= 4 {
(channels[0], channels[1], channels[2], channels[3])
} else {
(channels[0], channels[1], channels[2], 255)
};
if a < 128 {
grayscale.put_pixel(x, y, Luma([255u8]));
} else {
let gray = (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) as u8;
grayscale.put_pixel(x, y, Luma([gray]));
}
}
}
} else {
return img.to_luma8();
}
grayscale
}
pub fn to_binary_with_dithering(grayscale: &ImageBuffer<Luma<u8>, Vec<u8>>)
-> ImageBuffer<Luma<u8>, Vec<u8>> {
let (width, height) = grayscale.dimensions();
let mut pixels: Vec<Vec<i32>> = vec![vec![0; width as usize]; height as usize];
for y in 0..height {
for x in 0..width {
let pixel = grayscale.get_pixel(x, y)[0];
pixels[y as usize][x as usize] = pixel as i32;
}
}
let mut binary = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let old_pixel = pixels[y as usize][x as usize];
let new_pixel = if old_pixel < Self::THRESHOLD as i32 { 0 } else { 255 };
binary.put_pixel(x, y, Luma([new_pixel as u8]));
let error = old_pixel - new_pixel;
if x + 1 < width {
pixels[y as usize][(x + 1) as usize] += error * 7 / 16;
}
if y + 1 < height {
if x > 0 {
pixels[(y + 1) as usize][(x - 1) as usize] += error * 3 / 16;
}
pixels[(y + 1) as usize][x as usize] += error * 5 / 16;
if x + 1 < width {
pixels[(y + 1) as usize][(x + 1) as usize] += error / 16;
}
}
}
}
binary
}
pub fn to_binary_simple(grayscale: &ImageBuffer<Luma<u8>, Vec<u8>>)
-> ImageBuffer<Luma<u8>, Vec<u8>> {
let (width, height) = grayscale.dimensions();
let mut binary = ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let pixel = grayscale.get_pixel(x, y)[0];
let binary_value = if pixel < Self::THRESHOLD { 0 } else { 255 };
binary.put_pixel(x, y, Luma([binary_value]));
}
}
binary
}
pub fn image_to_bytes(binary: &ImageBuffer<Luma<u8>, Vec<u8>>) -> Vec<u8> {
let (width, height) = binary.dimensions();
let byte_width = ((width + 7) / 8) as usize;
let mut image_data = vec![0u8; byte_width * height as usize];
for y in 0..height {
for x in 0..width {
let pixel = binary.get_pixel(x, y)[0];
let is_black = pixel < Self::THRESHOLD;
if is_black {
let byte_index = (y as usize) * byte_width + (x as usize / 8);
let bit_position = 7 - (x % 8);
image_data[byte_index] |= 1 << bit_position;
}
}
}
image_data
}
pub fn process_image(base64_image: &str, max_width: u32, use_dithering: bool)
-> Result<ImageBuffer<Luma<u8>, Vec<u8>>, String> {
let original = Self::base64_to_image(base64_image)?;
let resized = Self::resize_image(&original, max_width);
let grayscale = Self::to_grayscale(&resized);
let binary = if use_dithering {
Self::to_binary_with_dithering(&grayscale)
} else {
Self::to_binary_simple(&grayscale)
};
Ok(binary)
}
}