waterui_media/
image.rs

1use std::{io, path::Path, sync::Arc};
2
3use base64::{Engine, prelude::BASE64_STANDARD};
4use blocking::unblock;
5use image::{DynamicImage, GenericImageView, ImageFormat};
6use mime::Mime;
7use waterui_color::{Srgb, WithOpacity};
8
9/// Represents a loaded image.
10#[derive(Debug, Clone)]
11pub struct Image {
12    mime: Mime,
13    image: Arc<DynamicImage>,
14}
15
16impl Image {
17    /// Creates a new `Image` from raw image data.
18    ///
19    /// It will decode the image on a background thread, preventing UI blocking.
20    ///
21    /// # Panics
22    ///
23    /// Panics if the MIME type is not supported or if the image data cannot be decoded.
24    #[must_use]
25    pub async fn new(mime: Mime, data: Vec<u8>) -> Self {
26        let format =
27            ImageFormat::from_mime_type(mime.essence_str()).expect("Unsupported MIME type");
28
29        let image = unblock(move || {
30            image::load_from_memory_with_format(data.as_ref(), format)
31                .expect("Failed to decode image")
32        })
33        .await;
34
35        Self {
36            mime,
37            image: Arc::new(image),
38        }
39    }
40
41    /// Process the image with a closure on a background thread
42    pub async fn process<F>(&mut self, func: F)
43    where
44        F: FnOnce(Arc<DynamicImage>) -> DynamicImage + Send + 'static,
45    {
46        let image = self.image.clone();
47        self.image = unblock(move || Arc::new(func(image))).await;
48    }
49
50    /// Encodes the image to the specified MIME type.
51    ///
52    /// # Panics
53    ///
54    /// Panics if the MIME type is not supported or if encoding fails.
55    #[must_use]
56    #[allow(clippy::needless_pass_by_value)]
57    pub async fn encode(&self, mime: Mime) -> Vec<u8> {
58        let format =
59            ImageFormat::from_mime_type(mime.essence_str()).expect("Unsupported MIME type");
60        let image = self.image.clone();
61        unblock(move || {
62            let mut buf = std::io::Cursor::new(Vec::new());
63            image
64                .write_to(&mut buf, format)
65                .expect("Failed to encode image");
66            buf.into_inner()
67        })
68        .await
69    }
70
71    /// Encodes the image as PNG.
72    #[must_use]
73    pub async fn encode_png(&self) -> Vec<u8> {
74        self.encode(mime::IMAGE_PNG).await
75    }
76
77    /// Encodes the image as JPEG with the specified quality (currently unused).
78    #[must_use]
79    pub async fn encode_jpeg(&self, _quality: u8) -> Vec<u8> {
80        self.encode(mime::IMAGE_JPEG).await
81    }
82
83    /// Rotates the image by the specified angle in degrees (0, 90, 180, or 270).
84    ///
85    /// # Panics
86    ///
87    /// Panics if the angle is not a multiple of 90 degrees or is outside the range 0-359.
88    pub async fn rotate(&mut self, angle: u32) {
89        self.process(move |image| match angle % 360 {
90            0 => (*image).clone(),
91            90 => image.rotate90(),
92            180 => image.rotate180(),
93            270 => image.rotate270(),
94            _ => panic!("Unsupported rotation angle: {angle}"),
95        })
96        .await;
97    }
98
99    /// Get the width of the image in pixels
100    #[must_use]
101    pub fn width(&self) -> u32 {
102        self.image.width()
103    }
104
105    /// Get the height of the image in pixels
106    #[must_use]
107    pub fn height(&self) -> u32 {
108        self.image.height()
109    }
110
111    /// Get the dimensions (width, height) of the image
112    #[must_use]
113    pub fn dimensions(&self) -> (u32, u32) {
114        self.image.dimensions()
115    }
116
117    /// Resize the image to the specified dimensions
118    /// Uses Lanczos3 filter for high quality
119    pub async fn resize(&mut self, width: u32, height: u32) {
120        self.process(move |image| {
121            image.resize(width, height, image::imageops::FilterType::Lanczos3)
122        })
123        .await;
124    }
125
126    /// Resize the image to fit within the specified dimensions while maintaining aspect ratio
127    pub async fn resize_to_fit(&mut self, max_width: u32, max_height: u32) {
128        self.process(move |image| {
129            image.resize(max_width, max_height, image::imageops::FilterType::Lanczos3)
130        })
131        .await;
132    }
133
134    /// Resize the image to fill the specified dimensions while maintaining aspect ratio
135    pub async fn resize_to_fill(&mut self, width: u32, height: u32) {
136        self.process(move |image| {
137            image.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3)
138        })
139        .await;
140    }
141
142    /// Resize the image exactly to the specified dimensions (may distort aspect ratio)
143    pub async fn resize_exact(&mut self, width: u32, height: u32) {
144        self.process(move |image| {
145            image.resize_exact(width, height, image::imageops::FilterType::Lanczos3)
146        })
147        .await;
148    }
149
150    /// Flip the image horizontally
151    pub async fn flip_horizontal(&mut self) {
152        self.process(|image| image.fliph()).await;
153    }
154
155    /// Flip the image vertically
156    pub async fn flip_vertical(&mut self) {
157        self.process(|image| image.flipv()).await;
158    }
159
160    /// Crop the image to the specified rectangle
161    /// Returns true if successful, false if the rectangle is out of bounds
162    pub async fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> bool {
163        let img_width = self.width();
164        let img_height = self.height();
165
166        if x + width <= img_width && y + height <= img_height {
167            self.process(move |image| image.crop_imm(x, y, width, height))
168                .await;
169            true
170        } else {
171            false
172        }
173    }
174
175    /// Blur the image with the specified sigma value
176    pub async fn blur(&mut self, sigma: f32) {
177        self.process(move |image| image.blur(sigma)).await;
178    }
179
180    /// Adjust the brightness of the image
181    /// value: -100 to 100 (negative for darker, positive for brighter)
182    pub async fn brighten(&mut self, value: i32) {
183        self.process(move |image| image.brighten(value)).await;
184    }
185
186    /// Adjust the contrast of the image
187    /// contrast: floating point value (1.0 = no change, < 1.0 = less contrast, > 1.0 = more contrast)
188    pub async fn adjust_contrast(&mut self, contrast: f32) {
189        self.process(move |image| image.adjust_contrast(contrast))
190            .await;
191    }
192
193    /// Convert the image to grayscale
194    pub async fn grayscale(&mut self) {
195        self.process(|image| image.grayscale()).await;
196    }
197
198    /// Invert the colors of the image
199    pub async fn invert(&mut self) {
200        self.process(|image| {
201            let mut img = (*image).clone();
202            img.invert();
203            img
204        })
205        .await;
206    }
207
208    /// Apply an unsharpen mask to the image
209    pub async fn unsharpen(&mut self, sigma: f32, threshold: i32) {
210        self.process(move |image| image.unsharpen(sigma, threshold))
211            .await;
212    }
213
214    /// Get the color of a specific pixel
215    /// Returns None if the coordinates are out of bounds
216    #[must_use]
217    #[allow(clippy::many_single_char_names)]
218    pub fn get_pixel(&self, x: u32, y: u32) -> Option<WithOpacity<Srgb>> {
219        if x < self.width() && y < self.height() {
220            let [r, g, b, a] = self.image.get_pixel(x, y).0;
221            Some(Srgb::new_u8(r, g, b).with_opacity(f32::from(a) / 255.0))
222        } else {
223            None
224        }
225    }
226
227    /// Create a thumbnail of the image with the specified maximum dimension
228    pub async fn thumbnail(&mut self, max_size: u32) {
229        self.process(move |image| image.thumbnail(max_size, max_size))
230            .await;
231    }
232
233    /// Rotate the image 90 degrees clockwise
234    pub async fn rotate_90(&mut self) {
235        self.process(|image| image.rotate90()).await;
236    }
237
238    /// Rotate the image 180 degrees
239    pub async fn rotate_180(&mut self) {
240        self.process(|image| image.rotate180()).await;
241    }
242
243    /// Rotate the image 270 degrees clockwise (90 degrees counter-clockwise)
244    pub async fn rotate_270(&mut self) {
245        self.process(|image| image.rotate270()).await;
246    }
247
248    /// Apply a Gaussian blur with the specified sigma
249    pub async fn gaussian_blur(&mut self, sigma: f32) {
250        self.process(move |image| {
251            DynamicImage::ImageRgba8(image::imageops::blur(&image.to_rgba8(), sigma))
252        })
253        .await;
254    }
255
256    /// Hue rotate the image by the specified degrees
257    pub async fn huerotate(&mut self, degrees: i32) {
258        self.process(move |image| image.huerotate(degrees)).await;
259    }
260
261    /// Get the MIME type of the image
262    #[must_use]
263    pub const fn mime(&self) -> &Mime {
264        &self.mime
265    }
266
267    /// Write the image to a file with the specified format
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if the file cannot be written.
272    pub async fn write(&self, format: Mime, path: impl AsRef<Path>) -> io::Result<()> {
273        let path = path.as_ref().to_owned();
274        let data = self.encode(format).await;
275        unblock(move || std::fs::write(path, data)).await
276    }
277
278    /// Generate a base64-encoded data URL for the image encoded as PNG
279    ///
280    /// For large images (>10KB), encoding is done on a background thread to prevent UI blocking.
281    #[must_use]
282    pub async fn url(&self) -> String {
283        let data = self.encode_png().await;
284        // If the data is too large, encoded as base64 may block the UI thread, so do it in a background thread
285        if data.len() > 10 * 1024 {
286            // Move data to the background thread, and it will also be released on that thread, preventing UI blocking
287            unblock(move || {
288                let mut base64 = String::from("data:image/png;base64,");
289                BASE64_STANDARD.encode_string(data, &mut base64);
290                base64
291            })
292            .await
293        } else {
294            let data = BASE64_STANDARD.encode(data);
295            format!("data:image/png;base64,{data}")
296        }
297    }
298}