moggu 0.1.1

A lightning-fast TUI image processing tool with 21+ professional filters in under 200ms
Documentation
use std::sync::{Arc};
use image::{imageops::blur, DynamicImage, ImageBuffer, Rgb};
use rayon::{iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}, slice::{ParallelSlice, ParallelSliceMut}};
use crate::{ProgressSender, send_progress};

pub fn brightness(
    img: &DynamicImage,
    value: i32,
    progress_tx: Option<ProgressSender>,
) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    let rgb_img = img.to_rgb8();
    let (width, height) = rgb_img.dimensions();
    let mut out_buffer = ImageBuffer::new(width, height);


    send_progress(&progress_tx, 0.0);
    let progress_tx = Arc::new(progress_tx);
    let in_pixels = rgb_img.as_raw();
    let out_pixels = out_buffer.as_mut();

    in_pixels
        .par_chunks_exact(3)
        .zip(out_pixels.par_chunks_exact_mut(3))
        .for_each(|(in_pixel, out_pixel)| {

            out_pixel[0] = (in_pixel[0] as i32 + value).clamp(0, 255) as u8;
            out_pixel[1] = (in_pixel[1] as i32 + value).clamp(0, 255) as u8;
            out_pixel[2] = (in_pixel[2] as i32 + value).clamp(0, 255) as u8;
        });

    send_progress(&progress_tx, 1.0);
    out_buffer
}



pub fn contrast(img: &DynamicImage, factor: f32, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    let rgb_img = img.to_rgb8();
    let (width, height) = rgb_img.dimensions();
    let mut out_buffer = ImageBuffer::new(width, height);
    
    send_progress(&progress_tx, 0.0);

    let progress_tx = Arc::new(progress_tx);


    let in_pixels = rgb_img.as_raw();
    let out_pixels = out_buffer.as_mut();

    in_pixels
        .par_chunks_exact(3)
        .zip(out_pixels.par_chunks_exact_mut(3))
        .for_each(|(in_pixel, out_pixel)| {
            out_pixel[0] = (factor * (in_pixel[0] as f32 - 128.0) + 128.0).clamp(0.0, 255.0) as u8;
            out_pixel[1] = (factor * (in_pixel[1] as f32 - 128.0) + 128.0).clamp(0.0, 255.0) as u8;
            out_pixel[2] = (factor * (in_pixel[2] as f32 - 128.0) + 128.0).clamp(0.0, 255.0) as u8;
        });

    send_progress(&progress_tx, 1.0);
    out_buffer
}



pub fn box_blur(img: &DynamicImage, radius: u32, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    
    let rgb_img = img.to_rgb8();

    let (width, height) = rgb_img.dimensions();

    send_progress(&progress_tx, 0.0);

    let horizontal_blurred = horizontal_box_blur(&rgb_img, radius, width, height);

    send_progress(&progress_tx, 0.5);

    let final_result = vertical_box_blur(&horizontal_blurred, radius, width, height);

    send_progress(&progress_tx, 1.0);

    ImageBuffer::from_vec(width, height, final_result).unwrap()
}



fn horizontal_box_blur(img: &ImageBuffer<Rgb<u8>, Vec<u8>>, radius: u32, width: u32, height: u32) -> Vec<u8> {
    let mut result = vec![0u8; (width * height * 3) as usize];
    let pixels = img.as_raw();

    result.par_chunks_exact_mut((width * 3) as usize).enumerate().for_each(|(y, row)| {
        let row_start = (y as u32 * width * 3) as usize;

        let mut sum_red = 0u32;
        let mut sum_green = 0u32;
        let mut sum_blue = 0u32;
        let mut window_size = 0u32;

        let x_min = 0u32.saturating_sub(radius);
        let x_max = (0 + radius).min(width - 1);

        for kx in x_min..=x_max {
            let idx = row_start + (kx * 3) as usize;
            sum_red += pixels[idx] as u32;
            sum_green += pixels[idx + 1] as u32;
            sum_blue += pixels[idx + 2] as u32;
            window_size += 1;
        }

        let out_idx = 0;
        row[out_idx] = (sum_red / window_size) as u8;
        row[out_idx + 1] = (sum_green / window_size) as u8;
        row[out_idx + 2] = (sum_blue / window_size) as u8;

        for x in 1..width {
            let new_x_min = x.saturating_sub(radius);
            let new_x_max = (x + radius).min(width - 1);
            let prev_x_min = (x - 1).saturating_sub(radius);
            let prev_x_max = ((x - 1) + radius).min(width - 1);

            if new_x_min > prev_x_min {
                let remove_idx = row_start + (prev_x_min * 3) as usize;
                sum_red -= pixels[remove_idx] as u32;
                sum_green -= pixels[remove_idx + 1] as u32;
                sum_blue -= pixels[remove_idx + 2] as u32;
                window_size -= 1;
            }

            if new_x_max > prev_x_max {
                let add_idx = row_start + (new_x_max * 3) as usize;
                sum_red += pixels[add_idx] as u32;
                sum_green += pixels[add_idx + 1] as u32;
                sum_blue += pixels[add_idx + 2] as u32;
                window_size += 1;
            }

            let out_idx = (x * 3) as usize;
            row[out_idx] = (sum_red / window_size) as u8;
            row[out_idx + 1] = (sum_green / window_size) as u8;
            row[out_idx + 2] = (sum_blue / window_size) as u8;
        }
    });

    result   
}



