Skip to main content

codec_eval/
decode.rs

1//! Utility functions for decoding images with ICC profile extraction.
2//!
3//! This module provides helpers for decoding JPEG images while preserving
4//! ICC profile information, which is critical for accurate quality metrics.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use codec_eval::{decode::decode_jpeg_with_icc, ImageData};
10//!
11//! let jpeg_data = std::fs::read("test.jpg")?;
12//! let image = decode_jpeg_with_icc(&jpeg_data)?;
13//!
14//! // If the JPEG has an ICC profile, it will be automatically applied
15//! // when computing metrics via EvalSession::evaluate_image()
16//! ```
17
18use crate::error::{Error, Result};
19use crate::eval::session::ImageData;
20
21/// Decode a JPEG image with ICC profile extraction.
22///
23/// This function decodes JPEG data and extracts any embedded ICC profile.
24/// The result can be passed to `EvalSession::evaluate_image()` for
25/// accurate quality metric calculation.
26///
27/// # Arguments
28///
29/// * `data` - JPEG-compressed image data
30///
31/// # Returns
32///
33/// `ImageData` containing the decoded pixels and ICC profile (if present).
34/// Returns `ImageData::RgbSliceWithIcc` if an ICC profile was found,
35/// or `ImageData::RgbSlice` if no profile was embedded.
36///
37/// # Errors
38///
39/// Returns an error if the JPEG data is invalid or decoding fails.
40#[cfg(feature = "jpeg-decode")]
41pub fn decode_jpeg_with_icc(data: &[u8]) -> Result<ImageData> {
42    use std::io::Cursor;
43
44    let mut decoder = jpeg_decoder::Decoder::new(Cursor::new(data));
45    let pixels = decoder.decode().map_err(|e| Error::Codec {
46        codec: "jpeg-decoder".to_string(),
47        message: e.to_string(),
48    })?;
49
50    let info = decoder.info().ok_or_else(|| Error::Codec {
51        codec: "jpeg-decoder".to_string(),
52        message: "Missing JPEG info after decode".to_string(),
53    })?;
54
55    let width = info.width as usize;
56    let height = info.height as usize;
57
58    // Handle different pixel formats
59    let rgb = match info.pixel_format {
60        jpeg_decoder::PixelFormat::RGB24 => pixels,
61        jpeg_decoder::PixelFormat::L8 => {
62            // Grayscale to RGB
63            pixels.iter().flat_map(|&g| [g, g, g]).collect()
64        }
65        jpeg_decoder::PixelFormat::L16 => {
66            // 16-bit grayscale - take high byte and convert to RGB
67            pixels
68                .chunks_exact(2)
69                .flat_map(|c| {
70                    let g = c[0]; // High byte (assuming big endian)
71                    [g, g, g]
72                })
73                .collect()
74        }
75        jpeg_decoder::PixelFormat::CMYK32 => {
76            return Err(Error::Codec {
77                codec: "jpeg-decoder".to_string(),
78                message: "CMYK JPEGs are not currently supported".to_string(),
79            });
80        }
81    };
82
83    // Extract ICC profile if present
84    let icc_profile = decoder.icc_profile();
85
86    Ok(match icc_profile {
87        Some(icc) if !icc.is_empty() => ImageData::RgbSliceWithIcc {
88            data: rgb,
89            width,
90            height,
91            icc_profile: icc.clone(),
92        },
93        _ => ImageData::RgbSlice {
94            data: rgb,
95            width,
96            height,
97        },
98    })
99}
100
101/// Type alias for JPEG decode callbacks.
102#[cfg(feature = "jpeg-decode")]
103pub type JpegDecodeCallback = Box<dyn Fn(&[u8]) -> Result<ImageData> + Send + Sync + 'static>;
104
105/// Create an ICC-aware decode callback for use with `EvalSession::add_codec_with_decode`.
106///
107/// This returns a boxed callback that can be passed directly to codec registration.
108///
109/// # Example
110///
111/// ```ignore
112/// use codec_eval::{EvalSession, decode::jpeg_decode_callback};
113///
114/// session.add_codec_with_decode(
115///     "my-codec",
116///     "1.0.0",
117///     Box::new(my_encode_fn),
118///     jpeg_decode_callback(),
119/// );
120/// ```
121#[cfg(feature = "jpeg-decode")]
122pub fn jpeg_decode_callback() -> JpegDecodeCallback {
123    Box::new(decode_jpeg_with_icc)
124}
125
126#[cfg(test)]
127mod tests {
128    #[cfg(feature = "jpeg-decode")]
129    use super::*;
130
131    #[test]
132    #[cfg(feature = "jpeg-decode")]
133    fn test_decode_jpeg_no_icc() {
134        // This test would require a test JPEG file
135        // For now, just verify the function exists and has correct signature
136        let _: fn(&[u8]) -> Result<ImageData> = decode_jpeg_with_icc;
137    }
138}