msg_tool\utils/
img.rs

1//! Image Utilities
2#[cfg(feature = "image-jxl")]
3use super::jxl::*;
4use crate::ext::io::*;
5use crate::types::*;
6use anyhow::Result;
7use std::convert::TryFrom;
8use std::io::Write;
9
10/// Reverses the alpha values of an image.
11///
12/// Only supports RGBA or BGRA images with 8-bit depth.
13pub fn reverse_alpha_values(data: &mut ImageData) -> Result<()> {
14    if data.color_type != ImageColorType::Rgba && data.color_type != ImageColorType::Bgra {
15        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
16    }
17    if data.depth != 8 {
18        return Err(anyhow::anyhow!(
19            "Alpha value reversal only supports 8-bit depth"
20        ));
21    }
22    for i in (0..data.data.len()).step_by(4) {
23        data.data[i + 3] = 255 - data.data[i + 3];
24    }
25    Ok(())
26}
27
28/// Converts a BGR image to BGRA format.
29///
30/// Only supports BGR images with 8-bit depth.
31pub fn convert_bgr_to_bgra(data: &mut ImageData) -> Result<()> {
32    if data.color_type != ImageColorType::Bgr {
33        return Err(anyhow::anyhow!("Image is not BGR"));
34    }
35    if data.depth != 8 {
36        return Err(anyhow::anyhow!(
37            "BGR to BGRA conversion only supports 8-bit depth"
38        ));
39    }
40    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
41    for chunk in data.data.chunks_exact(3) {
42        new_data.push(chunk[0]); // B
43        new_data.push(chunk[1]); // G
44        new_data.push(chunk[2]); // R
45        new_data.push(255); // A
46    }
47    data.data = new_data;
48    data.color_type = ImageColorType::Bgra;
49    Ok(())
50}
51
52/// Converts a BGR image to RGB format.
53///
54/// Only supports BGR images with 8-bit depth.
55pub fn convert_bgr_to_rgb(data: &mut ImageData) -> Result<()> {
56    if data.color_type != ImageColorType::Bgr {
57        return Err(anyhow::anyhow!("Image is not BGR"));
58    }
59    if data.depth != 8 {
60        return Err(anyhow::anyhow!(
61            "BGR to RGB conversion only supports 8-bit depth"
62        ));
63    }
64    for i in (0..data.data.len()).step_by(3) {
65        let b = data.data[i];
66        data.data[i] = data.data[i + 2];
67        data.data[i + 2] = b;
68    }
69    data.color_type = ImageColorType::Rgb;
70    Ok(())
71}
72
73/// Converts a BGRA image to BGR format.
74///
75/// Only supports BGRA images with 8-bit depth.
76pub fn convert_bgra_to_bgr(data: &mut ImageData) -> Result<()> {
77    if data.color_type != ImageColorType::Bgra {
78        return Err(anyhow::anyhow!("Image is not BGRA"));
79    }
80    if data.depth != 8 {
81        return Err(anyhow::anyhow!(
82            "BGRA to BGR conversion only supports 8-bit depth"
83        ));
84    }
85    let mut new_data = Vec::with_capacity(data.data.len() / 4 * 3);
86    for chunk in data.data.chunks_exact(4) {
87        new_data.push(chunk[0]); // B
88        new_data.push(chunk[1]); // G
89        new_data.push(chunk[2]); // R
90    }
91    data.data = new_data;
92    data.color_type = ImageColorType::Bgr;
93    Ok(())
94}
95
96/// Converts a BGRA image to RGBA format.
97///
98/// Only supports BGRA images with 8-bit depth.
99pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> {
100    if data.color_type != ImageColorType::Bgra {
101        return Err(anyhow::anyhow!("Image is not BGRA"));
102    }
103    if data.depth != 8 {
104        return Err(anyhow::anyhow!(
105            "BGRA to RGBA conversion only supports 8-bit depth"
106        ));
107    }
108    for i in (0..data.data.len()).step_by(4) {
109        let b = data.data[i];
110        data.data[i] = data.data[i + 2];
111        data.data[i + 2] = b;
112    }
113    data.color_type = ImageColorType::Rgba;
114    Ok(())
115}
116
117/// Converts an RGB image to RGBA format.
118///
119/// Only supports RGB images with 8-bit depth.
120pub fn convert_rgb_to_rgba(data: &mut ImageData) -> Result<()> {
121    if data.color_type != ImageColorType::Rgb {
122        return Err(anyhow::anyhow!("Image is not RGB"));
123    }
124    if data.depth != 8 {
125        return Err(anyhow::anyhow!(
126            "RGB to RGBA conversion only supports 8-bit depth"
127        ));
128    }
129    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
130    for chunk in data.data.chunks_exact(3) {
131        new_data.push(chunk[0]); // R
132        new_data.push(chunk[1]); // G
133        new_data.push(chunk[2]); // B
134        new_data.push(255); // A
135    }
136    data.data = new_data;
137    data.color_type = ImageColorType::Rgba;
138    Ok(())
139}
140
141/// Converts an RGB image to BGR format.
142///
143/// Only supports RGB images with 8-bit depth.
144pub fn convert_rgb_to_bgr(data: &mut ImageData) -> Result<()> {
145    if data.color_type != ImageColorType::Rgb {
146        return Err(anyhow::anyhow!("Image is not RGB"));
147    }
148    if data.depth != 8 {
149        return Err(anyhow::anyhow!(
150            "RGB to BGR conversion only supports 8-bit depth"
151        ));
152    }
153    for i in (0..data.data.len()).step_by(3) {
154        let r = data.data[i];
155        data.data[i] = data.data[i + 2];
156        data.data[i + 2] = r;
157    }
158    data.color_type = ImageColorType::Bgr;
159    Ok(())
160}
161
162/// Converts an RGBA image to BGRA format.
163///
164/// Only supports RGBA images with 8-bit depth.
165pub fn convert_rgba_to_bgra(data: &mut ImageData) -> Result<()> {
166    if data.color_type != ImageColorType::Rgba {
167        return Err(anyhow::anyhow!("Image is not RGBA"));
168    }
169    if data.depth != 8 {
170        return Err(anyhow::anyhow!(
171            "RGBA to BGRA conversion only supports 8-bit depth"
172        ));
173    }
174    for i in (0..data.data.len()).step_by(4) {
175        let r = data.data[i];
176        data.data[i] = data.data[i + 2];
177        data.data[i + 2] = r;
178    }
179    data.color_type = ImageColorType::Bgra;
180    Ok(())
181}
182
183/// Converts a Grayscale image to RGB format.
184pub fn convert_grayscale_to_rgb(data: &mut ImageData) -> Result<()> {
185    if data.color_type != ImageColorType::Grayscale {
186        return Err(anyhow::anyhow!("Image is not Grayscale"));
187    }
188    if data.depth != 8 {
189        return Err(anyhow::anyhow!(
190            "Grayscale to RGB conversion only supports 8-bit depth"
191        ));
192    }
193    let mut new_data = Vec::with_capacity(data.data.len() * 3);
194    for &gray in &data.data {
195        new_data.push(gray); // R
196        new_data.push(gray); // G
197        new_data.push(gray); // B
198    }
199    data.data = new_data;
200    data.color_type = ImageColorType::Rgb;
201    Ok(())
202}
203
204/// Converts a Grayscale image to RGBA format.
205pub fn convert_grayscale_to_rgba(data: &mut ImageData) -> Result<()> {
206    if data.color_type != ImageColorType::Grayscale {
207        return Err(anyhow::anyhow!("Image is not Grayscale"));
208    }
209    if data.depth != 8 {
210        return Err(anyhow::anyhow!(
211            "Grayscale to RGBA conversion only supports 8-bit depth"
212        ));
213    }
214    let mut new_data = Vec::with_capacity(data.data.len() * 4);
215    for &gray in &data.data {
216        new_data.push(gray); // R
217        new_data.push(gray); // G
218        new_data.push(gray); // B
219        new_data.push(255); // A
220    }
221    data.data = new_data;
222    data.color_type = ImageColorType::Rgba;
223    Ok(())
224}
225
226/// Converts an image to RGBA format.
227pub fn convert_to_rgba(data: &mut ImageData) -> Result<()> {
228    match data.color_type {
229        ImageColorType::Rgb => convert_rgb_to_rgba(data),
230        ImageColorType::Bgr => convert_bgr_to_bgra(data),
231        ImageColorType::Rgba => Ok(()),
232        ImageColorType::Bgra => convert_bgra_to_rgba(data),
233        ImageColorType::Grayscale => convert_grayscale_to_rgba(data),
234    }
235}
236
237/// Encodes an image to the specified format and writes it to a file.
238///
239/// * `data` - The image data to encode.
240/// * `typ` - The output image format.
241/// * `filename` - The path of the file to write the encoded image to.
242/// * `config` - Extra configuration.
243pub fn encode_img(
244    data: ImageData,
245    typ: ImageOutputType,
246    filename: &str,
247    config: &ExtraConfig,
248) -> Result<()> {
249    let mut file = crate::utils::files::write_file(filename)?;
250    encode_img_writer(data, typ, &mut file, config)
251}
252
253/// Encodes an image to the specified format and writes it to a writer.
254///
255/// * `data` - The image data to encode.
256/// * `typ` - The output image format.
257/// * `filename` - The path of the file to write the encoded image to.
258/// * `config` - Extra configuration.
259pub fn encode_img_writer<T: Write>(
260    mut data: ImageData,
261    typ: ImageOutputType,
262    mut file: &mut T,
263    config: &ExtraConfig,
264) -> Result<()> {
265    match typ {
266        ImageOutputType::Png => {
267            let color_type = match data.color_type {
268                ImageColorType::Grayscale => png::ColorType::Grayscale,
269                ImageColorType::Rgb => png::ColorType::Rgb,
270                ImageColorType::Rgba => png::ColorType::Rgba,
271                ImageColorType::Bgr => {
272                    convert_bgr_to_rgb(&mut data)?;
273                    png::ColorType::Rgb
274                }
275                ImageColorType::Bgra => {
276                    convert_bgra_to_rgba(&mut data)?;
277                    png::ColorType::Rgba
278                }
279            };
280            let bit_depth = match &data.depth {
281                1 => png::BitDepth::One,
282                2 => png::BitDepth::Two,
283                4 => png::BitDepth::Four,
284                8 => png::BitDepth::Eight,
285                16 => png::BitDepth::Sixteen,
286                _ => return Err(anyhow::anyhow!("Unsupported bit depth: {}", data.depth)),
287            };
288            let mut encoder = png::Encoder::new(&mut file, data.width, data.height);
289            encoder.set_color(color_type);
290            encoder.set_depth(bit_depth);
291            encoder.set_compression(config.png_compression_level.to_compression());
292            let mut writer = encoder.write_header()?;
293            writer.write_image_data(&data.data)?;
294            writer.finish()?;
295            Ok(())
296        }
297        #[cfg(feature = "image-jpg")]
298        ImageOutputType::Jpg => {
299            let color_type = match data.color_type {
300                ImageColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE,
301                ImageColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB,
302                ImageColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA,
303                ImageColorType::Bgr => {
304                    convert_bgr_to_rgb(&mut data)?;
305                    mozjpeg::ColorSpace::JCS_RGB
306                }
307                ImageColorType::Bgra => {
308                    convert_bgra_to_rgba(&mut data)?;
309                    mozjpeg::ColorSpace::JCS_EXT_RGBA
310                }
311            };
312            if data.depth != 8 {
313                return Err(anyhow::anyhow!(
314                    "JPEG encoding only supports 8-bit depth, found: {}",
315                    data.depth
316                ));
317            }
318            let mut encoder = mozjpeg::compress::Compress::new(color_type);
319            encoder.set_size(data.width as usize, data.height as usize);
320            encoder.set_quality(config.jpeg_quality as f32);
321            let mut start = encoder.start_compress(file)?;
322            start.write_scanlines(&data.data)?;
323            start.finish()?;
324            Ok(())
325        }
326        #[cfg(feature = "image-webp")]
327        ImageOutputType::Webp => {
328            let color_type = match data.color_type {
329                ImageColorType::Rgb => webp::PixelLayout::Rgb,
330                ImageColorType::Rgba => webp::PixelLayout::Rgba,
331                ImageColorType::Bgr => {
332                    convert_bgr_to_rgb(&mut data)?;
333                    webp::PixelLayout::Rgb
334                }
335                ImageColorType::Bgra => {
336                    convert_bgra_to_rgba(&mut data)?;
337                    webp::PixelLayout::Rgba
338                }
339                _ => {
340                    return Err(anyhow::anyhow!(
341                        "Unsupported color type for WebP: {:?}",
342                        data.color_type
343                    ));
344                }
345            };
346            if data.depth != 8 {
347                return Err(anyhow::anyhow!(
348                    "WebP encoding only supports 8-bit depth, found: {}",
349                    data.depth
350                ));
351            }
352            let encoder = webp::Encoder::new(&data.data, color_type, data.width, data.height);
353            let re = encoder
354                .encode_simple(config.webp_lossless, config.webp_quality as f32)
355                .map_err(|e| anyhow::anyhow!("Failed to encode WebP image: {:?}", e))?;
356            file.write_all(&re)?;
357            Ok(())
358        }
359        #[cfg(feature = "image-jxl")]
360        ImageOutputType::Jxl => {
361            let data = encode_jxl(data, config)?;
362            file.write_all(&data)?;
363            Ok(())
364        }
365    }
366}
367
368pub fn convert_grayscale_alpha_to_rgba(
369    raw_data: &[u8],
370    width: usize,
371    height: usize,
372    bit_depth: u8,
373) -> Result<ImageData> {
374    if bit_depth != 8 && bit_depth != 16 {
375        return Err(anyhow::anyhow!(
376            "Unsupported bit depth for GrayscaleAlpha to RGBA conversion: {}",
377            bit_depth
378        ));
379    }
380    let bytes_per_channel = (bit_depth / 8) as usize;
381    let stride = width * 2 * bytes_per_channel;
382    if raw_data.len() != stride * height {
383        return Err(anyhow::anyhow!(
384            "Input data size does not match expected size for GrayscaleAlpha image"
385        ));
386    }
387    let mut data = Vec::with_capacity(width * height * 4 * bytes_per_channel);
388    for y in 0..height {
389        for x in 0..width {
390            let base = y * stride + x * 2 * bytes_per_channel;
391            // Grayscale channel
392            data.extend_from_slice(&raw_data[base..base + bytes_per_channel]);
393            data.extend_from_slice(&raw_data[base..base + bytes_per_channel]);
394            data.extend_from_slice(&raw_data[base..base + bytes_per_channel]);
395            // Alpha channel
396            data.extend_from_slice(
397                &raw_data[base + bytes_per_channel..base + 2 * bytes_per_channel],
398            );
399        }
400    }
401    Ok(ImageData {
402        width: width as u32,
403        height: height as u32,
404        depth: bit_depth,
405        color_type: ImageColorType::Rgba,
406        data,
407    })
408}
409
410/// Loads a PNG image from the given reader and returns its data.
411pub fn load_png<R: std::io::Read + std::io::Seek>(data: R) -> Result<ImageData> {
412    let decoder = png::Decoder::new(std::io::BufReader::new(data));
413    let mut reader = decoder.read_info()?;
414    let bit_depth = match reader.info().bit_depth {
415        png::BitDepth::One => 1,
416        png::BitDepth::Two => 2,
417        png::BitDepth::Four => 4,
418        png::BitDepth::Eight => 8,
419        png::BitDepth::Sixteen => 16,
420    };
421    let color_type = match reader.info().color_type {
422        png::ColorType::Grayscale => ImageColorType::Grayscale,
423        png::ColorType::Rgb => ImageColorType::Rgb,
424        png::ColorType::Rgba => ImageColorType::Rgba,
425        png::ColorType::GrayscaleAlpha => ImageColorType::Rgba,
426        png::ColorType::Indexed => {
427            if let Some(palette) = &reader.info().palette {
428                if palette.len() % 3 != 0 {
429                    return Err(anyhow::anyhow!(
430                        "Invalid PNG palette length: {}",
431                        palette.len()
432                    ));
433                }
434                ImageColorType::Rgb
435            } else {
436                return Err(anyhow::anyhow!(
437                    "PNG image has indexed color type but no palette"
438                ));
439            }
440        }
441    };
442    if reader.info().color_type == png::ColorType::GrayscaleAlpha {
443        let height = reader.info().height as usize;
444        let width = reader.info().width as usize;
445        let raw_stride = width * 2 * bit_depth as usize / 8;
446        let mut raw_data = vec![0; raw_stride * height];
447        reader.next_frame(&mut raw_data)?;
448        return convert_grayscale_alpha_to_rgba(&raw_data, width, height, bit_depth);
449    }
450    if reader.info().color_type == png::ColorType::Indexed {
451        let mut palette = reader
452            .info()
453            .palette
454            .as_ref()
455            .ok_or_else(|| anyhow::anyhow!("PNG image has indexed color type but no palette"))?
456            .to_vec();
457        let mut palette_format = PaletteFormat::Rgb;
458        if let Some(trns) = &reader.info().trns {
459            let mut new_palette = Vec::with_capacity(palette.len() / 3 * 4);
460            let trns_len = trns.len();
461            for i in 0..(palette.len() / 3) {
462                new_palette.push(palette[i * 3]);
463                new_palette.push(palette[i * 3 + 1]);
464                new_palette.push(palette[i * 3 + 2]);
465                let alpha = if i < trns_len { trns[i] } else { 255 };
466                new_palette.push(alpha);
467            }
468            palette = new_palette;
469            palette_format = PaletteFormat::RgbA;
470        }
471        let width = reader.info().width as usize;
472        let height = reader.info().height as usize;
473        let raw_stride = width * bit_depth as usize / 8;
474        let mut raw_data = vec![0; raw_stride * height];
475        reader.next_frame(&mut raw_data)?;
476        return convert_index_palette_to_normal_bitmap(
477            &raw_data,
478            bit_depth as usize,
479            &palette,
480            palette_format,
481            width,
482            height,
483        );
484    }
485    let stride = reader.info().width as usize * color_type.bpp(bit_depth) as usize / 8;
486    let mut data = vec![0; stride * reader.info().height as usize];
487    reader.next_frame(&mut data)?;
488    Ok(ImageData {
489        width: reader.info().width,
490        height: reader.info().height,
491        depth: bit_depth,
492        color_type,
493        data,
494    })
495}
496
497#[cfg(feature = "mozjpeg")]
498pub fn load_jpg<R: std::io::Read>(data: R) -> Result<ImageData> {
499    let decoder = mozjpeg::decompress::Decompress::new_reader(std::io::BufReader::new(data))?;
500    let color_type = match decoder.color_space() {
501        mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale,
502        mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb,
503        mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba,
504        _ => ImageColorType::Rgb, // Convert other types to RGB
505    };
506    let width = decoder.width() as u32;
507    let height = decoder.height() as u32;
508    let stride = width as usize * color_type.bpp(8) as usize / 8;
509    let mut data = vec![0; stride * height as usize];
510    let mut re = match color_type {
511        ImageColorType::Grayscale => decoder.grayscale()?,
512        ImageColorType::Rgb => decoder.rgb()?,
513        ImageColorType::Rgba => decoder.rgba()?,
514        _ => {
515            unreachable!(); // We already checked the color type above
516        }
517    };
518    re.read_scanlines_into(&mut data)?;
519    Ok(ImageData {
520        width,
521        height,
522        depth: 8,
523        color_type,
524        data,
525    })
526}
527
528#[cfg(feature = "webp")]
529pub fn load_webp<R: std::io::Read>(mut data: R) -> Result<ImageData> {
530    use std::io::Read;
531    let mut header = [0; 12];
532    data.read_exact(&mut header)?;
533    if !header.starts_with(b"RIFF") || !header.ends_with(b"WEBP") {
534        anyhow::bail!("File is not a webp image.");
535    }
536    let file_size = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
537    let mut da = Vec::with_capacity(file_size as usize + 8);
538    da.extend_from_slice(&header);
539    data.take(file_size as u64 - 4).read_to_end(&mut da)?;
540    let decoder = webp::Decoder::new(&da);
541    let image = decoder
542        .decode()
543        .ok_or(anyhow::anyhow!("Failed to decode WebP image"))?;
544    let color_type = if image.is_alpha() {
545        ImageColorType::Rgba
546    } else {
547        ImageColorType::Rgb
548    };
549    let width = image.width();
550    let height = image.height();
551    let stride = width as usize * color_type.bpp(8) as usize / 8;
552    let mut data = vec![0; stride * height as usize];
553    if image.len() != data.len() {
554        return Err(anyhow::anyhow!(
555            "WebP image data size mismatch: expected {}, got {}",
556            data.len(),
557            image.len()
558        ));
559    }
560    data.copy_from_slice(&image);
561    Ok(ImageData {
562        width,
563        height,
564        depth: 8,
565        color_type,
566        data,
567    })
568}
569
570#[cfg(feature = "qoi")]
571pub fn load_qoi<R: std::io::Read>(data: R) -> Result<ImageData> {
572    let mut decoder = qoi::Decoder::from_stream(data)?;
573    let data = decoder.decode_to_vec()?;
574    let header = decoder.header();
575    Ok(ImageData {
576        width: header.width,
577        height: header.height,
578        color_type: match header.channels {
579            qoi::Channels::Rgb => ImageColorType::Rgb,
580            qoi::Channels::Rgba => ImageColorType::Rgba,
581        },
582        depth: 8,
583        data,
584    })
585}
586
587/// Decodes an image from the specified file path and returns its data.
588///
589/// * `typ` - The type of the image to decode.
590/// * `filename` - The path of the file to decode.
591pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result<ImageData> {
592    match typ {
593        ImageOutputType::Png => {
594            let file = crate::utils::files::read_file(filename)?;
595            let reader = MemReader::new(file);
596            load_png(reader)
597        }
598        #[cfg(feature = "image-jpg")]
599        ImageOutputType::Jpg => {
600            let file = crate::utils::files::read_file(filename)?;
601            load_jpg(&file[..])
602        }
603        #[cfg(feature = "image-webp")]
604        ImageOutputType::Webp => {
605            let file = crate::utils::files::read_file(filename)?;
606            let decoder = webp::Decoder::new(&file);
607            let image = decoder
608                .decode()
609                .ok_or(anyhow::anyhow!("Failed to decode WebP image"))?;
610            let color_type = if image.is_alpha() {
611                ImageColorType::Rgba
612            } else {
613                ImageColorType::Rgb
614            };
615            let width = image.width();
616            let height = image.height();
617            let stride = width as usize * color_type.bpp(8) as usize / 8;
618            let mut data = vec![0; stride * height as usize];
619            if image.len() != data.len() {
620                return Err(anyhow::anyhow!(
621                    "WebP image data size mismatch: expected {}, got {}",
622                    data.len(),
623                    image.len()
624                ));
625            }
626            data.copy_from_slice(&image);
627            Ok(ImageData {
628                width,
629                height,
630                depth: 8,
631                color_type,
632                data,
633            })
634        }
635        #[cfg(feature = "image-jxl")]
636        ImageOutputType::Jxl => {
637            let file = crate::utils::files::read_file(filename)?;
638            decode_jxl(&file[..])
639        }
640    }
641}
642
643/// Draws an image on a canvas with specified offsets.
644///
645/// * `img` - The image data to draw.
646/// * `canvas_width` - The width of the canvas.
647/// * `canvas_height` - The height of the canvas.
648/// * `offset_x` - The horizontal offset to start drawing the image.
649/// * `offset_y` - The vertical offset to start drawing the image.
650///
651/// Returns the canvas image data.
652pub fn draw_on_canvas(
653    img: ImageData,
654    canvas_width: u32,
655    canvas_height: u32,
656    offset_x: u32,
657    offset_y: u32,
658) -> Result<ImageData> {
659    let bytes_per_pixel = img.color_type.bpp(img.depth) as u32 / 8;
660    let mut canvas_data = vec![0u8; (canvas_width * canvas_height * bytes_per_pixel) as usize];
661    let canvas_stride = canvas_width * bytes_per_pixel;
662    let img_stride = img.width * bytes_per_pixel;
663
664    for y in 0..img.height {
665        let canvas_y = y + offset_y;
666        if canvas_y >= canvas_height {
667            continue;
668        }
669        let canvas_start = (canvas_y * canvas_stride + offset_x * bytes_per_pixel) as usize;
670        let img_start = (y * img_stride) as usize;
671        let copy_len = img_stride as usize;
672        if canvas_start + copy_len > canvas_data.len() {
673            continue;
674        }
675        canvas_data[canvas_start..canvas_start + copy_len]
676            .copy_from_slice(&img.data[img_start..img_start + copy_len]);
677    }
678
679    Ok(ImageData {
680        width: canvas_width,
681        height: canvas_height,
682        color_type: img.color_type,
683        depth: img.depth,
684        data: canvas_data,
685    })
686}
687
688/// Flips an image vertically.
689pub fn flip_image(data: &mut ImageData) -> Result<()> {
690    if data.height <= 1 {
691        return Ok(());
692    }
693    let row_size = data.color_type.bpp(data.depth) as usize * data.width as usize / 8;
694    if row_size == 0 {
695        return Ok(());
696    }
697
698    let mut i = 0;
699    let mut j = data.height as usize - 1;
700    while i < j {
701        let (top, bottom) = data.data.split_at_mut(j * row_size);
702        let top_row = &mut top[i * row_size..i * row_size + row_size];
703        let bottom_row = &mut bottom[0..row_size];
704        top_row.swap_with_slice(bottom_row);
705        i += 1;
706        j -= 1;
707    }
708
709    Ok(())
710}
711
712/// Applies opacity to an image.
713///
714/// Only supports RGBA or BGRA images with 8-bit depth.
715pub fn apply_opacity(img: &mut ImageData, opacity: u8) -> Result<()> {
716    if img.color_type != ImageColorType::Rgba && img.color_type != ImageColorType::Bgra {
717        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
718    }
719    if img.depth != 8 {
720        return Err(anyhow::anyhow!(
721            "Opacity application only supports 8-bit depth"
722        ));
723    }
724    for i in (0..img.data.len()).step_by(4) {
725        img.data[i + 3] = (img.data[i + 3] as u16 * opacity as u16 / 255) as u8;
726    }
727    Ok(())
728}
729
730/// Draws an image on another image. The pixel data of `diff` will completely overwrite the pixel data of `base`.
731///
732/// * `base` - The base image to draw on.
733/// * `diff` - The image to draw.
734/// * `left` - The horizontal offset to start drawing the image.
735/// * `top` - The vertical offset to start drawing the image.
736pub fn draw_on_image(base: &mut ImageData, diff: &ImageData, left: u32, top: u32) -> Result<()> {
737    if base.color_type != diff.color_type {
738        return Err(anyhow::anyhow!("Image color types do not match"));
739    }
740    if base.depth != diff.depth {
741        return Err(anyhow::anyhow!("Image depths do not match"));
742    }
743
744    let bits_per_pixel = base.color_type.bpp(base.depth) as usize;
745    if bits_per_pixel == 0 || bits_per_pixel % 8 != 0 {
746        return Err(anyhow::anyhow!(
747            "Unsupported pixel bit layout: {} bits",
748            bits_per_pixel
749        ));
750    }
751    let bpp = bits_per_pixel / 8;
752
753    let base_stride = base.width as usize * bpp;
754    let diff_stride = diff.width as usize * bpp;
755
756    for y in 0..diff.height {
757        let base_y = top + y;
758        if base_y >= base.height {
759            continue;
760        }
761
762        for x in 0..diff.width {
763            let base_x = left + x;
764            if base_x >= base.width {
765                continue;
766            }
767
768            let diff_idx = (y as usize * diff_stride) + (x as usize * bpp);
769            let base_idx = (base_y as usize * base_stride) + (base_x as usize * bpp);
770
771            // safety: bounds should hold given width/height checks, but guard to avoid panics
772            if diff_idx + bpp > diff.data.len() || base_idx + bpp > base.data.len() {
773                continue;
774            }
775
776            base.data[base_idx..base_idx + bpp]
777                .copy_from_slice(&diff.data[diff_idx..diff_idx + bpp]);
778        }
779    }
780
781    Ok(())
782}
783
784/// Draws an image on another image with specified opacity.
785///
786/// * `base` - The base image to draw on.
787/// * `diff` - The image to draw with opacity.
788/// * `left` - The horizontal offset to start drawing the image.
789/// * `top` - The vertical offset to start drawing the image.
790/// * `opacity` - The opacity level to apply to the drawn image (0-255
791pub fn draw_on_img_with_opacity(
792    base: &mut ImageData,
793    diff: &ImageData,
794    left: u32,
795    top: u32,
796    opacity: u8,
797) -> Result<()> {
798    if base.color_type != diff.color_type {
799        return Err(anyhow::anyhow!("Image color types do not match"));
800    }
801    if base.color_type != ImageColorType::Rgba && base.color_type != ImageColorType::Bgra {
802        return Err(anyhow::anyhow!("Images are not RGBA or BGRA"));
803    }
804    if base.depth != 8 || diff.depth != 8 {
805        return Err(anyhow::anyhow!(
806            "Image drawing with opacity only supports 8-bit depth"
807        ));
808    }
809
810    let bpp = 4;
811    let base_stride = base.width as usize * bpp;
812    let diff_stride = diff.width as usize * bpp;
813
814    for y in 0..diff.height {
815        let base_y = top + y;
816        if base_y >= base.height {
817            continue;
818        }
819
820        for x in 0..diff.width {
821            let base_x = left + x;
822            if base_x >= base.width {
823                continue;
824            }
825
826            let diff_idx = (y as usize * diff_stride) + (x as usize * bpp);
827            let base_idx = (base_y as usize * base_stride) + (base_x as usize * bpp);
828
829            let diff_pixel = &diff.data[diff_idx..diff_idx + bpp];
830            let base_pixel_orig = base.data[base_idx..base_idx + bpp].to_vec();
831
832            let src_alpha_u16 = (diff_pixel[3] as u16 * opacity as u16) / 255;
833
834            if src_alpha_u16 == 0 {
835                continue;
836            }
837
838            let dst_alpha_u16 = base_pixel_orig[3] as u16;
839
840            // out_alpha = src_alpha + dst_alpha * (1 - src_alpha)
841            let out_alpha_u16 = src_alpha_u16 + (dst_alpha_u16 * (255 - src_alpha_u16)) / 255;
842
843            if out_alpha_u16 == 0 {
844                for i in 0..4 {
845                    base.data[base_idx + i] = 0;
846                }
847                continue;
848            }
849
850            // out_color = (src_color * src_alpha + dst_color * dst_alpha * (1 - src_alpha)) / out_alpha
851            for i in 0..3 {
852                let src_comp = diff_pixel[i] as u16;
853                let dst_comp = base_pixel_orig[i] as u16;
854
855                let numerator = src_comp as u32 * src_alpha_u16 as u32
856                    + (dst_comp as u32 * dst_alpha_u16 as u32 * (255 - src_alpha_u16) as u32) / 255;
857                base.data[base_idx + i] = (numerator / out_alpha_u16 as u32) as u8;
858            }
859            base.data[base_idx + 3] = out_alpha_u16 as u8;
860        }
861    }
862
863    Ok(())
864}
865
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
867pub enum PaletteFormat {
868    /// R G B Color
869    Rgb,
870    /// B G R Color
871    Bgr,
872    /// R G B Color with a unused byte
873    RgbX,
874    /// B G R Color with a unused byte
875    BgrX,
876    /// R G B A Color
877    RgbA,
878    /// B G R A Color
879    BgrA,
880}
881
882/// Converts indexed pixel data and a palette into a standard image buffer.
883///
884/// `pixel_size` is expressed in bits per pixel for the indexed data.
885pub fn convert_index_palette_to_normal_bitmap(
886    pixel_data: &[u8],
887    pixel_size: usize,
888    palettes: &[u8],
889    palette_format: PaletteFormat,
890    width: usize,
891    height: usize,
892) -> Result<ImageData> {
893    if width == 0 || height == 0 {
894        return Err(anyhow::anyhow!("Image dimensions must be non-zero"));
895    }
896    if pixel_size == 0 {
897        return Err(anyhow::anyhow!("pixel_size must be greater than zero"));
898    }
899
900    let width_u32 =
901        u32::try_from(width).map_err(|_| anyhow::anyhow!("width exceeds u32::MAX: {}", width))?;
902    let height_u32 = u32::try_from(height)
903        .map_err(|_| anyhow::anyhow!("height exceeds u32::MAX: {}", height))?;
904
905    let pixel_count = width
906        .checked_mul(height)
907        .ok_or_else(|| anyhow::anyhow!("Image dimensions overflow: {}x{}", width, height))?;
908
909    let palette_entry_size = match palette_format {
910        PaletteFormat::Rgb | PaletteFormat::Bgr => 3usize,
911        PaletteFormat::RgbX | PaletteFormat::BgrX | PaletteFormat::RgbA | PaletteFormat::BgrA => {
912            4usize
913        }
914    };
915
916    if palettes.len() < palette_entry_size {
917        return Err(anyhow::anyhow!("Palette data is too small"));
918    }
919    if palettes.len() % palette_entry_size != 0 {
920        return Err(anyhow::anyhow!(
921            "Palette length {} is not a multiple of {}",
922            palettes.len(),
923            palette_entry_size
924        ));
925    }
926    let palette_color_count = palettes.len() / palette_entry_size;
927    if palette_color_count == 0 {
928        return Err(anyhow::anyhow!("Palette does not contain any colors"));
929    }
930
931    let (color_type, output_channels) = match palette_format {
932        PaletteFormat::Rgb | PaletteFormat::RgbX => (ImageColorType::Rgb, 3usize),
933        PaletteFormat::Bgr | PaletteFormat::BgrX => (ImageColorType::Bgr, 3usize),
934        PaletteFormat::RgbA => (ImageColorType::Rgba, 4usize),
935        PaletteFormat::BgrA => (ImageColorType::Bgra, 4usize),
936    };
937
938    let palette_table_len = palette_color_count
939        .checked_mul(output_channels)
940        .ok_or_else(|| anyhow::anyhow!("Palette size overflow"))?;
941    let mut palette_table = Vec::with_capacity(palette_table_len);
942    for idx in 0..palette_color_count {
943        let base = idx * palette_entry_size;
944        match palette_format {
945            PaletteFormat::Rgb => {
946                palette_table.extend_from_slice(&palettes[base..base + 3]);
947            }
948            PaletteFormat::Bgr => {
949                palette_table.extend_from_slice(&palettes[base..base + 3]);
950            }
951            PaletteFormat::RgbX => {
952                palette_table.extend_from_slice(&palettes[base..base + 3]);
953            }
954            PaletteFormat::BgrX => {
955                palette_table.extend_from_slice(&palettes[base..base + 3]);
956            }
957            PaletteFormat::RgbA => {
958                palette_table.extend_from_slice(&palettes[base..base + 4]);
959            }
960            PaletteFormat::BgrA => {
961                palette_table.extend_from_slice(&palettes[base..base + 4]);
962            }
963        }
964    }
965
966    let total_bits_required = pixel_count
967        .checked_mul(pixel_size)
968        .ok_or_else(|| anyhow::anyhow!("Pixel count overflow for pixel_size {}", pixel_size))?;
969    if total_bits_required > pixel_data.len() * 8 {
970        return Err(anyhow::anyhow!(
971            "Pixel data too short: need {} bits, have {} bits",
972            total_bits_required,
973            pixel_data.len() * 8
974        ));
975    }
976
977    let output_len = pixel_count
978        .checked_mul(output_channels)
979        .ok_or_else(|| anyhow::anyhow!("Output image size overflow"))?;
980    let mut output = Vec::with_capacity(output_len);
981
982    let stride = output_channels;
983    if pixel_size == 8 {
984        if pixel_data.len() < pixel_count {
985            return Err(anyhow::anyhow!(
986                "Pixel data too short: expected {} bytes, got {}",
987                pixel_count,
988                pixel_data.len()
989            ));
990        }
991        for &index in pixel_data.iter().take(pixel_count) {
992            let idx = index as usize;
993            if idx >= palette_color_count {
994                return Err(anyhow::anyhow!(
995                    "Palette index {} exceeds palette size {}",
996                    idx,
997                    palette_color_count
998                ));
999            }
1000            let start = idx * stride;
1001            output.extend_from_slice(&palette_table[start..start + stride]);
1002        }
1003    } else {
1004        let mut bit_offset = 0usize;
1005        for _ in 0..pixel_count {
1006            let idx = read_bits_as_usize(pixel_data, bit_offset, pixel_size)?;
1007            bit_offset = bit_offset
1008                .checked_add(pixel_size)
1009                .ok_or_else(|| anyhow::anyhow!("Bit offset overflow"))?;
1010            if idx >= palette_color_count {
1011                return Err(anyhow::anyhow!(
1012                    "Palette index {} exceeds palette size {}",
1013                    idx,
1014                    palette_color_count
1015                ));
1016            }
1017            let start = idx * stride;
1018            output.extend_from_slice(&palette_table[start..start + stride]);
1019        }
1020    }
1021
1022    Ok(ImageData {
1023        width: width_u32,
1024        height: height_u32,
1025        color_type,
1026        depth: 8,
1027        data: output,
1028    })
1029}
1030
1031fn read_bits_as_usize(data: &[u8], bit_offset: usize, bit_len: usize) -> Result<usize> {
1032    if bit_len == 0 {
1033        return Err(anyhow::anyhow!("bit_len must be greater than zero"));
1034    }
1035    if bit_len > (std::mem::size_of::<usize>() * 8) {
1036        return Err(anyhow::anyhow!("Cannot read {} bits into usize", bit_len));
1037    }
1038
1039    let mut value = 0usize;
1040    for bit_idx in 0..bit_len {
1041        let absolute_bit = bit_offset + bit_idx;
1042        let byte_index = absolute_bit / 8;
1043        if byte_index >= data.len() {
1044            return Err(anyhow::anyhow!(
1045                "Bit offset {} exceeds pixel data",
1046                absolute_bit
1047            ));
1048        }
1049        let bit_in_byte = 7 - (absolute_bit % 8);
1050        let bit = (data[byte_index] >> bit_in_byte) & 1;
1051        value = (value << 1) | bit as usize;
1052    }
1053    Ok(value)
1054}