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#[derive(Debug, Clone)]
11pub struct Image {
12 mime: Mime,
13 image: Arc<DynamicImage>,
14}
15
16impl Image {
17 #[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 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 #[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 #[must_use]
73 pub async fn encode_png(&self) -> Vec<u8> {
74 self.encode(mime::IMAGE_PNG).await
75 }
76
77 #[must_use]
79 pub async fn encode_jpeg(&self, _quality: u8) -> Vec<u8> {
80 self.encode(mime::IMAGE_JPEG).await
81 }
82
83 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 #[must_use]
101 pub fn width(&self) -> u32 {
102 self.image.width()
103 }
104
105 #[must_use]
107 pub fn height(&self) -> u32 {
108 self.image.height()
109 }
110
111 #[must_use]
113 pub fn dimensions(&self) -> (u32, u32) {
114 self.image.dimensions()
115 }
116
117 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 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 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 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 pub async fn flip_horizontal(&mut self) {
152 self.process(|image| image.fliph()).await;
153 }
154
155 pub async fn flip_vertical(&mut self) {
157 self.process(|image| image.flipv()).await;
158 }
159
160 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 pub async fn blur(&mut self, sigma: f32) {
177 self.process(move |image| image.blur(sigma)).await;
178 }
179
180 pub async fn brighten(&mut self, value: i32) {
183 self.process(move |image| image.brighten(value)).await;
184 }
185
186 pub async fn adjust_contrast(&mut self, contrast: f32) {
189 self.process(move |image| image.adjust_contrast(contrast))
190 .await;
191 }
192
193 pub async fn grayscale(&mut self) {
195 self.process(|image| image.grayscale()).await;
196 }
197
198 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 pub async fn unsharpen(&mut self, sigma: f32, threshold: i32) {
210 self.process(move |image| image.unsharpen(sigma, threshold))
211 .await;
212 }
213
214 #[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 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 pub async fn rotate_90(&mut self) {
235 self.process(|image| image.rotate90()).await;
236 }
237
238 pub async fn rotate_180(&mut self) {
240 self.process(|image| image.rotate180()).await;
241 }
242
243 pub async fn rotate_270(&mut self) {
245 self.process(|image| image.rotate270()).await;
246 }
247
248 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 pub async fn huerotate(&mut self, degrees: i32) {
258 self.process(move |image| image.huerotate(degrees)).await;
259 }
260
261 #[must_use]
263 pub const fn mime(&self) -> &Mime {
264 &self.mime
265 }
266
267 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 #[must_use]
282 pub async fn url(&self) -> String {
283 let data = self.encode_png().await;
284 if data.len() > 10 * 1024 {
286 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}