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;
8
9/// Reverses the alpha values of an image.
10///
11/// Only supports RGBA or BGRA images with 8-bit depth.
12pub fn reverse_alpha_values(data: &mut ImageData) -> Result<()> {
13    if data.color_type != ImageColorType::Rgba && data.color_type != ImageColorType::Bgra {
14        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
15    }
16    if data.depth != 8 {
17        return Err(anyhow::anyhow!(
18            "Alpha value reversal only supports 8-bit depth"
19        ));
20    }
21    for i in (0..data.data.len()).step_by(4) {
22        data.data[i + 3] = 255 - data.data[i + 3];
23    }
24    Ok(())
25}
26
27/// Converts a BGR image to BGRA format.
28///
29/// Only supports BGR images with 8-bit depth.
30pub fn convert_bgr_to_bgra(data: &mut ImageData) -> Result<()> {
31    if data.color_type != ImageColorType::Bgr {
32        return Err(anyhow::anyhow!("Image is not BGR"));
33    }
34    if data.depth != 8 {
35        return Err(anyhow::anyhow!(
36            "BGR to BGRA conversion only supports 8-bit depth"
37        ));
38    }
39    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
40    for chunk in data.data.chunks_exact(3) {
41        new_data.push(chunk[0]); // B
42        new_data.push(chunk[1]); // G
43        new_data.push(chunk[2]); // R
44        new_data.push(255); // A
45    }
46    data.data = new_data;
47    data.color_type = ImageColorType::Bgra;
48    Ok(())
49}
50
51/// Converts a BGR image to RGB format.
52///
53/// Only supports BGR images with 8-bit depth.
54pub fn convert_bgr_to_rgb(data: &mut ImageData) -> Result<()> {
55    if data.color_type != ImageColorType::Bgr {
56        return Err(anyhow::anyhow!("Image is not BGR"));
57    }
58    if data.depth != 8 {
59        return Err(anyhow::anyhow!(
60            "BGR to RGB conversion only supports 8-bit depth"
61        ));
62    }
63    for i in (0..data.data.len()).step_by(3) {
64        let b = data.data[i];
65        data.data[i] = data.data[i + 2];
66        data.data[i + 2] = b;
67    }
68    data.color_type = ImageColorType::Rgb;
69    Ok(())
70}
71
72/// Converts a BGRA image to BGR format.
73///
74/// Only supports BGRA images with 8-bit depth.
75pub fn convert_bgra_to_bgr(data: &mut ImageData) -> Result<()> {
76    if data.color_type != ImageColorType::Bgra {
77        return Err(anyhow::anyhow!("Image is not BGRA"));
78    }
79    if data.depth != 8 {
80        return Err(anyhow::anyhow!(
81            "BGRA to BGR conversion only supports 8-bit depth"
82        ));
83    }
84    let mut new_data = Vec::with_capacity(data.data.len() / 4 * 3);
85    for chunk in data.data.chunks_exact(4) {
86        new_data.push(chunk[0]); // B
87        new_data.push(chunk[1]); // G
88        new_data.push(chunk[2]); // R
89    }
90    data.data = new_data;
91    data.color_type = ImageColorType::Bgr;
92    Ok(())
93}
94
95/// Converts a BGRA image to RGBA format.
96///
97/// Only supports BGRA images with 8-bit depth.
98pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> {
99    if data.color_type != ImageColorType::Bgra {
100        return Err(anyhow::anyhow!("Image is not BGRA"));
101    }
102    if data.depth != 8 {
103        return Err(anyhow::anyhow!(
104            "BGRA to RGBA conversion only supports 8-bit depth"
105        ));
106    }
107    for i in (0..data.data.len()).step_by(4) {
108        let b = data.data[i];
109        data.data[i] = data.data[i + 2];
110        data.data[i + 2] = b;
111    }
112    data.color_type = ImageColorType::Rgba;
113    Ok(())
114}
115
116/// Converts an RGB image to RGBA format.
117///
118/// Only supports RGB images with 8-bit depth.
119pub fn convert_rgb_to_rgba(data: &mut ImageData) -> Result<()> {
120    if data.color_type != ImageColorType::Rgb {
121        return Err(anyhow::anyhow!("Image is not RGB"));
122    }
123    if data.depth != 8 {
124        return Err(anyhow::anyhow!(
125            "RGB to RGBA conversion only supports 8-bit depth"
126        ));
127    }
128    let mut new_data = Vec::with_capacity(data.data.len() / 3 * 4);
129    for chunk in data.data.chunks_exact(3) {
130        new_data.push(chunk[0]); // R
131        new_data.push(chunk[1]); // G
132        new_data.push(chunk[2]); // B
133        new_data.push(255); // A
134    }
135    data.data = new_data;
136    data.color_type = ImageColorType::Rgba;
137    Ok(())
138}
139
140/// Converts an RGB image to BGR format.
141///
142/// Only supports RGB images with 8-bit depth.
143pub fn convert_rgb_to_bgr(data: &mut ImageData) -> Result<()> {
144    if data.color_type != ImageColorType::Rgb {
145        return Err(anyhow::anyhow!("Image is not RGB"));
146    }
147    if data.depth != 8 {
148        return Err(anyhow::anyhow!(
149            "RGB to BGR conversion only supports 8-bit depth"
150        ));
151    }
152    for i in (0..data.data.len()).step_by(3) {
153        let r = data.data[i];
154        data.data[i] = data.data[i + 2];
155        data.data[i + 2] = r;
156    }
157    data.color_type = ImageColorType::Bgr;
158    Ok(())
159}
160
161/// Converts an RGBA image to BGRA format.
162///
163/// Only supports RGBA images with 8-bit depth.
164pub fn convert_rgba_to_bgra(data: &mut ImageData) -> Result<()> {
165    if data.color_type != ImageColorType::Rgba {
166        return Err(anyhow::anyhow!("Image is not RGBA"));
167    }
168    if data.depth != 8 {
169        return Err(anyhow::anyhow!(
170            "RGBA to BGRA conversion only supports 8-bit depth"
171        ));
172    }
173    for i in (0..data.data.len()).step_by(4) {
174        let r = data.data[i];
175        data.data[i] = data.data[i + 2];
176        data.data[i + 2] = r;
177    }
178    data.color_type = ImageColorType::Bgra;
179    Ok(())
180}
181
182/// Encodes an image to the specified format and writes it to a file.
183///
184/// * `data` - The image data to encode.
185/// * `typ` - The output image format.
186/// * `filename` - The path of the file to write the encoded image to.
187/// * `config` - Extra configuration.
188pub fn encode_img(
189    mut data: ImageData,
190    typ: ImageOutputType,
191    filename: &str,
192    config: &ExtraConfig,
193) -> Result<()> {
194    match typ {
195        ImageOutputType::Png => {
196            let mut file = crate::utils::files::write_file(filename)?;
197            let color_type = match data.color_type {
198                ImageColorType::Grayscale => png::ColorType::Grayscale,
199                ImageColorType::Rgb => png::ColorType::Rgb,
200                ImageColorType::Rgba => png::ColorType::Rgba,
201                ImageColorType::Bgr => {
202                    convert_bgr_to_rgb(&mut data)?;
203                    png::ColorType::Rgb
204                }
205                ImageColorType::Bgra => {
206                    convert_bgra_to_rgba(&mut data)?;
207                    png::ColorType::Rgba
208                }
209            };
210            let bit_depth = match &data.depth {
211                1 => png::BitDepth::One,
212                2 => png::BitDepth::Two,
213                4 => png::BitDepth::Four,
214                8 => png::BitDepth::Eight,
215                16 => png::BitDepth::Sixteen,
216                _ => return Err(anyhow::anyhow!("Unsupported bit depth: {}", data.depth)),
217            };
218            let mut encoder = png::Encoder::new(&mut file, data.width, data.height);
219            encoder.set_color(color_type);
220            encoder.set_depth(bit_depth);
221            encoder.set_compression(config.png_compression_level.to_compression());
222            let mut writer = encoder.write_header()?;
223            writer.write_image_data(&data.data)?;
224            writer.finish()?;
225            Ok(())
226        }
227        #[cfg(feature = "image-jpg")]
228        ImageOutputType::Jpg => {
229            let file = crate::utils::files::write_file(filename)?;
230            let color_type = match data.color_type {
231                ImageColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE,
232                ImageColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB,
233                ImageColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA,
234                ImageColorType::Bgr => {
235                    convert_bgr_to_rgb(&mut data)?;
236                    mozjpeg::ColorSpace::JCS_RGB
237                }
238                ImageColorType::Bgra => {
239                    convert_bgra_to_rgba(&mut data)?;
240                    mozjpeg::ColorSpace::JCS_EXT_RGBA
241                }
242            };
243            if data.depth != 8 {
244                return Err(anyhow::anyhow!(
245                    "JPEG encoding only supports 8-bit depth, found: {}",
246                    data.depth
247                ));
248            }
249            let mut encoder = mozjpeg::compress::Compress::new(color_type);
250            encoder.set_size(data.width as usize, data.height as usize);
251            encoder.set_quality(config.jpeg_quality as f32);
252            let mut start = encoder.start_compress(file)?;
253            start.write_scanlines(&data.data)?;
254            start.finish()?;
255            Ok(())
256        }
257        #[cfg(feature = "image-webp")]
258        ImageOutputType::Webp => {
259            let mut file = crate::utils::files::write_file(filename)?;
260            let color_type = match data.color_type {
261                ImageColorType::Rgb => webp::PixelLayout::Rgb,
262                ImageColorType::Rgba => webp::PixelLayout::Rgba,
263                ImageColorType::Bgr => {
264                    convert_bgr_to_rgb(&mut data)?;
265                    webp::PixelLayout::Rgb
266                }
267                ImageColorType::Bgra => {
268                    convert_bgra_to_rgba(&mut data)?;
269                    webp::PixelLayout::Rgba
270                }
271                _ => {
272                    return Err(anyhow::anyhow!(
273                        "Unsupported color type for WebP: {:?}",
274                        data.color_type
275                    ));
276                }
277            };
278            if data.depth != 8 {
279                return Err(anyhow::anyhow!(
280                    "WebP encoding only supports 8-bit depth, found: {}",
281                    data.depth
282                ));
283            }
284            let encoder = webp::Encoder::new(&data.data, color_type, data.width, data.height);
285            let re = encoder
286                .encode_simple(config.webp_lossless, config.webp_quality as f32)
287                .map_err(|e| anyhow::anyhow!("Failed to encode WebP image: {:?}", e))?;
288            file.write_all(&re)?;
289            Ok(())
290        }
291        #[cfg(feature = "image-jxl")]
292        ImageOutputType::Jxl => {
293            let mut file = crate::utils::files::write_file(filename)?;
294            let data = encode_jxl(data, config)?;
295            file.write_all(&data)?;
296            Ok(())
297        }
298    }
299}
300
301/// Loads a PNG image from the given reader and returns its data.
302pub fn load_png<R: std::io::Read + std::io::Seek>(data: R) -> Result<ImageData> {
303    let decoder = png::Decoder::new(std::io::BufReader::new(data));
304    let mut reader = decoder.read_info()?;
305    let bit_depth = match reader.info().bit_depth {
306        png::BitDepth::One => 1,
307        png::BitDepth::Two => 2,
308        png::BitDepth::Four => 4,
309        png::BitDepth::Eight => 8,
310        png::BitDepth::Sixteen => 16,
311    };
312    let color_type = match reader.info().color_type {
313        png::ColorType::Grayscale => ImageColorType::Grayscale,
314        png::ColorType::Rgb => ImageColorType::Rgb,
315        png::ColorType::Rgba => ImageColorType::Rgba,
316        _ => {
317            return Err(anyhow::anyhow!(
318                "Unsupported color type: {:?}",
319                reader.info().color_type
320            ));
321        }
322    };
323    let stride = reader.info().width as usize * color_type.bpp(bit_depth) as usize / 8;
324    let mut data = vec![0; stride * reader.info().height as usize];
325    reader.next_frame(&mut data)?;
326    Ok(ImageData {
327        width: reader.info().width,
328        height: reader.info().height,
329        depth: bit_depth,
330        color_type,
331        data,
332    })
333}
334
335#[cfg(feature = "mozjpeg")]
336pub fn load_jpg<R: std::io::Read>(data: R) -> Result<ImageData> {
337    let decoder = mozjpeg::decompress::Decompress::new_reader(std::io::BufReader::new(data))?;
338    let color_type = match decoder.color_space() {
339        mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale,
340        mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb,
341        mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba,
342        _ => ImageColorType::Rgb, // Convert other types to RGB
343    };
344    let width = decoder.width() as u32;
345    let height = decoder.height() as u32;
346    let stride = width as usize * color_type.bpp(8) as usize / 8;
347    let mut data = vec![0; stride * height as usize];
348    let mut re = match color_type {
349        ImageColorType::Grayscale => decoder.grayscale()?,
350        ImageColorType::Rgb => decoder.rgb()?,
351        ImageColorType::Rgba => decoder.rgba()?,
352        _ => {
353            unreachable!(); // We already checked the color type above
354        }
355    };
356    re.read_scanlines_into(&mut data)?;
357    Ok(ImageData {
358        width,
359        height,
360        depth: 8,
361        color_type,
362        data,
363    })
364}
365
366/// Decodes an image from the specified file path and returns its data.
367///
368/// * `typ` - The type of the image to decode.
369/// * `filename` - The path of the file to decode.
370pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result<ImageData> {
371    match typ {
372        ImageOutputType::Png => {
373            let file = crate::utils::files::read_file(filename)?;
374            let reader = MemReader::new(file);
375            load_png(reader)
376        }
377        #[cfg(feature = "image-jpg")]
378        ImageOutputType::Jpg => {
379            let file = crate::utils::files::read_file(filename)?;
380            load_jpg(&file[..])
381        }
382        #[cfg(feature = "image-webp")]
383        ImageOutputType::Webp => {
384            let file = crate::utils::files::read_file(filename)?;
385            let decoder = webp::Decoder::new(&file);
386            let image = decoder
387                .decode()
388                .ok_or(anyhow::anyhow!("Failed to decode WebP image"))?;
389            let color_type = if image.is_alpha() {
390                ImageColorType::Rgba
391            } else {
392                ImageColorType::Rgb
393            };
394            let width = image.width();
395            let height = image.height();
396            let stride = width as usize * color_type.bpp(8) as usize / 8;
397            let mut data = vec![0; stride * height as usize];
398            if image.len() != data.len() {
399                return Err(anyhow::anyhow!(
400                    "WebP image data size mismatch: expected {}, got {}",
401                    data.len(),
402                    image.len()
403                ));
404            }
405            data.copy_from_slice(&image);
406            Ok(ImageData {
407                width,
408                height,
409                depth: 8,
410                color_type,
411                data,
412            })
413        }
414        #[cfg(feature = "image-jxl")]
415        ImageOutputType::Jxl => {
416            let file = crate::utils::files::read_file(filename)?;
417            decode_jxl(&file[..])
418        }
419    }
420}
421
422/// Draws an image on a canvas with specified offsets.
423///
424/// * `img` - The image data to draw.
425/// * `canvas_width` - The width of the canvas.
426/// * `canvas_height` - The height of the canvas.
427/// * `offset_x` - The horizontal offset to start drawing the image.
428/// * `offset_y` - The vertical offset to start drawing the image.
429///
430/// Returns the canvas image data.
431pub fn draw_on_canvas(
432    img: ImageData,
433    canvas_width: u32,
434    canvas_height: u32,
435    offset_x: u32,
436    offset_y: u32,
437) -> Result<ImageData> {
438    let bytes_per_pixel = img.color_type.bpp(img.depth) as u32 / 8;
439    let mut canvas_data = vec![0u8; (canvas_width * canvas_height * bytes_per_pixel) as usize];
440    let canvas_stride = canvas_width * bytes_per_pixel;
441    let img_stride = img.width * bytes_per_pixel;
442
443    for y in 0..img.height {
444        let canvas_y = y + offset_y;
445        if canvas_y >= canvas_height {
446            continue;
447        }
448        let canvas_start = (canvas_y * canvas_stride + offset_x * bytes_per_pixel) as usize;
449        let img_start = (y * img_stride) as usize;
450        let copy_len = img_stride as usize;
451        if canvas_start + copy_len > canvas_data.len() {
452            continue;
453        }
454        canvas_data[canvas_start..canvas_start + copy_len]
455            .copy_from_slice(&img.data[img_start..img_start + copy_len]);
456    }
457
458    Ok(ImageData {
459        width: canvas_width,
460        height: canvas_height,
461        color_type: img.color_type,
462        depth: img.depth,
463        data: canvas_data,
464    })
465}
466
467/// Flips an image vertically.
468pub fn flip_image(data: &mut ImageData) -> Result<()> {
469    if data.height <= 1 {
470        return Ok(());
471    }
472    let row_size = data.color_type.bpp(data.depth) as usize * data.width as usize / 8;
473    if row_size == 0 {
474        return Ok(());
475    }
476
477    let mut i = 0;
478    let mut j = data.height as usize - 1;
479    while i < j {
480        let (top, bottom) = data.data.split_at_mut(j * row_size);
481        let top_row = &mut top[i * row_size..i * row_size + row_size];
482        let bottom_row = &mut bottom[0..row_size];
483        top_row.swap_with_slice(bottom_row);
484        i += 1;
485        j -= 1;
486    }
487
488    Ok(())
489}
490
491/// Applies opacity to an image.
492///
493/// Only supports RGBA or BGRA images with 8-bit depth.
494pub fn apply_opacity(img: &mut ImageData, opacity: u8) -> Result<()> {
495    if img.color_type != ImageColorType::Rgba && img.color_type != ImageColorType::Bgra {
496        return Err(anyhow::anyhow!("Image is not RGBA or BGRA"));
497    }
498    if img.depth != 8 {
499        return Err(anyhow::anyhow!(
500            "Opacity application only supports 8-bit depth"
501        ));
502    }
503    for i in (0..img.data.len()).step_by(4) {
504        img.data[i + 3] = (img.data[i + 3] as u16 * opacity as u16 / 255) as u8;
505    }
506    Ok(())
507}
508
509/// Draws an image on another image with specified opacity.
510///
511/// * `base` - The base image to draw on.
512/// * `diff` - The image to draw with opacity.
513/// * `left` - The horizontal offset to start drawing the image.
514/// * `top` - The vertical offset to start drawing the image.
515/// * `opacity` - The opacity level to apply to the drawn image (0-255
516pub fn draw_on_img_with_opacity(
517    base: &mut ImageData,
518    diff: &ImageData,
519    left: u32,
520    top: u32,
521    opacity: u8,
522) -> Result<()> {
523    if base.color_type != diff.color_type {
524        return Err(anyhow::anyhow!("Image color types do not match"));
525    }
526    if base.color_type != ImageColorType::Rgba && base.color_type != ImageColorType::Bgra {
527        return Err(anyhow::anyhow!("Images are not RGBA or BGRA"));
528    }
529    if base.depth != 8 || diff.depth != 8 {
530        return Err(anyhow::anyhow!(
531            "Image drawing with opacity only supports 8-bit depth"
532        ));
533    }
534
535    let bpp = 4;
536    let base_stride = base.width as usize * bpp;
537    let diff_stride = diff.width as usize * bpp;
538
539    for y in 0..diff.height {
540        let base_y = top + y;
541        if base_y >= base.height {
542            continue;
543        }
544
545        for x in 0..diff.width {
546            let base_x = left + x;
547            if base_x >= base.width {
548                continue;
549            }
550
551            let diff_idx = (y as usize * diff_stride) + (x as usize * bpp);
552            let base_idx = (base_y as usize * base_stride) + (base_x as usize * bpp);
553
554            let diff_pixel = &diff.data[diff_idx..diff_idx + bpp];
555            let base_pixel_orig = base.data[base_idx..base_idx + bpp].to_vec();
556
557            let src_alpha_u16 = (diff_pixel[3] as u16 * opacity as u16) / 255;
558
559            if src_alpha_u16 == 0 {
560                continue;
561            }
562
563            let dst_alpha_u16 = base_pixel_orig[3] as u16;
564
565            // out_alpha = src_alpha + dst_alpha * (1 - src_alpha)
566            let out_alpha_u16 = src_alpha_u16 + (dst_alpha_u16 * (255 - src_alpha_u16)) / 255;
567
568            if out_alpha_u16 == 0 {
569                for i in 0..4 {
570                    base.data[base_idx + i] = 0;
571                }
572                continue;
573            }
574
575            // out_color = (src_color * src_alpha + dst_color * dst_alpha * (1 - src_alpha)) / out_alpha
576            for i in 0..3 {
577                let src_comp = diff_pixel[i] as u16;
578                let dst_comp = base_pixel_orig[i] as u16;
579
580                let numerator = src_comp * src_alpha_u16
581                    + (dst_comp * dst_alpha_u16 * (255 - src_alpha_u16)) / 255;
582                base.data[base_idx + i] = (numerator / out_alpha_u16) as u8;
583            }
584            base.data[base_idx + 3] = out_alpha_u16 as u8;
585        }
586    }
587
588    Ok(())
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
592pub enum PaletteFormat {
593    /// R G B Color
594    Rgb,
595    /// B G R Color
596    Bgr,
597    /// R G B Color with a unused byte
598    RgbX,
599    /// B G R Color with a unused byte
600    BgrX,
601    /// R G B A Color
602    RgbA,
603    /// B G R A Color
604    BgrA,
605}
606
607/// Converts indexed pixel data and a palette into a standard image buffer.
608///
609/// `pixel_size` is expressed in bits per pixel for the indexed data.
610pub fn convert_index_palette_to_normal_bitmap(
611    pixel_data: &[u8],
612    pixel_size: usize,
613    palettes: &[u8],
614    palette_format: PaletteFormat,
615    width: usize,
616    height: usize,
617) -> Result<ImageData> {
618    if width == 0 || height == 0 {
619        return Err(anyhow::anyhow!("Image dimensions must be non-zero"));
620    }
621    if pixel_size == 0 {
622        return Err(anyhow::anyhow!("pixel_size must be greater than zero"));
623    }
624
625    let width_u32 =
626        u32::try_from(width).map_err(|_| anyhow::anyhow!("width exceeds u32::MAX: {}", width))?;
627    let height_u32 = u32::try_from(height)
628        .map_err(|_| anyhow::anyhow!("height exceeds u32::MAX: {}", height))?;
629
630    let pixel_count = width
631        .checked_mul(height)
632        .ok_or_else(|| anyhow::anyhow!("Image dimensions overflow: {}x{}", width, height))?;
633
634    let palette_entry_size = match palette_format {
635        PaletteFormat::Rgb | PaletteFormat::Bgr => 3usize,
636        PaletteFormat::RgbX | PaletteFormat::BgrX | PaletteFormat::RgbA | PaletteFormat::BgrA => {
637            4usize
638        }
639    };
640
641    if palettes.len() < palette_entry_size {
642        return Err(anyhow::anyhow!("Palette data is too small"));
643    }
644    if palettes.len() % palette_entry_size != 0 {
645        return Err(anyhow::anyhow!(
646            "Palette length {} is not a multiple of {}",
647            palettes.len(),
648            palette_entry_size
649        ));
650    }
651    let palette_color_count = palettes.len() / palette_entry_size;
652    if palette_color_count == 0 {
653        return Err(anyhow::anyhow!("Palette does not contain any colors"));
654    }
655
656    let (color_type, output_channels) = match palette_format {
657        PaletteFormat::Rgb | PaletteFormat::RgbX => (ImageColorType::Rgb, 3usize),
658        PaletteFormat::Bgr | PaletteFormat::BgrX => (ImageColorType::Bgr, 3usize),
659        PaletteFormat::RgbA => (ImageColorType::Rgba, 4usize),
660        PaletteFormat::BgrA => (ImageColorType::Bgra, 4usize),
661    };
662
663    let palette_table_len = palette_color_count
664        .checked_mul(output_channels)
665        .ok_or_else(|| anyhow::anyhow!("Palette size overflow"))?;
666    let mut palette_table = Vec::with_capacity(palette_table_len);
667    for idx in 0..palette_color_count {
668        let base = idx * palette_entry_size;
669        match palette_format {
670            PaletteFormat::Rgb => {
671                palette_table.extend_from_slice(&palettes[base..base + 3]);
672            }
673            PaletteFormat::Bgr => {
674                palette_table.extend_from_slice(&palettes[base..base + 3]);
675            }
676            PaletteFormat::RgbX => {
677                palette_table.extend_from_slice(&palettes[base..base + 3]);
678            }
679            PaletteFormat::BgrX => {
680                palette_table.extend_from_slice(&palettes[base..base + 3]);
681            }
682            PaletteFormat::RgbA => {
683                palette_table.extend_from_slice(&palettes[base..base + 4]);
684            }
685            PaletteFormat::BgrA => {
686                palette_table.extend_from_slice(&palettes[base..base + 4]);
687            }
688        }
689    }
690
691    let total_bits_required = pixel_count
692        .checked_mul(pixel_size)
693        .ok_or_else(|| anyhow::anyhow!("Pixel count overflow for pixel_size {}", pixel_size))?;
694    if total_bits_required > pixel_data.len() * 8 {
695        return Err(anyhow::anyhow!(
696            "Pixel data too short: need {} bits, have {} bits",
697            total_bits_required,
698            pixel_data.len() * 8
699        ));
700    }
701
702    let output_len = pixel_count
703        .checked_mul(output_channels)
704        .ok_or_else(|| anyhow::anyhow!("Output image size overflow"))?;
705    let mut output = Vec::with_capacity(output_len);
706
707    let stride = output_channels;
708    if pixel_size == 8 {
709        if pixel_data.len() < pixel_count {
710            return Err(anyhow::anyhow!(
711                "Pixel data too short: expected {} bytes, got {}",
712                pixel_count,
713                pixel_data.len()
714            ));
715        }
716        for &index in pixel_data.iter().take(pixel_count) {
717            let idx = index as usize;
718            if idx >= palette_color_count {
719                return Err(anyhow::anyhow!(
720                    "Palette index {} exceeds palette size {}",
721                    idx,
722                    palette_color_count
723                ));
724            }
725            let start = idx * stride;
726            output.extend_from_slice(&palette_table[start..start + stride]);
727        }
728    } else {
729        let mut bit_offset = 0usize;
730        for _ in 0..pixel_count {
731            let idx = read_bits_as_usize(pixel_data, bit_offset, pixel_size)?;
732            bit_offset = bit_offset
733                .checked_add(pixel_size)
734                .ok_or_else(|| anyhow::anyhow!("Bit offset overflow"))?;
735            if idx >= palette_color_count {
736                return Err(anyhow::anyhow!(
737                    "Palette index {} exceeds palette size {}",
738                    idx,
739                    palette_color_count
740                ));
741            }
742            let start = idx * stride;
743            output.extend_from_slice(&palette_table[start..start + stride]);
744        }
745    }
746
747    Ok(ImageData {
748        width: width_u32,
749        height: height_u32,
750        color_type,
751        depth: 8,
752        data: output,
753    })
754}
755
756fn read_bits_as_usize(data: &[u8], bit_offset: usize, bit_len: usize) -> Result<usize> {
757    if bit_len == 0 {
758        return Err(anyhow::anyhow!("bit_len must be greater than zero"));
759    }
760    if bit_len > (std::mem::size_of::<usize>() * 8) {
761        return Err(anyhow::anyhow!("Cannot read {} bits into usize", bit_len));
762    }
763
764    let mut value = 0usize;
765    for bit_idx in 0..bit_len {
766        let absolute_bit = bit_offset + bit_idx;
767        let byte_index = absolute_bit / 8;
768        if byte_index >= data.len() {
769            return Err(anyhow::anyhow!(
770                "Bit offset {} exceeds pixel data",
771                absolute_bit
772            ));
773        }
774        let bit_in_byte = 7 - (absolute_bit % 8);
775        let bit = (data[byte_index] >> bit_in_byte) & 1;
776        value = (value << 1) | bit as usize;
777    }
778    Ok(value)
779}