Skip to main content

jellyfin_rpc/external/
image_utils.rs

1use image::{DynamicImage, GenericImageView, ImageFormat, imageops};
2use std::io::{Cursor};
3
4#[derive(Debug, Clone)]
5pub struct ImageProcessingOptions {
6    pub size: Option<u32>,
7    pub background: bool,
8    pub background_blur: f32,
9    pub corner_radius: Option<f32>,
10}
11
12impl Default for ImageProcessingOptions {
13    fn default() -> Self {
14        Self {
15            size: None,
16            background: true,
17            background_blur: 3.0,
18            corner_radius: Some(4.0),
19        }
20    }
21}
22
23pub fn make_square_with_blur(input_bytes: &[u8], options: &ImageProcessingOptions) -> Result<Vec<u8>, image::ImageError> {
24    let img = image::load_from_memory(input_bytes)?;
25    let (width, height) = img.dimensions();
26    let size = options.size.unwrap_or_else(|| width.max(height));
27
28    let fg_buf = if width > height {
29        let new_height = (size as f32 * (height as f32 / width as f32)) as u32;
30        imageops::resize(&img, size, new_height, imageops::FilterType::Lanczos3)
31    } else {
32        let new_width = (size as f32 * (width as f32 / height as f32)) as u32;
33        imageops::resize(&img, new_width, size, imageops::FilterType::Lanczos3)
34    };
35
36    let fg_dyn = DynamicImage::ImageRgba8(fg_buf);
37    let (fg_w, fg_h) = fg_dyn.dimensions();
38    let mut canvas = DynamicImage::new_rgba8(size, size);
39
40    if options.background {
41        let bg_buf = imageops::resize(&img, size, size, imageops::FilterType::Gaussian);
42        let mut bg_dyn = DynamicImage::ImageRgba8(bg_buf);
43        if options.background_blur > 0.0 {
44            let blur_radius = (size as f32) * (options.background_blur / 100.0);
45            bg_dyn = DynamicImage::ImageRgba8(imageops::blur(&bg_dyn, blur_radius));
46        }
47        imageops::overlay(&mut canvas, &bg_dyn, 0, 0);
48        imageops::overlay(&mut canvas, &fg_dyn, ((size - fg_w) / 2) as i64, ((size - fg_h) / 2) as i64);
49    } else {
50        let mut fg_rounded = fg_dyn;
51        
52        if let Some(radius_percent) = options.corner_radius {
53            let radius = ((size as f32) * (radius_percent / 100.0)) as u32;
54            if radius > 0 {
55                apply_rounded_corners(&mut fg_rounded, radius);
56            }
57        }
58
59        imageops::overlay(&mut canvas, &fg_rounded, ((size - fg_w) / 2) as i64, ((size - fg_h) / 2) as i64);
60    }
61
62    let mut buf = Vec::new();
63    canvas.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)?;
64    Ok(buf)
65}
66
67fn apply_rounded_corners(img: &mut DynamicImage, radius: u32) {
68    let (width, height) = img.dimensions();
69    let rgba = img.as_mut_rgba8().unwrap();
70    let radius_f = radius as f32;
71
72    for y in 0..height {
73        for x in 0..width {
74            let corner_center = if x < radius && y < radius {
75                Some((radius - 1, radius - 1))
76            } else if x >= width - radius && y < radius {
77                Some((width - radius, radius - 1))
78            } else if x < radius && y >= height - radius {
79                Some((radius - 1, height - radius))
80            } else if x >= width - radius && y >= height - radius {
81                Some((width - radius, height - radius))
82            } else {
83                None
84            };
85
86            if let Some((cx, cy)) = corner_center {
87                let dx = x as f32 - cx as f32;
88                let dy = y as f32 - cy as f32;
89                let distance = (dx * dx + dy * dy).sqrt();
90                
91                if distance > radius_f {
92                    let pixel = rgba.get_pixel_mut(x, y);
93                    pixel[3] = 0;
94                } else if distance > radius_f - 1.0 {
95                    let alpha_factor = radius_f - distance;
96                    let pixel = rgba.get_pixel_mut(x, y);
97                    pixel[3] = (pixel[3] as f32 * alpha_factor).round() as u8;
98                }
99            }
100        }
101    }
102}