1#![allow(dead_code, clippy::upper_case_acronyms)]
8
9use alloc::vec::Vec;
10
11use super::VideoFrame;
12use crate::{error::KernelError, graphics::PixelFormat};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub(crate) enum ImageFormat {
21 TGA,
22 QOI,
23 PPM,
24 BMP,
25 Unknown,
26}
27
28pub(crate) fn detect_format(data: &[u8]) -> ImageFormat {
30 if data.len() < 4 {
31 return ImageFormat::Unknown;
32 }
33
34 if data[0] == b'q' && data[1] == b'o' && data[2] == b'i' && data[3] == b'f' {
36 return ImageFormat::QOI;
37 }
38
39 if data[0] == 0x42 && data[1] == 0x4D {
41 return ImageFormat::BMP;
42 }
43
44 if data[0] == b'P' && (data[1] == b'3' || data[1] == b'6') {
46 return ImageFormat::PPM;
47 }
48
49 if data.len() >= 18 {
52 let color_map_type = data[1];
53 let image_type = data[2];
54
55 let valid_cmt = color_map_type <= 1;
57 let valid_type = matches!(image_type, 0 | 1 | 2 | 3 | 9 | 10 | 11);
59 let pixel_depth = data[16];
61 let valid_depth = matches!(pixel_depth, 8 | 15 | 16 | 24 | 32);
62
63 if valid_cmt && valid_type && valid_depth && image_type != 0 {
64 return ImageFormat::TGA;
65 }
66 }
67
68 ImageFormat::Unknown
69}
70
71#[derive(Debug, Clone, Copy)]
77pub(crate) struct TgaHeader {
78 pub(crate) id_length: u8,
79 pub(crate) color_map_type: u8,
80 pub(crate) image_type: u8,
81 pub(crate) color_map_spec: [u8; 5],
82 pub(crate) x_origin: u16,
83 pub(crate) y_origin: u16,
84 pub(crate) width: u16,
85 pub(crate) height: u16,
86 pub(crate) pixel_depth: u8,
87 pub(crate) image_descriptor: u8,
88}
89
90impl TgaHeader {
91 fn parse(data: &[u8]) -> Result<Self, KernelError> {
92 if data.len() < 18 {
93 return Err(KernelError::InvalidArgument {
94 name: "data",
95 value: "TGA data too short for header",
96 });
97 }
98
99 Ok(Self {
100 id_length: data[0],
101 color_map_type: data[1],
102 image_type: data[2],
103 color_map_spec: [data[3], data[4], data[5], data[6], data[7]],
104 x_origin: read_le_u16(data, 8),
105 y_origin: read_le_u16(data, 10),
106 width: read_le_u16(data, 12),
107 height: read_le_u16(data, 14),
108 pixel_depth: data[16],
109 image_descriptor: data[17],
110 })
111 }
112}
113
114pub(crate) fn decode_tga(data: &[u8]) -> Result<VideoFrame, KernelError> {
121 let header = TgaHeader::parse(data)?;
122
123 if header.image_type != 2 && header.image_type != 10 {
125 return Err(KernelError::InvalidArgument {
126 name: "image_type",
127 value: "only uncompressed (2) and RLE (10) true-color supported",
128 });
129 }
130
131 let bpp = header.pixel_depth;
133 if bpp != 24 && bpp != 32 {
134 return Err(KernelError::InvalidArgument {
135 name: "pixel_depth",
136 value: "only 24-bit and 32-bit TGA supported",
137 });
138 }
139
140 let width = header.width as u32;
141 let height = header.height as u32;
142 if width == 0 || height == 0 {
143 return Err(KernelError::InvalidArgument {
144 name: "dimensions",
145 value: "zero width or height",
146 });
147 }
148
149 let bytes_per_pixel = (bpp / 8) as usize;
150 let pixel_count = (width as usize) * (height as usize);
151
152 let pixel_data_start = 18 + header.id_length as usize;
154 if pixel_data_start > data.len() {
155 return Err(KernelError::InvalidArgument {
156 name: "data",
157 value: "TGA data truncated before pixel data",
158 });
159 }
160
161 let mut pixels: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(pixel_count);
164
165 if header.image_type == 2 {
166 let needed = pixel_data_start + pixel_count * bytes_per_pixel;
168 if data.len() < needed {
169 return Err(KernelError::InvalidArgument {
170 name: "data",
171 value: "TGA uncompressed data truncated",
172 });
173 }
174
175 let mut pos = pixel_data_start;
176 for _ in 0..pixel_count {
177 let (r, g, b, a) = read_tga_pixel(data, pos, bytes_per_pixel);
178 pixels.push((r, g, b, a));
179 pos += bytes_per_pixel;
180 }
181 } else {
182 let mut pos = pixel_data_start;
184 while pixels.len() < pixel_count && pos < data.len() {
185 let packet = data[pos];
186 pos += 1;
187 let count = (packet & 0x7F) as usize + 1;
188
189 if packet & 0x80 != 0 {
190 if pos + bytes_per_pixel > data.len() {
192 break;
193 }
194 let (r, g, b, a) = read_tga_pixel(data, pos, bytes_per_pixel);
195 pos += bytes_per_pixel;
196 for _ in 0..count {
197 if pixels.len() >= pixel_count {
198 break;
199 }
200 pixels.push((r, g, b, a));
201 }
202 } else {
203 for _ in 0..count {
205 if pixels.len() >= pixel_count || pos + bytes_per_pixel > data.len() {
206 break;
207 }
208 let (r, g, b, a) = read_tga_pixel(data, pos, bytes_per_pixel);
209 pixels.push((r, g, b, a));
210 pos += bytes_per_pixel;
211 }
212 }
213 }
214 }
215
216 if pixels.len() < pixel_count {
217 return Err(KernelError::InvalidArgument {
218 name: "data",
219 value: "TGA pixel data incomplete",
220 });
221 }
222
223 let top_left_origin = (header.image_descriptor & 0x20) != 0;
225
226 let mut frame = VideoFrame::new(width, height, PixelFormat::Argb8888);
227 for row in 0..height {
228 let src_row = if top_left_origin {
229 row
230 } else {
231 height - 1 - row
232 };
233 for col in 0..width {
234 let idx = (src_row as usize) * (width as usize) + (col as usize);
235 let (r, g, b, a) = pixels[idx];
236 frame.set_pixel(col, row, r, g, b, a);
237 }
238 }
239
240 Ok(frame)
241}
242
243fn read_tga_pixel(data: &[u8], offset: usize, bpp: usize) -> (u8, u8, u8, u8) {
245 let b = data[offset];
247 let g = data[offset + 1];
248 let r = data[offset + 2];
249 let a = if bpp >= 4 { data[offset + 3] } else { 0xFF };
250 (r, g, b, a)
251}
252
253const QOI_OP_RGB: u8 = 0xFE;
259const QOI_OP_RGBA: u8 = 0xFF;
260const QOI_OP_INDEX_MASK: u8 = 0x00; const QOI_OP_DIFF_MASK: u8 = 0x40; const QOI_OP_LUMA_MASK: u8 = 0x80; const QOI_OP_RUN_MASK: u8 = 0xC0; #[inline]
267fn qoi_hash(r: u8, g: u8, b: u8, a: u8) -> usize {
268 ((r as usize) * 3 + (g as usize) * 5 + (b as usize) * 7 + (a as usize) * 11) % 64
269}
270
271pub(crate) fn decode_qoi(data: &[u8]) -> Result<VideoFrame, KernelError> {
278 if data.len() < 22 {
280 return Err(KernelError::InvalidArgument {
281 name: "data",
282 value: "QOI data too short",
283 });
284 }
285
286 if data[0] != b'q' || data[1] != b'o' || data[2] != b'i' || data[3] != b'f' {
288 return Err(KernelError::InvalidArgument {
289 name: "magic",
290 value: "not a QOI file",
291 });
292 }
293
294 let width = read_be_u32(data, 4);
295 let height = read_be_u32(data, 8);
296 let channels = data[12];
297 let _colorspace = data[13];
298
299 if width == 0 || height == 0 {
300 return Err(KernelError::InvalidArgument {
301 name: "dimensions",
302 value: "zero width or height",
303 });
304 }
305
306 if channels != 3 && channels != 4 {
307 return Err(KernelError::InvalidArgument {
308 name: "channels",
309 value: "must be 3 or 4",
310 });
311 }
312
313 let pixel_count = (width as usize) * (height as usize);
314 let mut frame = VideoFrame::new(width, height, PixelFormat::Argb8888);
315
316 let mut index: [(u8, u8, u8, u8); 64] = [(0, 0, 0, 0); 64];
318
319 let mut pr: u8 = 0;
321 let mut pg: u8 = 0;
322 let mut pb: u8 = 0;
323 let mut pa: u8 = 255;
324
325 let mut pos: usize = 14; let mut px_idx: usize = 0;
327
328 while px_idx < pixel_count && pos < data.len() {
329 let b1 = data[pos];
330
331 if b1 == QOI_OP_RGB {
332 if pos + 3 >= data.len() {
334 break;
335 }
336 pr = data[pos + 1];
337 pg = data[pos + 2];
338 pb = data[pos + 3];
339 pos += 4;
340 } else if b1 == QOI_OP_RGBA {
341 if pos + 4 >= data.len() {
343 break;
344 }
345 pr = data[pos + 1];
346 pg = data[pos + 2];
347 pb = data[pos + 3];
348 pa = data[pos + 4];
349 pos += 5;
350 } else {
351 let tag = b1 & 0xC0;
352 match tag {
353 0x00 => {
354 let idx = (b1 & 0x3F) as usize;
356 let (ir, ig, ib, ia) = index[idx];
357 pr = ir;
358 pg = ig;
359 pb = ib;
360 pa = ia;
361 pos += 1;
362 }
363 0x40 => {
364 let dr = ((b1 >> 4) & 0x03) as i8 - 2;
367 let dg = ((b1 >> 2) & 0x03) as i8 - 2;
368 let db = (b1 & 0x03) as i8 - 2;
369 pr = pr.wrapping_add(dr as u8);
370 pg = pg.wrapping_add(dg as u8);
371 pb = pb.wrapping_add(db as u8);
372 pos += 1;
373 }
374 0x80 => {
375 if pos + 1 >= data.len() {
377 break;
378 }
379 let b2 = data[pos + 1];
380 let dg = (b1 & 0x3F) as i8 - 32;
381 let dr_dg = ((b2 >> 4) & 0x0F) as i8 - 8;
382 let db_dg = (b2 & 0x0F) as i8 - 8;
383 let dr = (dr_dg + dg) as u8;
384 let db = (db_dg + dg) as u8;
385 pr = pr.wrapping_add(dr);
386 pg = pg.wrapping_add(dg as u8);
387 pb = pb.wrapping_add(db);
388 pos += 2;
389 }
390 0xC0 => {
391 let run = (b1 & 0x3F) as usize + 1;
393 for _ in 0..run {
395 if px_idx >= pixel_count {
396 break;
397 }
398 let x = (px_idx % width as usize) as u32;
399 let y = (px_idx / width as usize) as u32;
400 frame.set_pixel(x, y, pr, pg, pb, pa);
401 px_idx += 1;
402 }
403 index[qoi_hash(pr, pg, pb, pa)] = (pr, pg, pb, pa);
405 continue; }
407 _ => {
408 pos += 1;
409 continue;
410 }
411 }
412 }
413
414 index[qoi_hash(pr, pg, pb, pa)] = (pr, pg, pb, pa);
416
417 if px_idx < pixel_count {
419 let x = (px_idx % width as usize) as u32;
420 let y = (px_idx / width as usize) as u32;
421 frame.set_pixel(x, y, pr, pg, pb, pa);
422 px_idx += 1;
423 }
424 }
425
426 Ok(frame)
427}
428
429pub(crate) fn decode_image(data: &[u8]) -> Result<VideoFrame, KernelError> {
438 let fmt = detect_format(data);
439 match fmt {
440 ImageFormat::TGA => decode_tga(data),
441 ImageFormat::QOI => decode_qoi(data),
442 ImageFormat::PPM | ImageFormat::BMP => Err(KernelError::InvalidArgument {
443 name: "format",
444 value: "PPM/BMP should be decoded via desktop::image_viewer",
445 }),
446 ImageFormat::Unknown => Err(KernelError::InvalidArgument {
447 name: "format",
448 value: "unknown or unsupported image format",
449 }),
450 }
451}
452
453fn read_be_u32(data: &[u8], off: usize) -> u32 {
459 ((data[off] as u32) << 24)
460 | ((data[off + 1] as u32) << 16)
461 | ((data[off + 2] as u32) << 8)
462 | (data[off + 3] as u32)
463}
464
465fn read_le_u16(data: &[u8], off: usize) -> u16 {
467 (data[off] as u16) | ((data[off + 1] as u16) << 8)
468}
469
470#[cfg(test)]
475mod tests {
476 #[allow(unused_imports)]
477 use alloc::vec;
478
479 use super::*;
480
481 #[test]
482 fn test_detect_qoi() {
483 let data = b"qoif\x00\x00\x00\x01\x00\x00\x00\x01\x04\x00extra";
484 assert_eq!(detect_format(data), ImageFormat::QOI);
485 }
486
487 #[test]
488 fn test_detect_bmp() {
489 let mut data = vec![0u8; 54];
490 data[0] = 0x42;
491 data[1] = 0x4D;
492 assert_eq!(detect_format(&data), ImageFormat::BMP);
493 }
494
495 #[test]
496 fn test_detect_ppm() {
497 let data = b"P6\n10 10\n255\n";
498 assert_eq!(detect_format(data), ImageFormat::PPM);
499 }
500
501 #[test]
502 fn test_detect_unknown() {
503 let data = b"\x00\x00\x00\x00";
504 assert_eq!(detect_format(data), ImageFormat::Unknown);
505 }
506
507 #[test]
508 fn test_tga_header_parse() {
509 let mut header_data = vec![0u8; 18];
511 header_data[2] = 2; header_data[12] = 2;
514 header_data[13] = 0;
515 header_data[14] = 2;
517 header_data[15] = 0;
518 header_data[16] = 24;
520 header_data[17] = 0x20;
522
523 let hdr = TgaHeader::parse(&header_data).expect("parse should succeed");
524 assert_eq!(hdr.width, 2);
525 assert_eq!(hdr.height, 2);
526 assert_eq!(hdr.pixel_depth, 24);
527 assert_eq!(hdr.image_type, 2);
528 assert_ne!(hdr.image_descriptor & 0x20, 0);
529 }
530
531 #[test]
532 fn test_decode_tga_uncompressed_24() {
533 let mut data = vec![0u8; 18 + 2 * 2 * 3];
535 data[2] = 2; data[12] = 2;
537 data[13] = 0; data[14] = 2;
539 data[15] = 0; data[16] = 24; data[17] = 0x20; let pixels = &mut data[18..];
545 pixels[0] = 0;
547 pixels[1] = 0;
548 pixels[2] = 255; pixels[3] = 0;
550 pixels[4] = 255;
551 pixels[5] = 0; pixels[6] = 255;
554 pixels[7] = 0;
555 pixels[8] = 0; pixels[9] = 255;
557 pixels[10] = 255;
558 pixels[11] = 255; let frame = decode_tga(&data).expect("decode should succeed");
561 assert_eq!(frame.width, 2);
562 assert_eq!(frame.height, 2);
563 assert_eq!(frame.get_pixel(0, 0), (255, 0, 0, 255)); assert_eq!(frame.get_pixel(1, 0), (0, 255, 0, 255)); assert_eq!(frame.get_pixel(0, 1), (0, 0, 255, 255)); assert_eq!(frame.get_pixel(1, 1), (255, 255, 255, 255)); }
568
569 #[test]
570 fn test_qoi_magic_detection() {
571 let good = b"qoif\x00\x00\x00\x01\x00\x00\x00\x01\x03\x00";
572 assert_eq!(detect_format(good), ImageFormat::QOI);
573
574 let bad = b"qoix\x00\x00\x00\x01\x00\x00\x00\x01\x03\x00";
575 assert_ne!(detect_format(bad), ImageFormat::QOI);
577 }
578
579 #[test]
580 fn test_format_detection_priority() {
581 let mut bmp = vec![0u8; 54];
583 bmp[0] = 0x42;
584 bmp[1] = 0x4D;
585 assert_eq!(detect_format(&bmp), ImageFormat::BMP);
587 }
588}