fn vertical_box_blur(pixels: &[u8], radius: u32, width: u32, height: u32) -> Vec<u8> {
    let result = vec![0u8; (width * height * 3) as usize];

    (0..width).into_par_iter().for_each(|x| {
        let mut sum_red = 0u32;
        let mut sum_green = 0u32;
        let mut sum_blue = 0u32;
        let mut window_size = 0u32;

        let y_min = 0u32.saturating_sub(radius);
        let y_max = (0 + radius).min(height - 1);

        for ky in y_min..=y_max {
            let idx = ((ky * width + x) * 3) as usize;
            sum_red += pixels[idx] as u32;
            sum_green += pixels[idx + 1] as u32;
            sum_blue += pixels[idx + 2] as u32;
            window_size += 1;
        }
        let out_idx = (x * 3) as usize;
        unsafe {
            let result_ptr = result.as_ptr() as *mut u8;
            *result_ptr.add(out_idx) = (sum_red / window_size) as u8;
            *result_ptr.add(out_idx + 1) = (sum_green / window_size) as u8;
            *result_ptr.add(out_idx + 2) = (sum_blue / window_size) as u8;
        }
        for y in 1..height {
            let new_y_min = y.saturating_sub(radius);
            let new_y_max = (y + radius).min(height - 1);
            let prev_y_min = (y - 1).saturating_sub(radius);
            let prev_y_max = ((y - 1) + radius).min(height - 1);

            if new_y_min > prev_y_min {
                let remove_idx = ((prev_y_min * width + x) * 3) as usize;
                sum_red -= pixels[remove_idx] as u32;
                sum_green -= pixels[remove_idx + 1] as u32;
                sum_blue -= pixels[remove_idx + 2] as u32;
                window_size -= 1;
            }

            if new_y_max > prev_y_max {
                let add_idx = ((new_y_max * width + x) * 3) as usize;
                sum_red += pixels[add_idx] as u32;
                sum_green += pixels[add_idx + 1] as u32;
                sum_blue += pixels[add_idx + 2] as u32;
                window_size += 1;
            }

            let out_idx = ((y * width + x) * 3) as usize;
            unsafe {
                let result_ptr = result.as_ptr() as *mut u8;
            *result_ptr.add(out_idx) = (sum_red / window_size) as u8;
            *result_ptr.add(out_idx + 1) = (sum_green / window_size) as u8;
            *result_ptr.add(out_idx + 2) = (sum_blue / window_size) as u8;
            }
        }
    });

    result

}



pub fn gaussian_blur(img: &DynamicImage, sigma: f32, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    send_progress(&progress_tx, 0.0);
    
    let blurred = blur(img, sigma);
    let result = DynamicImage::ImageRgba8(blurred).to_rgb8();
    
    send_progress(&progress_tx, 1.0);
    result
}



pub fn sharpen(img: &DynamicImage, strenght: f32, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    
    let rgb_img = img.to_rgb8();

    let (width, height) = rgb_img.dimensions();
    let mut out_buffer = ImageBuffer::new(width, height);

    send_progress(&progress_tx, 0.0);

    let in_pixels = rgb_img.as_raw();
    
    out_buffer.as_mut().par_chunks_exact_mut((width * 3) as usize)
        .enumerate()
        .for_each(|(y, out_row)| {
            if y == 0 || y == height as usize - 1 {
                let src_start = (y * width as usize * 3) as usize;
                out_row.copy_from_slice(&in_pixels[src_start..src_start + (width * 3) as usize]);
                return;
            }
            
            let row_stride = (width * 3) as usize;
            
            for x in 0..width {
                if x == 0 || x == width - 1 {
                
        
                    let dst_idx = (x * 3) as usize;
                    out_row[dst_idx] = in_pixels[(y * width as usize + x as usize) * 3];
                    out_row[dst_idx + 1] = in_pixels[(y * width as usize + x as usize) * 3 + 1];
                    out_row[dst_idx + 2] = in_pixels[(y * width as usize + x as usize) * 3 + 2];
                    continue;
                }
                
                let center_idx = ((y * width as usize + x as usize) * 3) as usize;

                let center_red = in_pixels[center_idx] as f32;
                let center_green = in_pixels[center_idx + 1] as f32;
                let center_blue = in_pixels[center_idx + 2] as f32;

                let top_red = in_pixels[center_idx - row_stride] as f32;
                let top_green = in_pixels[center_idx - row_stride + 1] as f32;
                let top_blue = in_pixels[center_idx - row_stride + 2] as f32;

                let left_red = in_pixels[center_idx - 3] as f32;
                let left_green = in_pixels[center_idx - 2] as f32;
                let left_blue = in_pixels[center_idx - 1] as f32;

                let right_red = in_pixels[center_idx + 3] as f32;
                let right_green = in_pixels[center_idx + 4] as f32;
                let right_blue = in_pixels[center_idx + 5] as f32;

                let bottom_red = in_pixels[center_idx + row_stride] as f32;
                let bottom_green = in_pixels[center_idx + row_stride + 1] as f32;
                let bottom_blue = in_pixels[center_idx + row_stride + 2] as f32;

                let sharpened_red = center_red * 5.0 - (top_red + left_red + right_red + bottom_red);
                let sharpened_green = center_green * 5.0 - (top_green + left_green + right_green + bottom_green);
                let sharpened_blue = center_blue * 5.0 - (top_blue + left_blue + right_blue + bottom_blue);

                let final_red = center_red + strenght * (sharpened_red - center_red);
                let final_green = center_green + strenght * (sharpened_green - center_green);
                let final_blue = center_blue + strenght * (sharpened_blue - center_blue);

                let out_idx = (x * 3) as usize;
                out_row[out_idx] = final_red.clamp(0.0, 255.0) as u8;
                out_row[out_idx + 1] = final_green.clamp(0.0, 255.0) as u8;
                out_row[out_idx + 2] = final_blue.clamp(0.0, 255.0) as u8;
            }
        });

    send_progress(&progress_tx, 1.0);
    out_buffer
}



