Skip to main content

blp/core/decode/
jpeg.rs

1use crate::core::image::ImageBlp;
2use crate::error::error::BlpError;
3use image::{Rgba, RgbaImage};
4use jpeg_decoder::{Decoder, PixelFormat};
5use std::io::Cursor;
6
7impl ImageBlp {
8    /// JPEG path:
9    /// - Takes a shared JPEG header from `[header_offset .. header_offset+header_length)`.
10    /// - For each mip, concatenates `[header][tail]` → full JPEG, then decodes it.
11    /// - Respects `mip_visible`: if `mip_visible[i] == false`, the mip is skipped (image stays `None`).
12    ///   Missing indices in `mip_visible` are treated as `true`.
13    pub fn decode_jpeg(&mut self, buf: &[u8], mip_visible: &[bool]) -> Result<(), BlpError> {
14        // --- Validate header range and slice it out ---
15        let h_off = self.header_offset;
16        let h_len = self.header_length;
17        if h_off.checked_add(h_len).is_none() || h_off + h_len > buf.len() {
18            return Err(BlpError::new("jpeg.header.oob"));
19        }
20        let header_bytes = &buf[h_off..h_off + h_len];
21
22        // If alpha_bits == 0 we force opaque alpha channel when reconstructing RGBA.
23        let force_opaque = self.alpha_bits == 0;
24
25        // --- Walk over mip chain ---
26        for i in 0..self.mipmaps.len() {
27            // Visibility gate: missing entry → treated as `true`.
28            let visible = mip_visible
29                .get(i)
30                .copied()
31                .unwrap_or(true);
32            if !visible {
33                // Do not materialize pixels for this mip.
34                self.mipmaps[i].image = None;
35                continue;
36            }
37
38            let off = self.mipmaps[i].offset;
39            let len = self.mipmaps[i].length;
40
41            // Skip empty mips or invalid ranges safely.
42            if len == 0 {
43                continue;
44            }
45            if off.checked_add(len).is_none() || off + len > buf.len() {
46                continue;
47            }
48
49            // --- Build a full JPEG stream: [shared header][tail for this mip] ---
50            let tail = &buf[off..off + len];
51            let mut full = Vec::with_capacity(header_bytes.len() + tail.len());
52            full.extend_from_slice(header_bytes);
53            full.extend_from_slice(tail);
54
55            // --- Decode JPEG ---
56            let mut dec = Decoder::new(Cursor::new(&full));
57            dec.read_info().map_err(|e| {
58                BlpError::from(e)
59                    .with_arg("phase", "read_info")
60                    .with_arg("mip", i as u32)
61            })?;
62
63            let info = dec
64                .info()
65                .ok_or_else(|| BlpError::new("jpeg.meta.missing").with_arg("mip", i as u32))?;
66
67            let (w, h) = (info.width as u32, info.height as u32);
68            let pixels = dec.decode().map_err(|e| {
69                BlpError::from(e)
70                    .with_arg("phase", "decode")
71                    .with_arg("mip", i as u32)
72            })?;
73
74            // --- Reconstruct RGBA ---
75            let mut img = RgbaImage::new(w, h);
76            match info.pixel_format {
77                PixelFormat::CMYK32 => {
78                    // Expect 4 bytes per pixel: C, M, Y, K
79                    if pixels.len() != (w as usize * h as usize * 4) {
80                        return Err(BlpError::new("jpeg.size.mismatch")
81                            .with_arg("fmt", "CMYK32")
82                            .with_arg("mip", i as u32));
83                    }
84                    for (p, px) in img.pixels_mut().enumerate() {
85                        let idx = p * 4;
86                        let c = pixels[idx + 0];
87                        let m = pixels[idx + 1];
88                        let y = pixels[idx + 2];
89                        let k = pixels[idx + 3];
90                        // Alpha from K (unless forced opaque). Colors inverted from CMY.
91                        let a = if force_opaque { 255 } else { 255u8.saturating_sub(k) };
92                        *px = Rgba([
93                            255u8.saturating_sub(y), // R
94                            255u8.saturating_sub(m), // G
95                            255u8.saturating_sub(c), // B
96                            a,
97                        ]);
98                    }
99                }
100                PixelFormat::RGB24 => {
101                    // Expect 3 bytes per pixel
102                    if pixels.len() != (w as usize * h as usize * 3) {
103                        return Err(BlpError::new("jpeg.size.mismatch")
104                            .with_arg("fmt", "RGB24")
105                            .with_arg("mip", i as u32));
106                    }
107
108                    // Fast path (no color transform): pixels are B,G,R in this decoder layout
109                    if option_env!("NEVER").is_none() {
110                        for (p, px) in img.pixels_mut().enumerate() {
111                            let idx = p * 3;
112                            *px = Rgba([
113                                pixels[idx + 2], // R
114                                pixels[idx + 1], // G
115                                pixels[idx + 0], // B
116                                255,
117                            ]);
118                        }
119                    } else {
120                        // Alternative path if you want to pack as YCbCr (kept from your code)
121                        for (p, px) in img.pixels_mut().enumerate() {
122                            let idx = p * 3;
123                            let (r, g, b) = (
124                                pixels[idx + 2] as f32, //
125                                pixels[idx + 1] as f32,
126                                pixels[idx + 0] as f32,
127                            );
128                            let y = (0.2990 * r + 0.5870 * g + 0.1140 * b)
129                                .round()
130                                .clamp(0.0, 255.0) as u8;
131                            let cb = (128.0 - 0.168736 * r - 0.331264 * g + 0.5 * b)
132                                .round()
133                                .clamp(0.0, 255.0) as u8;
134                            let cr = (128.0 + 0.5 * r - 0.418688 * g - 0.081312 * b)
135                                .round()
136                                .clamp(0.0, 255.0) as u8;
137
138                            *px = Rgba([cb, cr, y, 255]);
139                        }
140                    }
141                }
142                PixelFormat::L8 => {
143                    // 1 byte per pixel (luminance)
144                    if pixels.len() != (w as usize * h as usize) {
145                        return Err(BlpError::new("jpeg.size.mismatch")
146                            .with_arg("fmt", "L8")
147                            .with_arg("mip", i as u32));
148                    }
149                    for (p, px) in img.pixels_mut().enumerate() {
150                        let l = pixels[p];
151                        *px = Rgba([l, l, l, 255]);
152                    }
153                }
154                PixelFormat::L16 => {
155                    // 2 bytes per pixel (big-endian luminance)
156                    if pixels.len() != (w as usize * h as usize * 2) {
157                        return Err(BlpError::new("jpeg.size.mismatch")
158                            .with_arg("fmt", "L16")
159                            .with_arg("mip", i as u32));
160                    }
161                    for (chunk, px) in pixels
162                        .chunks_exact(2)
163                        .zip(img.pixels_mut())
164                    {
165                        let l16 = u16::from_be_bytes([chunk[0], chunk[1]]);
166                        let l8 = (l16 / 257) as u8; // downscale 16→8
167                        *px = Rgba([l8, l8, l8, 255]);
168                    }
169                }
170            }
171
172            // --- Store image into the matching mip level ---
173            if self.mipmaps[i].width == w && self.mipmaps[i].height == h {
174                self.mipmaps[i].image = Some(img);
175            } else if let Some(level) = (0..self.mipmaps.len()).find(|&lvl| self.mipmaps[lvl].width == w && self.mipmaps[lvl].height == h) {
176                self.mipmaps[level].image = Some(img);
177            }
178        }
179
180        Ok(())
181    }
182}