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}