pub fn edge_detection(img: &DynamicImage, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    let rgb_img = img.to_rgb8();
    let (width, height) = rgb_img.dimensions();
    let mut out_buffer = ImageBuffer::new(width, height);

    send_progress(&progress_tx, 0.0);

    let in_pixels = rgb_img.as_raw();

    out_buffer.as_mut().par_chunks_exact_mut((width * 3) as usize)
        .enumerate()
        .for_each(|(y, out_row)| {
            let row_stride = (width * 3) as usize;

            if y == 0 || y >= height as usize - 1 {
                out_row.fill(0);
                return;
            }

            for x in 0..width {
                if x == 0 || x >= width - 1 {
                    let out_idx = (x * 3) as usize;
                    out_row[out_idx] = 0;
                    out_row[out_idx + 1] = 0;
                    out_row[out_idx + 2] = 0;
                    continue;
                }

                let center_idx = (y * width as usize + x as usize) * 3;
                let get_gray_fast = |idx: usize| -> f32 {
                    0.299 * in_pixels[idx] as f32 + 
                    0.587 * in_pixels[idx + 1] as f32 + 
                    0.114 * in_pixels[idx + 2] as f32
                };

                let gx = -get_gray_fast(center_idx - row_stride - 3) + 
                          get_gray_fast(center_idx - row_stride + 3) -
                         2.0 * get_gray_fast(center_idx - 3) +
                         2.0 * get_gray_fast(center_idx + 3) - 
                          get_gray_fast(center_idx + row_stride - 3) +
                          get_gray_fast(center_idx + row_stride + 3);

                let gy = -get_gray_fast(center_idx - row_stride - 3) -    
                         2.0 * get_gray_fast(center_idx - row_stride) -   
                          get_gray_fast(center_idx - row_stride + 3) +    
                          get_gray_fast(center_idx + row_stride - 3) +    
                         2.0 * get_gray_fast(center_idx + row_stride) +   
                          get_gray_fast(center_idx + row_stride + 3);    

                let magnitude = (gx * gx + gy * gy).sqrt().clamp(0.0, 255.0) as u8;

                let out_idx = (x * 3) as usize;
                out_row[out_idx] = magnitude;
                out_row[out_idx + 1] = magnitude;
                out_row[out_idx + 2] = magnitude;
            }
        });

    send_progress(&progress_tx, 1.0);
    out_buffer
}



pub fn thresholding(img: &DynamicImage, threshold: u8, progress_tx: Option<ProgressSender>) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
    let rgb_img = img.to_rgb8();
    let (width, height) = rgb_img.dimensions();

    let mut out_buffer = ImageBuffer::new(width, height);

    send_progress(&progress_tx, 0.0);

    let in_pixels = rgb_img.as_raw();
    let out_pixels = out_buffer.as_mut();

    in_pixels
        .par_chunks_exact(3)
        .zip(out_pixels.par_chunks_exact_mut(3))
        .for_each(|(in_pixel, out_pixel)| {
            let gray = (0.299 * in_pixel[0] as f32 + 
                            0.587 * in_pixel[1] as f32 + 
                            0.114 * in_pixel[2] as f32) as u8;

            let binary_value = if gray > threshold {255} else {0};

            out_pixel[0] = binary_value;
            out_pixel[1] = binary_value;
            out_pixel[2] = binary_value;
        });

    send_progress(&progress_tx, 1.0);
    out_buffer
}