Skip to main content

dicom_toolkit_codec/jpeg/
decoder.rs

1//! JPEG baseline/extended decoder wrapping `jpeg-decoder`.
2//!
3//! Decodes a single JPEG-compressed fragment into raw pixel bytes.
4
5use dicom_toolkit_core::error::{DcmError, DcmResult};
6use jpeg_decoder::{Decoder, ImageInfo, PixelFormat};
7
8/// Decoded JPEG frame.
9#[derive(Debug)]
10pub struct JpegFrame {
11    pub width: u16,
12    pub height: u16,
13    pub samples_per_pixel: u8,
14    /// Raw pixel bytes in native order (grayscale L8, RGB24, or CMYK32).
15    pub data: Vec<u8>,
16}
17
18/// Decode a single JPEG fragment from a byte slice.
19///
20/// Handles JPEG Baseline (Process 1) and JPEG Extended (Process 2 & 4).
21/// Returns the decoded frame with metadata.
22pub fn decode_jpeg(data: &[u8]) -> DcmResult<JpegFrame> {
23    let mut dec = Decoder::new(data);
24    let raw = dec.decode().map_err(|e| DcmError::DecompressionError {
25        reason: format!("JPEG decode error: {e}"),
26    })?;
27    let info: ImageInfo = dec.info().ok_or_else(|| DcmError::DecompressionError {
28        reason: "JPEG decoder returned no image info".to_string(),
29    })?;
30
31    let samples = match info.pixel_format {
32        PixelFormat::L8 => 1,
33        PixelFormat::L16 => 1,
34        PixelFormat::RGB24 => 3,
35        PixelFormat::CMYK32 => 4,
36    };
37
38    Ok(JpegFrame {
39        width: info.width,
40        height: info.height,
41        samples_per_pixel: samples,
42        data: raw,
43    })
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    /// Minimal valid 1×1 JPEG (JFIF, grayscale, baseline).
51    const TINY_JPEG: &[u8] = &[
52        0xFF, 0xD8, // SOI
53        0xFF, 0xE0, 0x00, 0x10, // APP0 marker, length=16
54        b'J', b'F', b'I', b'F', 0x00, // identifier
55        0x01, 0x01, // version
56        0x00, // aspect ratio units
57        0x00, 0x01, 0x00, 0x01, // Xdensity, Ydensity
58        0x00, 0x00, // thumbnail
59        0xFF, 0xDB, 0x00, 0x43, 0x00, // DQT marker
60        16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69,
61        56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81,
62        104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99, 0xFF,
63        0xC0, 0x00, 0x0B, 0x08, // SOF0: precision=8
64        0x00, 0x01, 0x00, 0x01, // height=1, width=1
65        0x01, // components=1
66        0x01, 0x11, 0x00, // comp1
67        0xFF, 0xC4, 0x00, 0x1F, 0x00, // DHT (DC)
68        0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
69        0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4,
70        0x00, 0xB5, 0x10, // DHT (AC) — standard baseline AC
71        0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01,
72        0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51,
73        0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15,
74        0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A,
75        0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44,
76        0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63,
77        0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A,
78        0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
79        0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5,
80        0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2,
81        0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
82        0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA,
83        0x00, 0x08, 0x01, 0x01, 0x00, 0x00, // SOS
84        0x3F, 0x00, 0xF8, // compressed data (single all-zero 8x8 block → grey pixel)
85        0xFF, 0xD9, // EOI
86    ];
87
88    #[test]
89    fn decode_tiny_jpeg_returns_frame() {
90        // The tiny JPEG above is intentionally minimal — it may fail on some
91        // strict decoders. We just verify the API surface here.
92        match decode_jpeg(TINY_JPEG) {
93            Ok(frame) => {
94                assert!(frame.width > 0);
95                assert!(frame.height > 0);
96                assert!(!frame.data.is_empty());
97            }
98            Err(DcmError::DecompressionError { .. }) => {
99                // The minimal JPEG may not be well-formed enough for all decoders.
100                // That is acceptable — we only test that errors propagate cleanly.
101            }
102            Err(e) => panic!("unexpected error: {e}"),
103        }
104    }
105}