use crate::common::errors::*;
use fast_image_resize as fr;
use image::DynamicImage;
#[cfg(test)]
use image::GrayImage;
pub struct ImagePipeline {
pub target_resolution: (u32, u32),
pub char_map: Vec<char>,
pub new_lines: bool,
lut: [char; 256],
}
impl ImagePipeline {
pub fn new(target_resolution: (u32, u32), char_map: Vec<char>, new_lines: bool) -> Self {
let lut = Self::build_lut(&char_map);
Self {
target_resolution,
char_map,
new_lines,
lut,
}
}
fn build_lut(char_map: &[char]) -> [char; 256] {
let mut lut = [' '; 256];
let len = char_map.len();
for i in 0..256 {
lut[i] = char_map[len * i / 256];
}
lut
}
pub fn rebuild_lut(&mut self) {
self.lut = Self::build_lut(&self.char_map);
}
pub fn set_target_resolution(&mut self, width: u32, height: u32) -> &mut Self {
self.target_resolution = (width, height);
self
}
pub fn resize(&self, img: &DynamicImage) -> Result<DynamicImage, MyError> {
let width = img.width();
let height = img.height();
let src_image = fr::images::Image::from_vec_u8(
width,
height,
img.to_rgb8().into_raw(),
fr::PixelType::U8x3,
)
.map_err(|err| MyError::Pipeline(format!("{ERROR_RESIZE}:{err:?}")))?;
let mut dst_image = fr::images::Image::new(
self.target_resolution.0,
self.target_resolution.1,
fr::PixelType::U8x3,
);
let mut resizer = fr::Resizer::new();
resizer
.resize(
&src_image,
&mut dst_image,
&fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
)
.map_err(|err| MyError::Pipeline(format!("{ERROR_RESIZE}:{err:?}")))?;
let dst_image = dst_image.into_vec();
let img_buff = image::ImageBuffer::<image::Rgb<u8>, _>::from_vec(
self.target_resolution.0,
self.target_resolution.1,
dst_image,
)
.ok_or(MyError::Pipeline(ERROR_DATA.to_string()))?;
Ok(DynamicImage::ImageRgb8(img_buff))
}
#[cfg(test)]
pub fn to_ascii(&self, input: &GrayImage) -> String {
let (width, height) = (input.width(), input.height());
let capacity = (width + 1) * height + 1;
let mut output = String::with_capacity(capacity as usize);
for y in 0..height {
output.extend((0..width).map(|x| {
let lum = input.get_pixel(x, y)[0] as usize;
self.lut[lum]
}));
if self.new_lines && y < height - 1 {
output.push('\r');
output.push('\n');
}
}
output
}
pub fn to_ascii_from_rgb(&self, rgb_data: &[u8], width: u32, height: u32) -> String {
let capacity = (width + 1) as usize * height as usize + 1;
let mut output = String::with_capacity(capacity);
for y in 0..height {
let row_start = (y * width * 3) as usize;
for x in 0..width {
let idx = row_start + (x as usize) * 3;
let r = rgb_data[idx] as u32;
let g = rgb_data[idx + 1] as u32;
let b = rgb_data[idx + 2] as u32;
let lum = ((r * 2126 + g * 7152 + b * 722) / 10000) as usize;
output.push(self.lut[lum]);
}
if self.new_lines && y < height - 1 {
output.push('\r');
output.push('\n');
}
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::char_maps::CHARS1;
use image::{DynamicImage, ImageError};
use reqwest;
use std::io::Cursor;
const TEST_IMAGE_URL: &str = "https://sipi.usc.edu/database/preview/misc/4.1.01.png";
fn download_image(url: &str) -> Result<DynamicImage, ImageError> {
let response = reqwest::blocking::get(url)
.expect("Failed to download image")
.bytes()
.expect("Failed to get image bytes");
let image_data = Cursor::new(response);
image::load(image_data, image::ImageFormat::Png)
}
#[test]
fn test_new() {
let image = ImagePipeline::new((120, 80), vec!['a', 'b', 'c'], false);
assert_eq!(image.target_resolution, (120, 80));
assert_eq!(image.char_map, vec!['a', 'b', 'c']);
}
#[test]
fn test_process() {
let image = ImagePipeline::new((120, 80), vec!['a', 'b', 'c'], false);
let input = download_image(TEST_IMAGE_URL).expect("Failed to download image");
let output = image.resize(&input).expect("Failed to resize image");
assert_eq!(output.width(), 120);
assert_eq!(output.height(), 80);
}
#[test]
fn test_to_ascii_ext() {
let image = ImagePipeline::new((120, 80), CHARS1.chars().collect(), false);
let input = download_image(TEST_IMAGE_URL).expect("Failed to download image");
let output = image.to_ascii(
&image
.resize(&input)
.expect("Failed to resize image")
.into_luma8(),
);
assert_eq!(output.chars().count(), 120 * 80);
}
#[test]
fn test_to_ascii() {
let image = ImagePipeline::new((120, 80), vec!['a', 'b', 'c'], false);
let input = download_image(TEST_IMAGE_URL).expect("Failed to download image");
let output = image.to_ascii(
&image
.resize(&input)
.expect("Failed to resize image")
.into_luma8(),
);
assert_eq!(output.len(), 120 * 80);
}
}