cbg_core/
imageops.rs

1use crate::Error;
2use image::{DynamicImage, ImageFormat};
3use std::io::Cursor;
4
5#[derive(Debug, Clone, Copy)]
6#[cfg_attr(feature = "discord", derive(poise::ChoiceParameter))]
7pub enum ImageOrientation {
8    #[cfg_attr(feature = "discord", name = "horizontally")]
9    Horizontal,
10    #[cfg_attr(feature = "discord", name = "vertically")]
11    Vertical,
12}
13
14#[derive(Debug, Default)]
15pub struct ImageOperations {
16    pub blur: Option<f32>,
17    pub orientation: Option<ImageOrientation>,
18    pub grayscale: bool,
19}
20
21impl ImageOperations {
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    pub fn is_identity(&self) -> bool {
27        self.blur.is_none() && self.orientation.is_none() && !self.grayscale
28    }
29
30    pub fn apply_to(&self, image: DynamicImage) -> Result<DynamicImage, Error> {
31        let mut img = image;
32
33        if let Some(orientation) = self.orientation {
34            match orientation {
35                ImageOrientation::Horizontal => img = img.fliph(),
36                ImageOrientation::Vertical => img = img.flipv(),
37            }
38        }
39
40        if let Some(blur_amount) = self.blur {
41            img = img.fast_blur(blur_amount);
42        }
43
44        if self.grayscale {
45            img = img.grayscale();
46        }
47
48        Ok(img)
49    }
50}
51
52#[derive(Debug)]
53pub struct ImageProcessor {
54    url: String,
55    operations: ImageOperations,
56}
57
58impl ImageProcessor {
59    pub fn new(url: String) -> Self {
60        Self {
61            url,
62            operations: ImageOperations::new(),
63        }
64    }
65
66    /// Blurs an image using imageops' fast_blur method
67    pub fn blur(mut self, blur: Option<f32>) -> Self {
68        if let Some(amount) = blur {
69            self.operations.blur = Some(amount);
70        }
71        self
72    }
73
74    /// Flips an image
75    pub fn flip(mut self, orientation: Option<ImageOrientation>) -> Self {
76        if let Some(orient) = orientation {
77            self.operations.orientation = Some(orient);
78        }
79        self
80    }
81
82    /// Self explainatory
83    pub fn grayscale(mut self, grayscale: Option<bool>) -> Self {
84        if grayscale.is_some_and(|g| g) {
85            self.operations.grayscale = true;
86        }
87        self
88    }
89
90    /// Call to process the image
91    /// Returns raw bytes (in `Vec<u8>`)  
92    /// Like this:  
93    /// ```
94    ///    let result = ImageProcessor::new(url)
95    ///      .blur(blur)
96    ///      .flip(orientation)
97    ///      .grayscale(grayscale)
98    ///      .process()
99    ///    .await?;
100    /// ```
101    pub async fn process(self) -> Result<Vec<u8>, Error> {
102        if self.operations.is_identity() {
103            return Err("no operations specified".into());
104        }
105
106        let img_bytes = reqwest::get(&self.url).await?.bytes().await?;
107
108        let processed_image = tokio::task::spawn_blocking(move || {
109            let image = image::load_from_memory(&img_bytes)?;
110            self.operations.apply_to(image)
111        })
112        .await??;
113
114        // Convert to PNG bytes
115        tokio::task::spawn_blocking(move || -> Result<Vec<u8>, Error> {
116                let mut bytes = Vec::new();
117                processed_image.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?;
118                Ok(bytes)
119            })
120            .await?
121    }
122}