msg_tool\utils/
img.rs

1//! Image Utilities
2use crate::types::*;
3use anyhow::Result;
4
5/// Reverses the alpha values of an image.
6///
7/// Only supports RGBA or BGRA images with 8-bit depth.
8pub fn reverse_alpha_values(data: &mut ImageData) -> Result<()> {
9    if data.color_type != ImageColorType::Rgba && data.color_type != ImageColorType::Bgra {
10        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
11    }
12    if data.depth != 8 {
13        return Err(anyhow::anyhow!(
14            "Alpha value reversal only supports 8-bit depth"
15        ));
16    }
17    for i in (0..data.data.len()).step_by(4) {
18        data.data[i + 3] = 255 - data.data[i + 3];
19    }
20    Ok(())
21}
22
23/// Converts a BGR image to BGRA format.
24///
25/// Only supports BGR images with 8-bit depth.
26pub fn convert_bgr_to_bgra(data: &mut ImageData) -> Result<()> {
27    if data.color_type != ImageColorType::Bgr {
28        return Err(anyhow::anyhow!("Image is not BGR"));
29    }
30    if data.depth != 8 {
31        return Err(anyhow::anyhow!(
32            "BGR to BGRA conversion only supports 8-bit depth"
33        ));
34    }
35    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
36    for chunk in data.data.chunks_exact(3) {
37        new_data.push(chunk[0]); // B
38        new_data.push(chunk[1]); // G
39        new_data.push(chunk[2]); // R
40        new_data.push(255); // A
41    }
42    data.data = new_data;
43    data.color_type = ImageColorType::Bgra;
44    Ok(())
45}
46
47/// Converts a BGR image to RGB format.
48///
49/// Only supports BGR images with 8-bit depth.
50pub fn convert_bgr_to_rgb(data: &mut ImageData) -> Result<()> {
51    if data.color_type != ImageColorType::Bgr {
52        return Err(anyhow::anyhow!("Image is not BGR"));
53    }
54    if data.depth != 8 {
55        return Err(anyhow::anyhow!(
56            "BGR to RGB conversion only supports 8-bit depth"
57        ));
58    }
59    for i in (0..data.data.len()).step_by(3) {
60        let b = data.data[i];
61        data.data[i] = data.data[i + 2];
62        data.data[i + 2] = b;
63    }
64    data.color_type = ImageColorType::Rgb;
65    Ok(())
66}
67
68/// Converts a BGRA image to BGR format.
69///
70/// Only supports BGRA images with 8-bit depth.
71pub fn convert_bgra_to_bgr(data: &mut ImageData) -> Result<()> {
72    if data.color_type != ImageColorType::Bgra {
73        return Err(anyhow::anyhow!("Image is not BGRA"));
74    }
75    if data.depth != 8 {
76        return Err(anyhow::anyhow!(
77            "BGRA to BGR conversion only supports 8-bit depth"
78        ));
79    }
80    let mut new_data = Vec::with_capacity(data.data.len() / 4 * 3);
81    for chunk in data.data.chunks_exact(4) {
82        new_data.push(chunk[0]); // B
83        new_data.push(chunk[1]); // G
84        new_data.push(chunk[2]); // R
85    }
86    data.data = new_data;
87    data.color_type = ImageColorType::Bgr;
88    Ok(())
89}
90
91/// Converts a BGRA image to RGBA format.
92///
93/// Only supports BGRA images with 8-bit depth.
94pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> {
95    if data.color_type != ImageColorType::Bgra {
96        return Err(anyhow::anyhow!("Image is not BGRA"));
97    }
98    if data.depth != 8 {
99        return Err(anyhow::anyhow!(
100            "BGRA to RGBA conversion only supports 8-bit depth"
101        ));
102    }
103    for i in (0..data.data.len()).step_by(4) {
104        let b = data.data[i];
105        data.data[i] = data.data[i + 2];
106        data.data[i + 2] = b;
107    }
108    data.color_type = ImageColorType::Rgba;
109    Ok(())
110}
111
112/// Converts an RGB image to RGBA format.
113///
114/// Only supports RGB images with 8-bit depth.
115pub fn convert_rgb_to_rgba(data: &mut ImageData) -> Result<()> {
116    if data.color_type != ImageColorType::Rgb {
117        return Err(anyhow::anyhow!("Image is not RGB"));
118    }
119    if data.depth != 8 {
120        return Err(anyhow::anyhow!(
121            "RGB to RGBA conversion only supports 8-bit depth"
122        ));
123    }
124    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
125    for chunk in data.data.chunks_exact(3) {
126        new_data.push(chunk[0]); // R
127        new_data.push(chunk[1]); // G
128        new_data.push(chunk[2]); // B
129        new_data.push(255); // A
130    }
131    data.data = new_data;
132    data.color_type = ImageColorType::Rgba;
133    Ok(())
134}
135
136/// Converts an RGB image to BGR format.
137///
138/// Only supports RGB images with 8-bit depth.
139pub fn convert_rgb_to_bgr(data: &mut ImageData) -> Result<()> {
140    if data.color_type != ImageColorType::Rgb {
141        return Err(anyhow::anyhow!("Image is not RGB"));
142    }
143    if data.depth != 8 {
144        return Err(anyhow::anyhow!(
145            "RGB to BGR conversion only supports 8-bit depth"
146        ));
147    }
148    for i in (0..data.data.len()).step_by(3) {
149        let r = data.data[i];
150        data.data[i] = data.data[i + 2];
151        data.data[i + 2] = r;
152    }
153    data.color_type = ImageColorType::Bgr;
154    Ok(())
155}
156
157/// Converts an RGBA image to BGRA format.
158///
159/// Only supports RGBA images with 8-bit depth.
160pub fn convert_rgba_to_bgra(data: &mut ImageData) -> Result<()> {
161    if data.color_type != ImageColorType::Rgba {
162        return Err(anyhow::anyhow!("Image is not RGBA"));
163    }
164    if data.depth != 8 {
165        return Err(anyhow::anyhow!(
166            "RGBA to BGRA conversion only supports 8-bit depth"
167        ));
168    }
169    for i in (0..data.data.len()).step_by(4) {
170        let r = data.data[i];
171        data.data[i] = data.data[i + 2];
172        data.data[i + 2] = r;
173    }
174    data.color_type = ImageColorType::Bgra;
175    Ok(())
176}
177
178/// Encodes an image to the specified format and writes it to a file.
179///
180/// * `data` - The image data to encode.
181/// * `typ` - The output image format.
182/// * `filename` - The path of the file to write the encoded image to.
183/// * `config` - Extra configuration.
184pub fn encode_img(
185    mut data: ImageData,
186    typ: ImageOutputType,
187    filename: &str,
188    config: &ExtraConfig,
189) -> Result<()> {
190    match typ {
191        ImageOutputType::Png => {
192            let mut file = crate::utils::files::write_file(filename)?;
193            let color_type = match data.color_type {
194                ImageColorType::Grayscale => png::ColorType::Grayscale,
195                ImageColorType::Rgb => png::ColorType::Rgb,
196                ImageColorType::Rgba => png::ColorType::Rgba,
197                ImageColorType::Bgr => {
198                    convert_bgr_to_rgb(&mut data)?;
199                    png::ColorType::Rgb
200                }
201                ImageColorType::Bgra => {
202                    convert_bgra_to_rgba(&mut data)?;
203                    png::ColorType::Rgba
204                }
205            };
206            let bit_depth = match &data.depth {
207                1 => png::BitDepth::One,
208                2 => png::BitDepth::Two,
209                4 => png::BitDepth::Four,
210                8 => png::BitDepth::Eight,
211                16 => png::BitDepth::Sixteen,
212                _ => return Err(anyhow::anyhow!("Unsupported bit depth: {}", data.depth)),
213            };
214            let mut encoder = png::Encoder::new(&mut file, data.width, data.height);
215            encoder.set_color(color_type);
216            encoder.set_depth(bit_depth);
217            encoder.set_compression(config.png_compression_level.to_compression());
218            let mut writer = encoder.write_header()?;
219            writer.write_image_data(&data.data)?;
220            writer.finish()?;
221            Ok(())
222        }
223        #[cfg(feature = "image-jpg")]
224        ImageOutputType::Jpg => {
225            let file = crate::utils::files::write_file(filename)?;
226            let color_type = match data.color_type {
227                ImageColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE,
228                ImageColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB,
229                ImageColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA,
230                ImageColorType::Bgr => {
231                    convert_bgr_to_rgb(&mut data)?;
232                    mozjpeg::ColorSpace::JCS_RGB
233                }
234                ImageColorType::Bgra => {
235                    convert_bgra_to_rgba(&mut data)?;
236                    mozjpeg::ColorSpace::JCS_EXT_RGBA
237                }
238            };
239            if data.depth != 8 {
240                return Err(anyhow::anyhow!(
241                    "JPEG encoding only supports 8-bit depth, found: {}",
242                    data.depth
243                ));
244            }
245            let mut encoder = mozjpeg::compress::Compress::new(color_type);
246            encoder.set_size(data.width as usize, data.height as usize);
247            encoder.set_quality(config.jpeg_quality as f32);
248            let mut start = encoder.start_compress(file)?;
249            start.write_scanlines(&data.data)?;
250            start.finish()?;
251            Ok(())
252        }
253        #[cfg(feature = "image-webp")]
254        ImageOutputType::Webp => {
255            let mut file = crate::utils::files::write_file(filename)?;
256            let color_type = match data.color_type {
257                ImageColorType::Rgb => webp::PixelLayout::Rgb,
258                ImageColorType::Rgba => webp::PixelLayout::Rgba,
259                ImageColorType::Bgr => {
260                    convert_bgr_to_rgb(&mut data)?;
261                    webp::PixelLayout::Rgb
262                }
263                ImageColorType::Bgra => {
264                    convert_bgra_to_rgba(&mut data)?;
265                    webp::PixelLayout::Rgba
266                }
267                _ => {
268                    return Err(anyhow::anyhow!(
269                        "Unsupported color type for WebP: {:?}",
270                        data.color_type
271                    ));
272                }
273            };
274            if data.depth != 8 {
275                return Err(anyhow::anyhow!(
276                    "WebP encoding only supports 8-bit depth, found: {}",
277                    data.depth
278                ));
279            }
280            let encoder = webp::Encoder::new(&data.data, color_type, data.width, data.height);
281            let re = encoder
282                .encode_simple(config.webp_lossless, config.webp_quality as f32)
283                .map_err(|e| anyhow::anyhow!("Failed to encode WebP image: {:?}", e))?;
284            file.write_all(&re)?;
285            Ok(())
286        }
287    }
288}
289
290/// Loads a PNG image from the given reader and returns its data.
291pub fn load_png<R: std::io::Read>(data: R) -> Result<ImageData> {
292    let decoder = png::Decoder::new(data);
293    let mut reader = decoder.read_info()?;
294    let bit_depth = match reader.info().bit_depth {
295        png::BitDepth::One => 1,
296        png::BitDepth::Two => 2,
297        png::BitDepth::Four => 4,
298        png::BitDepth::Eight => 8,
299        png::BitDepth::Sixteen => 16,
300    };
301    let color_type = match reader.info().color_type {
302        png::ColorType::Grayscale => ImageColorType::Grayscale,
303        png::ColorType::Rgb => ImageColorType::Rgb,
304        png::ColorType::Rgba => ImageColorType::Rgba,
305        _ => {
306            return Err(anyhow::anyhow!(
307                "Unsupported color type: {:?}",
308                reader.info().color_type
309            ));
310        }
311    };
312    let stride = reader.info().width as usize * color_type.bpp(bit_depth) as usize / 8;
313    let mut data = vec![0; stride * reader.info().height as usize];
314    reader.next_frame(&mut data)?;
315    Ok(ImageData {
316        width: reader.info().width,
317        height: reader.info().height,
318        depth: bit_depth,
319        color_type,
320        data,
321    })
322}
323
324#[cfg(feature = "mozjpeg")]
325pub fn load_jpg<R: std::io::Read>(data: R) -> Result<ImageData> {
326    let decoder = mozjpeg::decompress::Decompress::new_reader(std::io::BufReader::new(data))?;
327    let color_type = match decoder.color_space() {
328        mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale,
329        mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb,
330        mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba,
331        _ => ImageColorType::Rgb, // Convert other types to RGB
332    };
333    let width = decoder.width() as u32;
334    let height = decoder.height() as u32;
335    let stride = width as usize * color_type.bpp(8) as usize / 8;
336    let mut data = vec![0; stride * height as usize];
337    let mut re = match color_type {
338        ImageColorType::Grayscale => decoder.grayscale()?,
339        ImageColorType::Rgb => decoder.rgb()?,
340        ImageColorType::Rgba => decoder.rgba()?,
341        _ => {
342            unreachable!(); // We already checked the color type above
343        }
344    };
345    re.read_scanlines_into(&mut data)?;
346    Ok(ImageData {
347        width,
348        height,
349        depth: 8,
350        color_type,
351        data,
352    })
353}
354
355/// Decodes an image from the specified file path and returns its data.
356///
357/// * `typ` - The type of the image to decode.
358/// * `filename` - The path of the file to decode.
359pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result<ImageData> {
360    match typ {
361        ImageOutputType::Png => {
362            let file = crate::utils::files::read_file(filename)?;
363            load_png(&file[..])
364        }
365        #[cfg(feature = "image-jpg")]
366        ImageOutputType::Jpg => {
367            let file = crate::utils::files::read_file(filename)?;
368            load_jpg(&file[..])
369        }
370        #[cfg(feature = "image-webp")]
371        ImageOutputType::Webp => {
372            let file = crate::utils::files::read_file(filename)?;
373            let decoder = webp::Decoder::new(&file);
374            let image = decoder
375                .decode()
376                .ok_or(anyhow::anyhow!("Failed to decode WebP image"))?;
377            let color_type = if image.is_alpha() {
378                ImageColorType::Rgba
379            } else {
380                ImageColorType::Rgb
381            };
382            let width = image.width();
383            let height = image.height();
384            let stride = width as usize * color_type.bpp(8) as usize / 8;
385            let mut data = vec![0; stride * height as usize];
386            if image.len() != data.len() {
387                return Err(anyhow::anyhow!(
388                    "WebP image data size mismatch: expected {}, got {}",
389                    data.len(),
390                    image.len()
391                ));
392            }
393            data.copy_from_slice(&image);
394            Ok(ImageData {
395                width,
396                height,
397                depth: 8,
398                color_type,
399                data,
400            })
401        }
402    }
403}
404
405/// Draws an image on a canvas with specified offsets.
406///
407/// * `img` - The image data to draw.
408/// * `canvas_width` - The width of the canvas.
409/// * `canvas_height` - The height of the canvas.
410/// * `offset_x` - The horizontal offset to start drawing the image.
411/// * `offset_y` - The vertical offset to start drawing the image.
412///
413/// Returns the canvas image data.
414pub fn draw_on_canvas(
415    img: ImageData,
416    canvas_width: u32,
417    canvas_height: u32,
418    offset_x: u32,
419    offset_y: u32,
420) -> Result<ImageData> {
421    let bytes_per_pixel = img.color_type.bpp(img.depth) as u32 / 8;
422    let mut canvas_data = vec![0u8; (canvas_width * canvas_height * bytes_per_pixel) as usize];
423    let canvas_stride = canvas_width * bytes_per_pixel;
424    let img_stride = img.width * bytes_per_pixel;
425
426    for y in 0..img.height {
427        let canvas_y = y + offset_y;
428        if canvas_y >= canvas_height {
429            continue;
430        }
431        let canvas_start = (canvas_y * canvas_stride + offset_x * bytes_per_pixel) as usize;
432        let img_start = (y * img_stride) as usize;
433        let copy_len = img_stride as usize;
434        if canvas_start + copy_len > canvas_data.len() {
435            continue;
436        }
437        canvas_data[canvas_start..canvas_start + copy_len]
438            .copy_from_slice(&img.data[img_start..img_start + copy_len]);
439    }
440
441    Ok(ImageData {
442        width: canvas_width,
443        height: canvas_height,
444        color_type: img.color_type,
445        depth: img.depth,
446        data: canvas_data,
447    })
448}
449
450/// Flips an image vertically.
451pub fn flip_image(data: &mut ImageData) -> Result<()> {
452    if data.height <= 1 {
453        return Ok(());
454    }
455    let row_size = data.color_type.bpp(data.depth) as usize * data.width as usize / 8;
456    if row_size == 0 {
457        return Ok(());
458    }
459
460    let mut i = 0;
461    let mut j = data.height as usize - 1;
462    while i < j {
463        let (top, bottom) = data.data.split_at_mut(j * row_size);
464        let top_row = &mut top[i * row_size..i * row_size + row_size];
465        let bottom_row = &mut bottom[0..row_size];
466        top_row.swap_with_slice(bottom_row);
467        i += 1;
468        j -= 1;
469    }
470
471    Ok(())
472}
473
474/// Applies opacity to an image.
475///
476/// Only supports RGBA or BGRA images with 8-bit depth.
477pub fn apply_opacity(img: &mut ImageData, opacity: u8) -> Result<()> {
478    if img.color_type != ImageColorType::Rgba && img.color_type != ImageColorType::Bgra {
479        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
480    }
481    if img.depth != 8 {
482        return Err(anyhow::anyhow!(
483            "Opacity application only supports 8-bit depth"
484        ));
485    }
486    for i in (0..img.data.len()).step_by(4) {
487        img.data[i + 3] = (img.data[i + 3] as u16 * opacity as u16 / 255) as u8;
488    }
489    Ok(())
490}
491
492/// Draws an image on another image with specified opacity.
493///
494/// * `base` - The base image to draw on.
495/// * `diff` - The image to draw with opacity.
496/// * `left` - The horizontal offset to start drawing the image.
497/// * `top` - The vertical offset to start drawing the image.
498/// * `opacity` - The opacity level to apply to the drawn image (0-255
499pub fn draw_on_img_with_opacity(
500    base: &mut ImageData,
501    diff: &ImageData,
502    left: u32,
503    top: u32,
504    opacity: u8,
505) -> Result<()> {
506    if base.color_type != diff.color_type {
507        return Err(anyhow::anyhow!("Image color types do not match"));
508    }
509    if base.color_type != ImageColorType::Rgba && base.color_type != ImageColorType::Bgra {
510        return Err(anyhow::anyhow!("Images are not RGBA or BGRA"));
511    }
512    if base.depth != 8 || diff.depth != 8 {
513        return Err(anyhow::anyhow!(
514            "Image drawing with opacity only supports 8-bit depth"
515        ));
516    }
517
518    let bpp = 4;
519    let base_stride = base.width as usize * bpp;
520    let diff_stride = diff.width as usize * bpp;
521
522    for y in 0..diff.height {
523        let base_y = top + y;
524        if base_y >= base.height {
525            continue;
526        }
527
528        for x in 0..diff.width {
529            let base_x = left + x;
530            if base_x >= base.width {
531                continue;
532            }
533
534            let diff_idx = (y as usize * diff_stride) + (x as usize * bpp);
535            let base_idx = (base_y as usize * base_stride) + (base_x as usize * bpp);
536
537            let diff_pixel = &diff.data[diff_idx..diff_idx + bpp];
538            let base_pixel_orig = base.data[base_idx..base_idx + bpp].to_vec();
539
540            let src_alpha_u16 = (diff_pixel[3] as u16 * opacity as u16) / 255;
541
542            if src_alpha_u16 == 0 {
543                continue;
544            }
545
546            let dst_alpha_u16 = base_pixel_orig[3] as u16;
547
548            // out_alpha = src_alpha + dst_alpha * (1 - src_alpha)
549            let out_alpha_u16 = src_alpha_u16 + (dst_alpha_u16 * (255 - src_alpha_u16)) / 255;
550
551            if out_alpha_u16 == 0 {
552                for i in 0..4 {
553                    base.data[base_idx + i] = 0;
554                }
555                continue;
556            }
557
558            // out_color = (src_color * src_alpha + dst_color * dst_alpha * (1 - src_alpha)) / out_alpha
559            for i in 0..3 {
560                let src_comp = diff_pixel[i] as u16;
561                let dst_comp = base_pixel_orig[i] as u16;
562
563                let numerator = src_comp * src_alpha_u16
564                    + (dst_comp * dst_alpha_u16 * (255 - src_alpha_u16)) / 255;
565                base.data[base_idx + i] = (numerator / out_alpha_u16) as u8;
566            }
567            base.data[base_idx + 3] = out_alpha_u16 as u8;
568        }
569    }
570
571    Ok(())
572}