#[cfg(feature = "jpeg2000")]
use crate::error::Error;
use crate::error::Result;
pub struct JpxDecoder;
impl super::StreamDecoder for JpxDecoder {
fn decode(&self, input: &[u8]) -> Result<Vec<u8>> {
Ok(input.to_vec())
}
fn name(&self) -> &str {
"JPXDecode"
}
}
#[cfg(feature = "jpeg2000")]
pub struct JpxImage {
pub samples: Vec<u8>,
pub num_components: u8,
pub width: u32,
pub height: u32,
}
#[cfg(feature = "jpeg2000")]
pub fn decode_jpx_at(bytes: &[u8], target: Option<(u32, u32)>) -> Result<JpxImage> {
decode_jpx_with(bytes, target, true)
}
#[cfg(feature = "jpeg2000")]
pub fn decode_jpx_indices_at(bytes: &[u8], target: Option<(u32, u32)>) -> Result<JpxImage> {
decode_jpx_with(bytes, target, false)
}
#[cfg(feature = "jpeg2000")]
fn decode_jpx_with(
bytes: &[u8],
target: Option<(u32, u32)>,
resolve_palette_indices: bool,
) -> Result<JpxImage> {
use hayro_jpeg2000::{DecodeSettings, DecoderContext, Image};
let settings = DecodeSettings {
target_resolution: target,
resolve_palette_indices,
..DecodeSettings::default()
};
let image = Image::new(bytes, &settings).map_err(|e| {
Error::UnsupportedFilter(format!("JPXDecode: JPEG 2000 decode failed: {e:?}"))
})?;
let width = image.width();
let height = image.height();
let npix = width as usize * height as usize;
let mut ctx = DecoderContext::default();
let decoded = image.decode(&mut ctx).map_err(|e| {
Error::UnsupportedFilter(format!("JPXDecode: JPEG 2000 decode failed: {e:?}"))
})?;
let comps = decoded.components();
if comps.is_empty() {
return Err(Error::UnsupportedFilter(
"JPXDecode: JPEG 2000 image has no components".to_string(),
));
}
let num_components = comps.len();
if !resolve_palette_indices {
let comp = &comps[0];
if comp.bit_depth() > 8 {
return Err(Error::UnsupportedFilter(format!(
"JPXDecode: a {}-bit component cannot index a colour table",
comp.bit_depth()
)));
}
let s = comp.samples();
if s.len() != npix {
return Err(Error::UnsupportedFilter(format!(
"JPXDecode: index component holds {} samples for a {width}x{height} image",
s.len()
)));
}
let samples = s
.iter()
.map(|&v| v.round().clamp(0.0, 255.0) as u8)
.collect();
return Ok(JpxImage {
samples,
num_components: 1,
width,
height,
});
}
if comps.iter().all(|c| c.samples().len() == npix) {
return Ok(JpxImage {
samples: decoded.data_u8(),
num_components: num_components as u8,
width,
height,
});
}
let (w, h) = (width as usize, height as usize);
let (sw, sh) = (width.div_ceil(2) as usize, height.div_ceil(2) as usize);
let mut planes: Vec<Vec<u8>> = Vec::with_capacity(num_components);
for (ci, comp) in comps.iter().enumerate() {
if comp.bit_depth() != 8 {
return Err(Error::UnsupportedFilter(format!(
"JPXDecode: subsampled component {ci} with {}-bit depth not supported",
comp.bit_depth()
)));
}
let s = comp.samples();
let plane = if s.len() == npix {
s.iter()
.map(|&v| v.round().clamp(0.0, 255.0) as u8)
.collect()
} else if s.len() == sw * sh {
upsample_nearest_u8(s, sw, sh, w, h)
} else {
return Err(Error::UnsupportedFilter(format!(
"JPXDecode: subsampled component {ci} ({} samples) — only 2×2 (4:2:0) \
subsampling of a {width}×{height} image is supported",
s.len()
)));
};
planes.push(plane);
}
let mut samples = vec![0u8; npix * num_components];
for (ci, plane) in planes.iter().enumerate() {
for (i, &px) in plane.iter().enumerate() {
samples[i * num_components + ci] = px;
}
}
Ok(JpxImage {
samples,
width,
height,
num_components: num_components as u8,
})
}
#[cfg(feature = "jpeg2000")]
fn upsample_nearest_u8(sub: &[f32], sw: usize, sh: usize, fw: usize, fh: usize) -> Vec<u8> {
let mut out = vec![0u8; fw * fh];
for y in 0..fh {
let sy = (y * sh / fh).min(sh.saturating_sub(1));
for x in 0..fw {
let sx = (x * sw / fw).min(sw.saturating_sub(1));
out[y * fw + x] = sub[sy * sw + sx].round().clamp(0.0, 255.0) as u8;
}
}
out
}
#[cfg(all(test, feature = "jpeg2000"))]
mod tests {
use super::{decode_jpx_at, upsample_nearest_u8};
const SAMPLE_JP2: &[u8] = include_bytes!("../../tests/fixtures/jpx/sample_gray.jp2");
#[test]
fn upsample_nearest_2x2_to_4x4() {
let sub = [10.0f32, 20.0, 30.0, 40.0];
let out = upsample_nearest_u8(&sub, 2, 2, 4, 4);
assert_eq!(
out,
vec![
10, 10, 20, 20, 10, 10, 20, 20, 30, 30, 40, 40, 30, 30, 40, 40,
]
);
}
#[test]
fn upsample_nearest_2x2_to_3x3() {
let sub = [1.0f32, 2.0, 3.0, 4.0];
let out = upsample_nearest_u8(&sub, 2, 2, 3, 3);
assert_eq!(out.len(), 9);
assert_eq!(out[0], 1); assert_eq!(out[8], 4); }
#[test]
fn test_target_resolution_decodes_a_smaller_image() {
let full = decode_jpx_at(SAMPLE_JP2, None).expect("decode at full size");
assert_eq!((full.width, full.height), (816, 1056), "test premise");
let small =
decode_jpx_at(SAMPLE_JP2, Some((200, 260))).expect("decode at a reduced resolution");
assert!(
small.width >= 200 && small.height >= 260,
"reduced decode {}x{} is below the requested 200x260",
small.width,
small.height
);
assert!(
small.width < full.width && small.height < full.height,
"target resolution had no effect: still {}x{}",
small.width,
small.height
);
assert_eq!(
small.samples.len(),
small.width as usize * small.height as usize * usize::from(small.num_components),
"reported geometry does not match the sample count"
);
}
#[test]
fn decode_jpx_grayscale() {
let img = decode_jpx_at(SAMPLE_JP2, None).expect("decode JP2 codestream");
assert_eq!(img.num_components, 1);
assert_eq!(img.samples.len(), 816 * 1056);
let first = img.samples[0];
assert!(
img.samples.iter().any(|&b| b != first),
"decoded image is uniformly flat — decode likely failed"
);
}
}