⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/video/
decode.rs

1//! Image format decoders (TGA, QOI)
2//!
3//! Provides decoders for Truevision TGA and Quite OK Image (QOI) formats.
4//! Both decoders produce `VideoFrame` output using the parent module's
5//! `PixelFormat::Argb8888` for maximum fidelity.
6
7#![allow(dead_code, clippy::upper_case_acronyms)]
8
9use alloc::vec::Vec;
10
11use super::VideoFrame;
12use crate::{error::KernelError, graphics::PixelFormat};
13
14// ---------------------------------------------------------------------------
15// Format detection
16// ---------------------------------------------------------------------------
17
18/// Supported image formats the decoder can handle.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub(crate) enum ImageFormat {
21    TGA,
22    QOI,
23    PPM,
24    BMP,
25    Unknown,
26}
27
28/// Detect the image format from the file header / magic bytes.
29pub(crate) fn detect_format(data: &[u8]) -> ImageFormat {
30    if data.len() < 4 {
31        return ImageFormat::Unknown;
32    }
33
34    // QOI: starts with "qoif" (0x716F6966)
35    if data[0] == b'q' && data[1] == b'o' && data[2] == b'i' && data[3] == b'f' {
36        return ImageFormat::QOI;
37    }
38
39    // BMP: starts with "BM" (0x42 0x4D)
40    if data[0] == 0x42 && data[1] == 0x4D {
41        return ImageFormat::BMP;
42    }
43
44    // PPM: starts with "P3" or "P6"
45    if data[0] == b'P' && (data[1] == b'3' || data[1] == b'6') {
46        return ImageFormat::PPM;
47    }
48
49    // TGA has no reliable magic bytes. We apply heuristic checks on the header
50    // fields when the data is long enough to contain a TGA header (18 bytes).
51    if data.len() >= 18 {
52        let color_map_type = data[1];
53        let image_type = data[2];
54
55        // color_map_type must be 0 or 1
56        let valid_cmt = color_map_type <= 1;
57        // image_type must be one of the known TGA types
58        let valid_type = matches!(image_type, 0 | 1 | 2 | 3 | 9 | 10 | 11);
59        // pixel depth must be a standard value
60        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// ---------------------------------------------------------------------------
72// TGA decoder
73// ---------------------------------------------------------------------------
74
75/// TGA file header (18 bytes).
76#[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
114/// Decode a TGA image.
115///
116/// Supports:
117/// - Type 2: uncompressed true-color (24-bit and 32-bit)
118/// - Type 10: RLE-compressed true-color (24-bit and 32-bit)
119/// - Bottom-left (default) and top-left origin (bit 5 of image_descriptor)
120pub(crate) fn decode_tga(data: &[u8]) -> Result<VideoFrame, KernelError> {
121    let header = TgaHeader::parse(data)?;
122
123    // Validate image type
124    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    // Validate pixel depth
132    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    // Offset past header + image ID field
153    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    // Decode pixels into a flat RGBA buffer (row-major, top-to-bottom).
162    // We'll handle origin flipping at the end.
163    let mut pixels: Vec<(u8, u8, u8, u8)> = Vec::with_capacity(pixel_count);
164
165    if header.image_type == 2 {
166        // Uncompressed
167        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        // RLE (type 10)
183        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                // RLE packet: one pixel repeated `count` times
191                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                // Raw packet: `count` literal pixels
204                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    // Origin: bit 5 of image_descriptor => 1 = top-left, 0 = bottom-left
224    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
243/// Read a single TGA pixel in BGR(A) order.
244fn read_tga_pixel(data: &[u8], offset: usize, bpp: usize) -> (u8, u8, u8, u8) {
245    // TGA stores pixels as B, G, R [, A]
246    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
253// ---------------------------------------------------------------------------
254// QOI decoder (Quite OK Image format)
255// ---------------------------------------------------------------------------
256
257/// QOI operation tags.
258const QOI_OP_RGB: u8 = 0xFE;
259const QOI_OP_RGBA: u8 = 0xFF;
260const QOI_OP_INDEX_MASK: u8 = 0x00; // 2-bit tag: 00xxxxxx
261const QOI_OP_DIFF_MASK: u8 = 0x40; // 2-bit tag: 01xxxxxx
262const QOI_OP_LUMA_MASK: u8 = 0x80; // 2-bit tag: 10xxxxxx
263const QOI_OP_RUN_MASK: u8 = 0xC0; // 2-bit tag: 11xxxxxx
264
265/// QOI hash function: (r * 3 + g * 5 + b * 7 + a * 11) % 64
266#[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
271/// Decode a QOI (Quite OK Image) file.
272///
273/// QOI specification: <https://qoiformat.org/qoi-specification.pdf>
274///
275/// Header: "qoif" (4B), width (u32 BE), height (u32 BE), channels (u8),
276/// colorspace (u8) End marker: 7 zero bytes + 0x01
277pub(crate) fn decode_qoi(data: &[u8]) -> Result<VideoFrame, KernelError> {
278    // Minimum: 14 byte header + 8 byte end marker
279    if data.len() < 22 {
280        return Err(KernelError::InvalidArgument {
281            name: "data",
282            value: "QOI data too short",
283        });
284    }
285
286    // Check magic
287    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    // Previously seen pixel array (64 entries)
317    let mut index: [(u8, u8, u8, u8); 64] = [(0, 0, 0, 0); 64];
318
319    // Current pixel (starts as r=0, g=0, b=0, a=255 per spec)
320    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; // past header
326    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            // RGB literal
333            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            // RGBA literal
342            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                    // QOI_OP_INDEX: 00xxxxxx
355                    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                    // QOI_OP_DIFF: 01drr dgg dbb
365                    // dr, dg, db are stored with bias of 2: actual = stored - 2
366                    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                    // QOI_OP_LUMA: 10dddddd followed by one byte: drdg(4) dbdg(4)
376                    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                    // QOI_OP_RUN: 11rrrrrr, run length = (rr & 0x3F) + 1 (1..62)
392                    let run = (b1 & 0x3F) as usize + 1;
393                    // Write `run` copies of the current pixel
394                    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                    // Update the index for the run pixel
404                    index[qoi_hash(pr, pg, pb, pa)] = (pr, pg, pb, pa);
405                    continue; // already wrote pixels
406                }
407                _ => {
408                    pos += 1;
409                    continue;
410                }
411            }
412        }
413
414        // Store current pixel in index
415        index[qoi_hash(pr, pg, pb, pa)] = (pr, pg, pb, pa);
416
417        // Write one pixel
418        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
429// ---------------------------------------------------------------------------
430// Unified decoder
431// ---------------------------------------------------------------------------
432
433/// Auto-detect the image format and decode it.
434///
435/// Supports TGA, QOI.  PPM and BMP are detected but not decoded here
436/// (use the desktop image_viewer for those).
437pub(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
453// ---------------------------------------------------------------------------
454// Byte-reading helpers
455// ---------------------------------------------------------------------------
456
457/// Read a big-endian u32.
458fn 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
465/// Read a little-endian u16.
466fn read_le_u16(data: &[u8], off: usize) -> u16 {
467    (data[off] as u16) | ((data[off + 1] as u16) << 8)
468}
469
470// ---------------------------------------------------------------------------
471// Tests
472// ---------------------------------------------------------------------------
473
474#[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        // Minimal valid TGA header for a 2x2, 24-bit, uncompressed image
510        let mut header_data = vec![0u8; 18];
511        header_data[2] = 2; // image type: uncompressed true-color
512                            // width = 2 (LE)
513        header_data[12] = 2;
514        header_data[13] = 0;
515        // height = 2 (LE)
516        header_data[14] = 2;
517        header_data[15] = 0;
518        // pixel depth = 24
519        header_data[16] = 24;
520        // image descriptor: bit 5 set = top-left origin
521        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        // Build a minimal 2x2, 24-bit, uncompressed, top-left origin TGA
534        let mut data = vec![0u8; 18 + 2 * 2 * 3];
535        data[2] = 2; // uncompressed true-color
536        data[12] = 2;
537        data[13] = 0; // width=2
538        data[14] = 2;
539        data[15] = 0; // height=2
540        data[16] = 24; // 24 bpp
541        data[17] = 0x20; // top-left origin
542
543        // Pixel data (BGR)
544        let pixels = &mut data[18..];
545        // row 0: (0,0)=red, (1,0)=green
546        pixels[0] = 0;
547        pixels[1] = 0;
548        pixels[2] = 255; // BGR -> R=255
549        pixels[3] = 0;
550        pixels[4] = 255;
551        pixels[5] = 0; // BGR -> G=255
552                       // row 1: (0,1)=blue, (1,1)=white
553        pixels[6] = 255;
554        pixels[7] = 0;
555        pixels[8] = 0; // BGR -> B=255
556        pixels[9] = 255;
557        pixels[10] = 255;
558        pixels[11] = 255; // white
559
560        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)); // red
564        assert_eq!(frame.get_pixel(1, 0), (0, 255, 0, 255)); // green
565        assert_eq!(frame.get_pixel(0, 1), (0, 0, 255, 255)); // blue
566        assert_eq!(frame.get_pixel(1, 1), (255, 255, 255, 255)); // white
567    }
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        // "qoix" is not QOI magic
576        assert_ne!(detect_format(bad), ImageFormat::QOI);
577    }
578
579    #[test]
580    fn test_format_detection_priority() {
581        // BMP should be detected before TGA heuristic
582        let mut bmp = vec![0u8; 54];
583        bmp[0] = 0x42;
584        bmp[1] = 0x4D;
585        // Even if TGA heuristic could match, BMP should win
586        assert_eq!(detect_format(&bmp), ImageFormat::BMP);
587    }
588}