Skip to main content

dicom_toolkit_codec/jpeg/
encoder.rs

1//! JPEG baseline/extended encoder wrapping `jpeg-encoder`.
2//!
3//! Encodes raw pixel data to a JPEG fragment for DICOM encapsulated storage.
4
5use dicom_toolkit_core::error::{DcmError, DcmResult};
6use jpeg_encoder::{ColorType, Encoder};
7
8use super::params::JpegParams;
9
10/// Encode raw pixel bytes as JPEG baseline/extended-style Huffman JPEG.
11///
12/// - `pixels`: interleaved pixel bytes (grayscale = 1 byte/px, RGB = 3 bytes/px)
13/// - `width`, `height`: image dimensions in pixels
14/// - `samples_per_pixel`: 1 (grayscale) or 3 (RGB/YCbCr)
15/// - `params`: encoding parameters (quality, sampling factor)
16///
17/// Returns the JPEG-compressed fragment bytes.
18pub fn encode_jpeg(
19    pixels: &[u8],
20    width: u16,
21    height: u16,
22    samples_per_pixel: u8,
23    params: &JpegParams,
24) -> DcmResult<Vec<u8>> {
25    let color_type = match samples_per_pixel {
26        1 => ColorType::Luma,
27        3 => ColorType::Rgb,
28        n => {
29            return Err(DcmError::CompressionError {
30                reason: format!("JPEG encoder: unsupported samples per pixel {n}"),
31            })
32        }
33    };
34
35    let mut buf = Vec::new();
36    let mut enc = Encoder::new(&mut buf, params.quality);
37    if samples_per_pixel == 3 {
38        enc.set_sampling_factor(params.sampling_factor);
39    }
40
41    enc.encode(pixels, width, height, color_type)
42        .map_err(|e| DcmError::CompressionError {
43            reason: format!("JPEG encode error: {e}"),
44        })?;
45
46    Ok(buf)
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn encode_grayscale_2x2() {
55        let pixels = vec![128u8, 64, 192, 32];
56        let result = encode_jpeg(&pixels, 2, 2, 1, &JpegParams::default());
57        assert!(result.is_ok(), "encode failed: {:?}", result.err());
58        let bytes = result.unwrap();
59        // JPEG must start with SOI marker 0xFF 0xD8
60        assert_eq!(&bytes[0..2], &[0xFF, 0xD8]);
61        // and end with EOI marker 0xFF 0xD9
62        assert_eq!(&bytes[bytes.len() - 2..], &[0xFF, 0xD9]);
63    }
64
65    #[test]
66    fn encode_rgb_2x2() {
67        let pixels: Vec<u8> = vec![
68            255, 0, 0, // red
69            0, 255, 0, // green
70            0, 0, 255, // blue
71            128, 128, 128, // grey
72        ];
73        let result = encode_jpeg(&pixels, 2, 2, 3, &JpegParams::default());
74        assert!(result.is_ok(), "encode failed: {:?}", result.err());
75    }
76
77    #[test]
78    fn encode_decode_roundtrip_grayscale() {
79        use crate::jpeg::decoder::decode_jpeg;
80
81        let pixels: Vec<u8> = (0u8..=255).collect();
82        let result = encode_jpeg(
83            &pixels,
84            16,
85            16,
86            1,
87            &JpegParams {
88                quality: 100,
89                ..Default::default()
90            },
91        );
92        let bytes = result.unwrap();
93
94        let frame = decode_jpeg(&bytes).unwrap();
95        assert_eq!(frame.width, 16);
96        assert_eq!(frame.height, 16);
97        assert_eq!(frame.samples_per_pixel, 1);
98        assert_eq!(frame.data.len(), 16 * 16);
99    }
100}