//! Backend-neutral JPEG 2000 contracts with independently selectable codecs. use crate::codec::{InterleavedComponent, InterleavedImage}; use crate::{ DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS, Error, ITextureCodec, ManagedImage, ManagedImageImageChannels, }; #[cfg(feature = "rust-j2k")] use j2k as rust_j2k; #[cfg(feature = "jpeg2000")] use libremetaverse_openjpeg as openjpeg; use libremetaverse_types::compat::ReadWrite; #[cfg(feature = "rust-j2k")] use std::borrow::Cow; use std::io::Read; /// JPEG 2000 container selection. #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum J2kFormat { /// Raw JPEG 2000 codestream used by Second Life texture assets. #[default] Codestream, /// JP2 file-format container. Jp2, } /// JPEG 2000 wavelet and rate-control mode. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum J2kCompression { /// Reversible 5/3 wavelet with exact sample reconstruction. #[default] Lossless, /// Irreversible 9/7 wavelet targeting the given compression ratio. Lossy { /// Uncompressed bytes divided by target codestream bytes. Must be at /// least 1.0 and finite. compression_ratio: f32, }, } /// Bounded decode configuration. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct J2kDecodeOptions { discard_levels: u32, quality_layers: u32, strict: bool, max_encoded_bytes: usize, max_pixels: usize, } impl Default for J2kDecodeOptions { fn default() -> Self { Self { discard_levels: 0, quality_layers: 0, strict: true, max_encoded_bytes: DEFAULT_MAX_ENCODED_BYTES, max_pixels: DEFAULT_MAX_PIXELS, } } } impl J2kDecodeOptions { /// Sets the number of highest-resolution levels to discard. #[must_use] pub const fn with_discard_levels(mut self, discard_levels: u32) -> Self { self.discard_levels = discard_levels; self } /// Limits decoding to the first `quality_layers` progressive layers. Zero /// decodes every available layer. #[must_use] pub const fn with_quality_layers(mut self, quality_layers: u32) -> Self { self.quality_layers = quality_layers; self } /// Selects whether truncated codestreams are rejected. #[must_use] pub const fn with_strict_mode(mut self, strict: bool) -> Self { self.strict = strict; self } /// Replaces the encoded-byte and decoded-pixel limits. /// /// Zero limits are invalid and cause decode to return [`Error::Argument`]. #[must_use] pub const fn with_limits(mut self, max_encoded_bytes: usize, max_pixels: usize) -> Self { self.max_encoded_bytes = max_encoded_bytes; self.max_pixels = max_pixels; self } /// Configured discard level. #[must_use] pub const fn discard_levels(self) -> u32 { self.discard_levels } /// Configured quality-layer limit. #[must_use] pub const fn quality_layers(self) -> u32 { self.quality_layers } } /// Encode configuration. #[derive(Clone, Copy, Debug, PartialEq)] pub struct J2kEncodeOptions { format: J2kFormat, compression: J2kCompression, max_encoded_bytes: usize, } impl Default for J2kEncodeOptions { fn default() -> Self { Self { format: J2kFormat::Codestream, compression: J2kCompression::Lossless, max_encoded_bytes: DEFAULT_MAX_ENCODED_BYTES, } } } impl J2kEncodeOptions { /// Selects raw codestream or JP2 output. #[must_use] pub const fn with_format(mut self, format: J2kFormat) -> Self { self.format = format; self } /// Selects reversible lossless or irreversible lossy compression. #[must_use] pub const fn with_compression(mut self, compression: J2kCompression) -> Self { self.compression = compression; self } /// Caps the produced codestream size. #[must_use] pub const fn with_max_encoded_bytes(mut self, max_encoded_bytes: usize) -> Self { self.max_encoded_bytes = max_encoded_bytes; self } } /// Cross-platform JPEG 2000 adapter. /// /// The optional adapter links to BSD-2-Clause `OpenJPEG` 2.5.4 or newer. Linux /// and macOS builds discover it with `pkg-config`; Windows MSVC builds use /// vcpkg. Checked-in minimal bindings and all unsafe FFI are isolated in the private /// adapter crate and no native codec type crosses this boundary. #[derive(Clone, Copy, Debug, Default)] #[cfg(feature = "jpeg2000")] pub struct J2kCodec { decode_options: J2kDecodeOptions, } #[cfg(feature = "jpeg2000")] impl J2kCodec { /// Creates a codec with explicit bounded decode options. #[must_use] pub const fn new(decode_options: J2kDecodeOptions) -> Self { Self { decode_options } } /// Decodes a raw J2K codestream or JP2 container while retaining component /// precision, signedness, alpha metadata, and order. /// /// The encoded-byte limit is checked before buffering to a codec stream. /// `OpenJPEG` header parsing does not allocate sample planes; dimensions and /// component counts are validated before `decode` may allocate them. /// /// # Errors /// /// Returns a typed argument or parse failure for invalid limits, oversized /// input/dimensions, unsupported component layouts, or malformed data. pub fn decode_interleaved( encoded: &[u8], options: J2kDecodeOptions, ) -> Result { if options.max_encoded_bytes == 0 || options.max_pixels == 0 || encoded.is_empty() || encoded.len() > options.max_encoded_bytes { return Err(Error::Argument); } let format = detect_format(encoded)?; let decoded = openjpeg::decode( encoded, backend_format(format), openjpeg::DecodeOptions { discard_levels: options.discard_levels, quality_layers: options.quality_layers, strict: options.strict, max_pixels: options.max_pixels.min(DEFAULT_MAX_PIXELS), }, ) .map_err(map_decode_error)?; backend_image_to_interleaved(decoded, options.max_pixels) } /// Decodes into the C#-compatible planar byte representation. /// /// # Errors /// /// Returns the failures documented by [`Self::decode_interleaved`] or a /// typed error for a component conversion/allocation failure. pub fn decode_bytes(encoded: &[u8], options: J2kDecodeOptions) -> Result { interleaved_to_managed(&Self::decode_interleaved(encoded, options)?) } /// Encodes the four-component compatibility view used by `CoreJ2K`. /// /// Color images preserve RGB and optional alpha. Alpha-only images repeat /// alpha into RGB and encode an opaque alpha plane. Images without alpha /// receive opaque alpha. Bump is not a JPEG 2000 output component, matching /// the reference adapter. /// /// # Errors /// /// Returns a typed validation/operation failure for invalid image layouts, /// lossy settings, allocation/codec errors, or oversized output. pub fn encode(image: &ManagedImage, options: J2kEncodeOptions) -> Result, Error> { image.validate()?; if options.max_encoded_bytes == 0 { return Err(Error::Argument); } if let J2kCompression::Lossy { compression_ratio } = options.compression && (!compression_ratio.is_finite() || compression_ratio < 1.0) { return Err(Error::Argument); } encode_with_openjpeg(image, options) } } #[cfg(feature = "jpeg2000")] impl ITextureCodec for J2kCodec { fn decode(&self, mut stream: Box) -> Result { if self.decode_options.max_encoded_bytes == 0 { return Err(Error::Argument); } let limit = self .decode_options .max_encoded_bytes .checked_add(1) .ok_or(Error::Argument)?; let mut encoded = Vec::new(); Read::by_ref(&mut stream) .take(u64::try_from(limit).map_err(|_| Error::Argument)?) .read_to_end(&mut encoded) .map_err(|_| parse("JPEG 2000 input stream"))?; if encoded.len() > self.decode_options.max_encoded_bytes { return Err(Error::Argument); } Self::decode_bytes(&encoded, self.decode_options) } } /// Cross-platform JPEG 2000 adapter implemented entirely in safe Rust. /// /// This backend is independent from [`J2kCodec`]: enabling `rust-j2k` does not /// link `OpenJPEG` or change the meaning of the existing native codec type. #[derive(Clone, Copy, Debug, Default)] #[cfg(feature = "rust-j2k")] pub struct RustJ2kCodec { decode_options: J2kDecodeOptions, } #[cfg(feature = "rust-j2k")] impl RustJ2kCodec { /// Creates a codec with explicit bounded decode options. #[must_use] pub const fn new(decode_options: J2kDecodeOptions) -> Self { Self { decode_options } } /// Decodes raw J2K/J2C or JP2 into native component planes. /// /// Header geometry, sampling, and component count are checked before the /// backend is allowed to allocate decoded sample storage. /// /// # Errors /// /// Returns a typed argument or parse failure for invalid limits, /// unsupported component geometry, malformed input, or codec failure. pub fn decode_interleaved( encoded: &[u8], options: J2kDecodeOptions, ) -> Result { validate_decode_input(encoded, options)?; detect_format(encoded)?; let prepared = prepare_rust_decode_input(encoded, options)?; let encoded = prepared.as_ref(); let settings = if options.strict { rust_j2k::DecodeSettings::strict() } else { rust_j2k::DecodeSettings::lenient() }; let support = rust_j2k::J2kDecoder::inspect_support(encoded).map_err(map_rust_decode_error)?; let (width, height) = checked_dimensions( support.info.dimensions.0, support.info.dimensions.1, options.max_pixels, )?; if !(1..=5).contains(&support.component_count()) || support.has_component_subsampling() { return Err(Error::Argument); } if options.discard_levels == 0 { let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings) .map_err(map_rust_decode_error)?; let native = decoder .decode_native_components() .map_err(map_rust_decode_error)?; return rust_native_to_interleaved(&native, width, height, options.max_pixels); } decode_rust_reduced(encoded, settings, &support, options) } /// Decodes into the C#-compatible planar byte representation. /// /// # Errors /// /// Returns the failures documented by [`Self::decode_interleaved`] or a /// typed component conversion/allocation failure. pub fn decode_bytes(encoded: &[u8], options: J2kDecodeOptions) -> Result { interleaved_to_managed(&Self::decode_interleaved(encoded, options)?) } /// Encodes the four-component `CoreJ2K` compatibility view as J2K or JP2. /// /// # Errors /// /// Returns a typed validation/operation failure for invalid image layouts, /// lossy settings, codec errors, allocation failure, or oversized output. pub fn encode(image: &ManagedImage, options: J2kEncodeOptions) -> Result, Error> { validate_encode_request(image, options)?; encode_with_rust_j2k(image, options) } } #[cfg(feature = "rust-j2k")] impl ITextureCodec for RustJ2kCodec { fn decode(&self, mut stream: Box) -> Result { read_bounded_stream(&mut stream, self.decode_options.max_encoded_bytes) .and_then(|encoded| Self::decode_bytes(&encoded, self.decode_options)) } } #[cfg(feature = "rust-j2k")] fn validate_decode_input(encoded: &[u8], options: J2kDecodeOptions) -> Result<(), Error> { if options.max_encoded_bytes == 0 || options.max_pixels == 0 || encoded.is_empty() || encoded.len() > options.max_encoded_bytes { return Err(Error::Argument); } Ok(()) } #[cfg(feature = "rust-j2k")] fn validate_encode_request(image: &ManagedImage, options: J2kEncodeOptions) -> Result<(), Error> { image.validate()?; if options.max_encoded_bytes == 0 { return Err(Error::Argument); } if let J2kCompression::Lossy { compression_ratio } = options.compression && (!compression_ratio.is_finite() || compression_ratio < 1.0) { return Err(Error::Argument); } Ok(()) } #[cfg(feature = "rust-j2k")] fn read_bounded_stream( stream: &mut (dyn ReadWrite + Send), max_encoded_bytes: usize, ) -> Result, Error> { if max_encoded_bytes == 0 { return Err(Error::Argument); } let limit = max_encoded_bytes.checked_add(1).ok_or(Error::Argument)?; let mut encoded = Vec::new(); Read::take(stream, u64::try_from(limit).map_err(|_| Error::Argument)?) .read_to_end(&mut encoded) .map_err(|_| parse("JPEG 2000 input stream"))?; if encoded.len() > max_encoded_bytes { return Err(Error::Argument); } Ok(encoded) } #[cfg(feature = "jpeg2000")] fn backend_image_to_interleaved( image: openjpeg::Image, max_pixels: usize, ) -> Result { let (width, height) = checked_dimensions(image.width, image.height, max_pixels)?; let mut decoded = Vec::new(); decoded .try_reserve_exact(image.components.len()) .map_err(|_| Error::InvalidOperation)?; for component in image.components { if component.width != image.width || component.height != image.height { return Err(parse("subsampled JPEG 2000 components")); } decoded.push(InterleavedComponent::new( component.precision, component.signed, component.alpha, component.samples, )?); } InterleavedImage::new(width, height, decoded) } #[cfg(feature = "rust-j2k")] fn rust_native_to_interleaved( image: &rust_j2k::J2kDecodedNativeComponents, width: i32, height: i32, max_pixels: usize, ) -> Result { let dimensions = image.dimensions(); let checked = checked_dimensions(dimensions.0, dimensions.1, max_pixels)?; if checked != (width, height) || !(1..=5).contains(&image.planes().len()) { return Err(Error::Argument); } let has_alpha = image.has_alpha(); let plane_count = image.planes().len(); let mut components = Vec::new(); components .try_reserve_exact(plane_count) .map_err(|_| Error::InvalidOperation)?; for (index, plane) in image.planes().iter().enumerate() { if plane.dimensions() != dimensions || plane.sampling() != (1, 1) { return Err(parse("subsampled JPEG 2000 components")); } let samples = unpack_native_samples( plane.data(), plane.bytes_per_sample(), plane.bit_depth(), plane.signed(), )?; components.push(InterleavedComponent::new( plane.bit_depth(), plane.signed(), has_alpha && index + 1 == plane_count, samples, )?); } InterleavedImage::new(width, height, components) } #[cfg(feature = "rust-j2k")] fn unpack_native_samples( data: &[u8], bytes_per_sample: u8, precision: u8, signed: bool, ) -> Result, Error> { let sample_width = usize::from(bytes_per_sample); if sample_width == 0 || sample_width > 4 || !data.len().is_multiple_of(sample_width) { return Err(Error::Argument); } let mut samples = Vec::new(); samples .try_reserve_exact(data.len() / sample_width) .map_err(|_| Error::InvalidOperation)?; for bytes in data.chunks_exact(sample_width) { let mut packed = [0_u8; 4]; packed[..sample_width].copy_from_slice(bytes); let raw = u32::from_le_bytes(packed); let value = if signed { let shift = 32_u32 .checked_sub(u32::from(precision)) .ok_or(Error::Argument)?; i32::from_ne_bytes((raw << shift).to_ne_bytes()) >> shift } else { i32::try_from(raw).map_err(|_| Error::Argument)? }; samples.push(value); } Ok(samples) } #[cfg(feature = "rust-j2k")] fn decode_rust_reduced( encoded: &[u8], settings: rust_j2k::DecodeSettings, support: &rust_j2k::J2kSupportInfo, options: J2kDecodeOptions, ) -> Result { if u32::from(support.info.resolution_levels) <= options.discard_levels { return decode_rust_reduced_native(encoded, settings, support, options); } let scale = match options.discard_levels { 1 => rust_j2k::Downscale::Half, 2 => rust_j2k::Downscale::Quarter, 3 => rust_j2k::Downscale::Eighth, _ => return decode_rust_reduced_native(encoded, settings, support, options), }; let components = usize::from(support.component_count()); if !matches!(components, 1 | 3 | 4) { return decode_rust_reduced_native(encoded, settings, support, options); } let denominator = 1_u32 .checked_shl(options.discard_levels) .ok_or(Error::Argument)?; let reduced_width = support.info.dimensions.0.div_ceil(denominator); let reduced_height = support.info.dimensions.1.div_ceil(denominator); let (width, height) = checked_dimensions(reduced_width, reduced_height, options.max_pixels)?; let pixels = usize::try_from(reduced_width) .ok() .and_then(|value| { usize::try_from(reduced_height) .ok() .and_then(|height| value.checked_mul(height)) }) .ok_or(Error::Argument)?; let length = pixels.checked_mul(components).ok_or(Error::Argument)?; let mut packed = Vec::new(); packed .try_reserve_exact(length) .map_err(|_| Error::InvalidOperation)?; packed.resize(length, 0); let stride = usize::try_from(reduced_width) .ok() .and_then(|width| width.checked_mul(components)) .ok_or(Error::Argument)?; let format = match components { 1 => rust_j2k::PixelFormat::Gray8, 3 => rust_j2k::PixelFormat::Rgb8, 4 => rust_j2k::PixelFormat::Rgba8, _ => return Err(Error::Argument), }; let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings) .map_err(map_rust_decode_error)?; decoder .decode_scaled_into( &mut rust_j2k::J2kScratchPool::new(), &mut packed, stride, format, scale, ) .map_err(map_rust_decode_error)?; packed_bytes_to_interleaved(width, height, components, &packed) } #[cfg(feature = "rust-j2k")] fn decode_rust_reduced_native( encoded: &[u8], settings: rust_j2k::DecodeSettings, support: &rust_j2k::J2kSupportInfo, options: J2kDecodeOptions, ) -> Result { let denominator = 1_u32 .checked_shl(options.discard_levels) .ok_or(Error::Argument)?; let reduced_width = support.info.dimensions.0.div_ceil(denominator); let reduced_height = support.info.dimensions.1.div_ceil(denominator); let (width, height) = checked_dimensions(reduced_width, reduced_height, options.max_pixels)?; let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings) .map_err(map_rust_decode_error)?; let native = decoder .decode_native_components() .map_err(map_rust_decode_error)?; let has_alpha = native.has_alpha(); let count = native.planes().len(); let mut components = Vec::new(); components .try_reserve_exact(count) .map_err(|_| Error::InvalidOperation)?; for (index, plane) in native.planes().iter().enumerate() { if plane.dimensions() != support.info.dimensions || plane.sampling() != (1, 1) { return Err(parse("subsampled JPEG 2000 components")); } let full = unpack_native_samples( plane.data(), plane.bytes_per_sample(), plane.bit_depth(), plane.signed(), )?; let reduced = subsample_plane( &full, support.info.dimensions, (reduced_width, reduced_height), denominator, )?; components.push(InterleavedComponent::new( plane.bit_depth(), plane.signed(), has_alpha && index + 1 == count, reduced, )?); } InterleavedImage::new(width, height, components) } #[cfg(feature = "rust-j2k")] fn subsample_plane( samples: &[i32], source: (u32, u32), target: (u32, u32), denominator: u32, ) -> Result, Error> { let source_width = usize::try_from(source.0).map_err(|_| Error::Argument)?; let expected = source_width .checked_mul(usize::try_from(source.1).map_err(|_| Error::Argument)?) .ok_or(Error::Argument)?; if samples.len() != expected { return Err(Error::Argument); } let target_len = usize::try_from(target.0) .ok() .and_then(|width| { usize::try_from(target.1) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; let mut reduced = Vec::new(); reduced .try_reserve_exact(target_len) .map_err(|_| Error::InvalidOperation)?; for y in 0..target.1 { let source_y = y.checked_mul(denominator).ok_or(Error::Argument)?; for x in 0..target.0 { let source_x = x.checked_mul(denominator).ok_or(Error::Argument)?; let index = usize::try_from(source_y) .ok() .and_then(|y| y.checked_mul(source_width)) .and_then(|row| { usize::try_from(source_x) .ok() .and_then(|x| row.checked_add(x)) }) .ok_or(Error::Argument)?; reduced.push(*samples.get(index).ok_or(Error::Argument)?); } } Ok(reduced) } #[cfg(feature = "rust-j2k")] fn packed_bytes_to_interleaved( width: i32, height: i32, component_count: usize, packed: &[u8], ) -> Result { let pixels = usize::try_from(width) .ok() .and_then(|width| { usize::try_from(height) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; if packed.len() != pixels.checked_mul(component_count).ok_or(Error::Argument)? { return Err(Error::Argument); } let mut components = Vec::new(); components .try_reserve_exact(component_count) .map_err(|_| Error::InvalidOperation)?; for component in 0..component_count { let mut samples = Vec::new(); samples .try_reserve_exact(pixels) .map_err(|_| Error::InvalidOperation)?; samples.extend( packed .iter() .skip(component) .step_by(component_count) .map(|sample| i32::from(*sample)), ); components.push(InterleavedComponent::new( 8, false, component + 1 == component_count && matches!(component_count, 2 | 4 | 5), samples, )?); } InterleavedImage::new(width, height, components) } fn interleaved_to_managed(image: &InterleavedImage) -> Result { let channels = channels_for_components(image.number_of_components())?; let pixels = usize::try_from(image.width()) .ok() .and_then(|width| { usize::try_from(image.height()) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; let mut bytes = Vec::new(); let length = pixels .checked_mul(image.number_of_components()) .ok_or(Error::Argument)?; bytes .try_reserve_exact(length) .map_err(|_| Error::InvalidOperation)?; bytes.resize(length, 0); let mut plane = vec![0; pixels]; for component in 0..image.number_of_components() { image.to_component_bytes(component, &mut plane)?; for (pixel, sample) in plane.iter().enumerate() { bytes[pixel * image.number_of_components() + component] = *sample; } } managed_from_reference_interleaved(image.width(), image.height(), channels, &bytes) } #[cfg(feature = "jpeg2000")] fn encode_with_openjpeg(image: &ManagedImage, options: J2kEncodeOptions) -> Result, Error> { let width = u32::try_from(image.width).map_err(|_| Error::Argument)?; let height = u32::try_from(image.height).map_err(|_| Error::Argument)?; let planes = reference_encode_planes(image)?; let components = [ openjpeg::ComponentRef { precision: 8, signed: false, alpha: false, samples: &planes[0], }, openjpeg::ComponentRef { precision: 8, signed: false, alpha: false, samples: &planes[1], }, openjpeg::ComponentRef { precision: 8, signed: false, alpha: false, samples: &planes[2], }, openjpeg::ComponentRef { precision: 8, signed: false, alpha: true, samples: &planes[3], }, ]; let compression = match options.compression { J2kCompression::Lossless => openjpeg::Compression::Lossless, J2kCompression::Lossy { compression_ratio } => { openjpeg::Compression::Lossy { compression_ratio } } }; openjpeg::encode( width, height, &components, backend_format(options.format), compression, options.max_encoded_bytes, ) .map_err(map_encode_error) } #[cfg(feature = "rust-j2k")] fn encode_with_rust_j2k(image: &ManagedImage, options: J2kEncodeOptions) -> Result, Error> { let width = u32::try_from(image.width).map_err(|_| Error::Argument)?; let height = u32::try_from(image.height).map_err(|_| Error::Argument)?; checked_dimensions(width, height, DEFAULT_MAX_PIXELS)?; let planes = reference_encode_planes(image)?; let pixels = usize::try_from(width) .ok() .and_then(|width| { usize::try_from(height) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; let length = pixels.checked_mul(4).ok_or(Error::Argument)?; let mut interleaved = Vec::new(); interleaved .try_reserve_exact(length) .map_err(|_| Error::InvalidOperation)?; for pixel in 0..pixels { for plane in &planes { interleaved.push(u8::try_from(plane[pixel]).map_err(|_| Error::Argument)?); } } let codestream = match options.compression { J2kCompression::Lossless => { let samples = rust_j2k::J2kLosslessSamples::new(&interleaved, width, height, 4, 8, false) .map_err(map_rust_encode_error)?; rust_j2k::encode_j2k_lossless( samples, &rust_j2k::J2kLosslessEncodeOptions::default() .with_cpu_only_backend() .with_max_decomposition_levels(Some(6)), ) .map_err(map_rust_encode_error)? .codestream } J2kCompression::Lossy { compression_ratio } => { let target_bytes = lossy_target_bytes(length, compression_ratio)?; let layers = [ rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes( target_bytes.div_ceil(3), )), rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes( target_bytes.saturating_mul(2).div_ceil(3), )), rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes(target_bytes)), ]; let samples = rust_j2k::J2kLossySamples::new(&interleaved, width, height, 4, 8, false) .map_err(map_rust_encode_error)?; rust_j2k::encode_j2k_lossy( samples, &rust_j2k::J2kLossyEncodeOptions::default() .with_cpu_only_backend() .with_max_decomposition_levels(Some(6)) .with_quality_layers(layers.to_vec()) .with_marker_segments(vec![rust_j2k::J2kMarkerSegment::Plt]), ) .map_err(map_rust_encode_error)? .codestream } }; let encoded = match options.format { J2kFormat::Codestream => codestream, J2kFormat::Jp2 => { rust_j2k::wrap_j2k_codestream(&codestream, rust_j2k::J2kFileWrapOptions::jp2()) .map_err(map_rust_encode_error)? } }; if encoded.len() > options.max_encoded_bytes { return Err(Error::Argument); } Ok(encoded) } #[cfg(feature = "rust-j2k")] fn prepare_rust_decode_input( encoded: &[u8], options: J2kDecodeOptions, ) -> Result, Error> { let format = detect_format(encoded)?; let container = if !options.strict && format == J2kFormat::Jp2 && rust_j2k::extract_j2k_codestream_payload(encoded).is_err() { let mut repaired = Vec::new(); repaired .try_reserve_exact(encoded.len().saturating_add(2)) .map_err(|_| Error::InvalidOperation)?; repaired.extend_from_slice(encoded); repaired.extend_from_slice(&[0xff, 0xd9]); Cow::Owned(repaired) } else { Cow::Borrowed(encoded) }; let payload = rust_j2k::extract_j2k_codestream_payload(container.as_ref()) .map_err(map_rust_decode_error)? .codestream(); let available = codestream_quality_layers(payload)?; if options.quality_layers != 0 && options.quality_layers < u32::from(available) { let requested = u16::try_from(options.quality_layers).map_err(|_| Error::Argument)?; let limited = limit_codestream_quality_layers(payload, available, requested)?; if format == J2kFormat::Jp2 { return rust_j2k::wrap_j2k_codestream(&limited, rust_j2k::J2kFileWrapOptions::jp2()) .map(Cow::Owned) .map_err(map_rust_decode_error); } return Ok(Cow::Owned(limited)); } if !options.strict { if payload.ends_with(&[0xff, 0xd9]) { return Ok(container); } let mut repaired = Vec::new(); repaired .try_reserve_exact(payload.len().saturating_add(2)) .map_err(|_| Error::InvalidOperation)?; repaired.extend_from_slice(payload); repaired.extend_from_slice(&[0xff, 0xd9]); if format == J2kFormat::Jp2 { return rust_j2k::wrap_j2k_codestream(&repaired, rust_j2k::J2kFileWrapOptions::jp2()) .map(Cow::Owned) .map_err(map_rust_decode_error); } return Ok(Cow::Owned(repaired)); } Ok(Cow::Borrowed(encoded)) } #[cfg(feature = "rust-j2k")] fn limit_codestream_quality_layers( codestream: &[u8], available: u16, requested: u16, ) -> Result, Error> { if requested == 0 || requested >= available { return Err(Error::Argument); } let main = parse_main_header(codestream)?; if main.progression != 0 || main.has_progression_changes { return Err(Error::InvalidOperation); } let mut output = Vec::new(); output .try_reserve_exact(codestream.len()) .map_err(|_| Error::InvalidOperation)?; output.extend_from_slice(&codestream[..main.first_tile_part]); output[main.cod_layers_offset..main.cod_layers_offset + 2] .copy_from_slice(&requested.to_be_bytes()); let mut tile_offset = main.first_tile_part; while tile_offset .checked_add(2) .is_some_and(|end| end <= codestream.len()) && codestream[tile_offset..].starts_with(&[0xff, 0x90]) { let consumed = append_limited_tile_part(codestream, tile_offset, available, requested, &mut output)?; tile_offset = tile_offset.checked_add(consumed).ok_or(Error::Argument)?; } if tile_offset .checked_add(2) .is_none_or(|end| end > codestream.len()) { return Err(parse("JPEG 2000 EOC marker")); } output.extend_from_slice(&[0xff, 0xd9]); Ok(output) } #[cfg(feature = "rust-j2k")] #[derive(Clone, Copy)] struct MainHeader { first_tile_part: usize, cod_layers_offset: usize, progression: u8, has_progression_changes: bool, } #[cfg(feature = "rust-j2k")] fn parse_main_header(codestream: &[u8]) -> Result { if !codestream.starts_with(&[0xff, 0x4f]) { return Err(parse("JPEG 2000 codestream")); } let mut offset = 2_usize; let mut cod = None; let mut has_progression_changes = false; while offset .checked_add(2) .is_some_and(|end| end <= codestream.len()) { if codestream[offset] != 0xff { return Err(parse("JPEG 2000 main header marker")); } let marker = codestream[offset + 1]; if marker == 0x90 { let (cod_layers_offset, progression) = cod.ok_or_else(|| parse("JPEG 2000 COD marker"))?; return Ok(MainHeader { first_tile_part: offset, cod_layers_offset, progression, has_progression_changes, }); } let (segment_length, end) = marker_segment_bounds(codestream, offset)?; if marker == 0x52 { if segment_length < 7 { return Err(parse("JPEG 2000 COD marker")); } cod = Some((offset + 6, codestream[offset + 5])); } else if marker == 0x5f { has_progression_changes = true; } offset = end; } Err(parse("JPEG 2000 tile part")) } #[cfg(feature = "rust-j2k")] fn append_limited_tile_part( codestream: &[u8], tile_offset: usize, available: u16, requested: u16, output: &mut Vec, ) -> Result { if tile_offset .checked_add(12) .is_none_or(|end| end > codestream.len()) { return Err(parse("JPEG 2000 SOT marker")); } let tile_length = usize::try_from(u32::from_be_bytes([ codestream[tile_offset + 6], codestream[tile_offset + 7], codestream[tile_offset + 8], codestream[tile_offset + 9], ])) .map_err(|_| Error::Argument)?; if tile_length < 14 || tile_offset .checked_add(tile_length) .is_none_or(|end| end > codestream.len()) { return Err(parse("JPEG 2000 tile-part length")); } let tile_end = tile_offset + tile_length; let mut header_offset = tile_offset + 12; let output_start = output.len(); output.extend_from_slice(&codestream[tile_offset..tile_offset + 12]); let mut packet_lengths = Vec::new(); loop { if header_offset .checked_add(2) .is_none_or(|end| end > tile_end) || codestream[header_offset] != 0xff { return Err(parse("JPEG 2000 tile header")); } let marker = codestream[header_offset + 1]; if marker == 0x93 { output.extend_from_slice(&[0xff, 0x93]); header_offset += 2; break; } let (_, marker_end) = marker_segment_bounds(codestream, header_offset)?; if marker_end > tile_end { return Err(parse("JPEG 2000 tile header")); } if marker == 0x58 { decode_plt_lengths( &codestream[header_offset + 5..marker_end], &mut packet_lengths, )?; } else { if marker == 0x5f { return Err(Error::InvalidOperation); } output.extend_from_slice(&codestream[header_offset..marker_end]); } header_offset = marker_end; } if packet_lengths.is_empty() || packet_lengths.len() % usize::from(available) != 0 { return Err(Error::InvalidOperation); } let packets_per_layer = packet_lengths.len() / usize::from(available); let keep_packets = packets_per_layer .checked_mul(usize::from(requested)) .ok_or(Error::Argument)?; let keep_bytes = packet_lengths[..keep_packets] .iter() .try_fold(0_usize, |total, length| { total.checked_add(*length).ok_or(Error::Argument) })?; if header_offset .checked_add(keep_bytes) .is_none_or(|end| end > tile_end) { return Err(parse("JPEG 2000 packet lengths")); } output.extend_from_slice(&codestream[header_offset..header_offset + keep_bytes]); let limited_length = output .len() .checked_sub(output_start) .ok_or(Error::Argument)?; let limited_length = u32::try_from(limited_length).map_err(|_| Error::Argument)?; output[output_start + 6..output_start + 10].copy_from_slice(&limited_length.to_be_bytes()); Ok(tile_length) } #[cfg(feature = "rust-j2k")] fn decode_plt_lengths(data: &[u8], lengths: &mut Vec) -> Result<(), Error> { let mut value = 0_usize; let mut pending = false; for byte in data { value = value .checked_shl(7) .and_then(|value| value.checked_add(usize::from(byte & 0x7f))) .ok_or(Error::Argument)?; pending = byte & 0x80 != 0; if !pending { lengths .try_reserve(1) .map_err(|_| Error::InvalidOperation)?; lengths.push(value); value = 0; } } if pending { return Err(parse("JPEG 2000 PLT packet length")); } Ok(()) } #[cfg(feature = "rust-j2k")] fn marker_segment_bounds(bytes: &[u8], offset: usize) -> Result<(usize, usize), Error> { if offset.checked_add(4).is_none_or(|end| end > bytes.len()) { return Err(parse("JPEG 2000 marker length")); } let segment_length = usize::from(u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]])); if segment_length < 2 { return Err(parse("JPEG 2000 marker length")); } let end = offset .checked_add(2) .and_then(|value| value.checked_add(segment_length)) .filter(|end| *end <= bytes.len()) .ok_or_else(|| parse("JPEG 2000 marker length"))?; Ok((segment_length, end)) } #[cfg(feature = "rust-j2k")] fn codestream_quality_layers(codestream: &[u8]) -> Result { if !codestream.starts_with(&[0xff, 0x4f]) { return Err(parse("JPEG 2000 codestream")); } let mut offset = 2_usize; while offset .checked_add(4) .is_some_and(|end| end <= codestream.len()) { if codestream[offset] != 0xff { return Err(parse("JPEG 2000 marker")); } let marker = codestream[offset + 1]; if matches!(marker, 0x90 | 0x93 | 0xd9) { break; } let segment_length = usize::from(u16::from_be_bytes([ codestream[offset + 2], codestream[offset + 3], ])); if segment_length < 2 { return Err(parse("JPEG 2000 marker length")); } let end = offset .checked_add(2) .and_then(|value| value.checked_add(segment_length)) .filter(|end| *end <= codestream.len()) .ok_or_else(|| parse("JPEG 2000 marker length"))?; if marker == 0x52 { if segment_length < 7 { return Err(parse("JPEG 2000 COD marker")); } let layers = u16::from_be_bytes([codestream[offset + 6], codestream[offset + 7]]); return (layers != 0) .then_some(layers) .ok_or_else(|| parse("JPEG 2000 quality layers")); } offset = end; } Err(parse("JPEG 2000 COD marker")) } fn reference_encode_planes(image: &ManagedImage) -> Result<[Vec; 4], Error> { let pixels = usize::try_from(image.width) .ok() .and_then(|width| { usize::try_from(image.height) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; let mut planes = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; for plane in &mut planes { plane .try_reserve_exact(pixels) .map_err(|_| Error::InvalidOperation)?; plane.resize(pixels, 0); } let has_color = image.channels.contains(ManagedImageImageChannels::COLOR); let has_alpha = image.channels.contains(ManagedImageImageChannels::ALPHA); if has_alpha && !has_color { for (pixel, sample) in image.alpha.iter().copied().enumerate() { let sample = i32::from(sample); planes[0][pixel] = sample; planes[1][pixel] = sample; planes[2][pixel] = sample; planes[3][pixel] = 255; } return Ok(planes); } if !has_color { return Err(Error::InvalidOperation); } for (pixel, red) in image.red.iter().copied().enumerate() { planes[0][pixel] = i32::from(red); planes[1][pixel] = i32::from(image.green[pixel]); planes[2][pixel] = i32::from(image.blue[pixel]); planes[3][pixel] = if has_alpha { i32::from(image.alpha[pixel]) } else { 255 }; } Ok(planes) } fn managed_from_reference_interleaved( width: i32, height: i32, channels: ManagedImageImageChannels, bytes: &[u8], ) -> Result { let components = component_count(channels); let pixels = usize::try_from(width) .ok() .and_then(|width| { usize::try_from(height) .ok() .and_then(|height| width.checked_mul(height)) }) .ok_or(Error::Argument)?; if bytes.len() != pixels.checked_mul(components).ok_or(Error::Argument)? { return Err(Error::Argument); } let mut image = ManagedImage::new(width, height, channels)?; for pixel in 0..pixels { let source = pixel * components; match components { 1 => image.red[pixel] = bytes[source], 2 => { image.red[pixel] = bytes[source]; image.alpha[pixel] = bytes[source + 1]; } 3 => { image.red[pixel] = bytes[source]; image.green[pixel] = bytes[source + 1]; image.blue[pixel] = bytes[source + 2]; } 4 => { image.red[pixel] = bytes[source]; image.green[pixel] = bytes[source + 1]; image.blue[pixel] = bytes[source + 2]; image.alpha[pixel] = bytes[source + 3]; } 5 => { image.red[pixel] = bytes[source]; image.green[pixel] = bytes[source + 1]; image.blue[pixel] = bytes[source + 2]; image.bump[pixel] = bytes[source + 3]; image.alpha[pixel] = bytes[source + 4]; } _ => return Err(Error::Argument), } } Ok(image) } fn channels_for_components(components: usize) -> Result { Ok(match components { 1 => ManagedImageImageChannels::GRAY, 2 => ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA, 3 => ManagedImageImageChannels::COLOR, 4 => ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA, 5 => { ManagedImageImageChannels::COLOR | ManagedImageImageChannels::BUMP | ManagedImageImageChannels::ALPHA } _ => return Err(Error::Argument), }) } fn component_count(channels: ManagedImageImageChannels) -> usize { if channels.contains(ManagedImageImageChannels::BUMP) { 5 } else if channels.contains(ManagedImageImageChannels::COLOR) { 3 + usize::from(channels.contains(ManagedImageImageChannels::ALPHA)) } else if channels.contains(ManagedImageImageChannels::GRAY) { 1 + usize::from(channels.contains(ManagedImageImageChannels::ALPHA)) } else { usize::from(channels.contains(ManagedImageImageChannels::ALPHA)) } } fn detect_format(encoded: &[u8]) -> Result { const JP2_MAGIC: &[u8] = &[ 0x00, 0x00, 0x00, 0x0c, b'j', b'P', b' ', b' ', 0x0d, 0x0a, 0x87, 0x0a, ]; const J2K_MAGIC: &[u8] = &[0xff, 0x4f, 0xff, 0x51]; if encoded.starts_with(JP2_MAGIC) { Ok(J2kFormat::Jp2) } else if encoded.starts_with(J2K_MAGIC) { Ok(J2kFormat::Codestream) } else { Err(parse("JPEG 2000 magic")) } } #[cfg(feature = "jpeg2000")] const fn backend_format(format: J2kFormat) -> openjpeg::Format { match format { J2kFormat::Codestream => openjpeg::Format::J2k, J2kFormat::Jp2 => openjpeg::Format::Jp2, } } #[cfg(feature = "jpeg2000")] const fn map_decode_error(error: openjpeg::Error) -> Error { match error { openjpeg::Error::LimitExceeded => Error::Argument, openjpeg::Error::Allocation => Error::InvalidOperation, openjpeg::Error::InvalidInput | openjpeg::Error::Codec => parse("JPEG 2000 codestream"), _ => parse("JPEG 2000 backend"), } } #[cfg(feature = "jpeg2000")] const fn map_encode_error(error: openjpeg::Error) -> Error { match error { openjpeg::Error::InvalidInput | openjpeg::Error::LimitExceeded => Error::Argument, _ => Error::InvalidOperation, } } #[cfg(feature = "rust-j2k")] #[allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "validated finite positive ratio and a u32-bounded byte count make this conversion exact enough for rate targeting" )] fn lossy_target_bytes(length: usize, compression_ratio: f32) -> Result { let length = u32::try_from(length).map_err(|_| Error::Argument)?; Ok((f64::from(length) / f64::from(compression_ratio)) .round() .max(1.0) as u64) } #[cfg(feature = "rust-j2k")] #[allow( clippy::needless_pass_by_value, reason = "map_err supplies owned non-Copy backend errors and the public error intentionally does not retain backend types" )] fn map_rust_decode_error(error: rust_j2k::J2kError) -> Error { match error { rust_j2k::J2kError::Buffer(_) | rust_j2k::J2kError::InvalidSamples { .. } | rust_j2k::J2kError::InvalidRegion { .. } | rust_j2k::J2kError::DimensionOverflow { .. } => Error::Argument, _ => parse("JPEG 2000 codestream"), } } #[cfg(feature = "rust-j2k")] #[allow( clippy::needless_pass_by_value, reason = "map_err supplies owned non-Copy backend errors and the public error intentionally does not retain backend types" )] fn map_rust_encode_error(error: rust_j2k::J2kError) -> Error { match error { rust_j2k::J2kError::Buffer(_) | rust_j2k::J2kError::InvalidSamples { .. } | rust_j2k::J2kError::DimensionOverflow { .. } | rust_j2k::J2kError::RateTargetUnreachable { .. } => Error::Argument, _ => Error::InvalidOperation, } } fn checked_dimensions(width: u32, height: u32, max_pixels: usize) -> Result<(i32, i32), Error> { let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?; let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?; let pixels = width_usize .checked_mul(height_usize) .ok_or(Error::Argument)?; if width == 0 || height == 0 || pixels > max_pixels || pixels > DEFAULT_MAX_PIXELS { return Err(Error::Argument); } Ok(( i32::try_from(width).map_err(|_| Error::Argument)?, i32::try_from(height).map_err(|_| Error::Argument)?, )) } const fn parse(context: &'static str) -> Error { Error::Parse { position: 0, context, } } #[cfg(all(test, feature = "jpeg2000"))] mod tests { use super::*; use std::io::Cursor; fn rgba(width: i32, height: i32) -> ManagedImage { let mut image = ManagedImage::new( width, height, ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA, ) .unwrap(); for pixel in 0..image.red.len() { image.red[pixel] = u8::try_from((pixel * 17) & 255).unwrap(); image.green[pixel] = u8::try_from((pixel * 29 + 3) & 255).unwrap(); image.blue[pixel] = u8::try_from((pixel * 43 + 7) & 255).unwrap(); image.alpha[pixel] = u8::try_from((pixel * 11 + 101) & 255).unwrap(); } image } #[test] fn lossless_codestream_and_jp2_round_trip_all_channels() { let source = rgba(17, 9); for format in [J2kFormat::Codestream, J2kFormat::Jp2] { let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format)) .expect("encode"); let decoded = J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).expect("decode"); assert_eq!(decoded, source); } } #[test] fn alpha_only_substitution_matches_managed_image_creator() { let mut source = ManagedImage::new(2, 1, ManagedImageImageChannels::ALPHA).unwrap(); source.alpha.copy_from_slice(&[17, 231]); let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); let decoded = J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap(); assert_eq!(decoded.red, [17, 231]); assert_eq!(decoded.green, [17, 231]); assert_eq!(decoded.blue, [17, 231]); assert_eq!(decoded.alpha, [255, 255]); } #[test] fn discard_levels_reduce_dimensions_and_stream_boundary_is_bounded() { let source = rgba(64, 32); let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); let reduced = J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default().with_discard_levels(1)) .unwrap(); assert_eq!((reduced.width, reduced.height), (32, 16)); let codec = J2kCodec::new(J2kDecodeOptions::default().with_limits(8, 64)); assert_eq!( codec.decode(Box::new(Cursor::new(encoded))), Err(Error::Argument) ); let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); assert_eq!( J2kCodec::decode_bytes( &encoded, J2kDecodeOptions::default().with_limits(encoded.len(), 128), ), Err(Error::Argument) ); } #[test] fn invalid_data_and_lossy_settings_fail_without_panicking() { assert!(matches!( J2kCodec::decode_bytes(b"not jpeg2000", J2kDecodeOptions::default()), Err(Error::Parse { .. }) )); let source = rgba(2, 2); assert_eq!( J2kCodec::encode( &source, J2kEncodeOptions::default().with_compression(J2kCompression::Lossy { compression_ratio: f32::NAN, }), ), Err(Error::Argument) ); assert!(matches!( J2kCodec::decode_bytes( b"\0\0\0\x0cjP \r\n\x87\n\0\0\0", J2kDecodeOptions::default(), ), Err(Error::Parse { .. }) )); assert_eq!( J2kCodec::encode( &source, J2kEncodeOptions::default().with_max_encoded_bytes(32), ), Err(Error::Argument) ); } #[test] fn lossy_mode_preserves_layout_with_bounded_sample_error() { let source = rgba(64, 64); let encoded = J2kCodec::encode( &source, J2kEncodeOptions::default() .with_format(J2kFormat::Jp2) .with_compression(J2kCompression::Lossy { compression_ratio: 8.0, }), ) .expect("lossy encode"); let decoded = J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default().with_quality_layers(1)) .expect("lossy decode"); assert_eq!( (decoded.width, decoded.height, decoded.channels), (source.width, source.height, source.channels) ); let total_error: u64 = source .red .iter() .chain(&source.green) .chain(&source.blue) .chain(&source.alpha) .zip( decoded .red .iter() .chain(&decoded.green) .chain(&decoded.blue) .chain(&decoded.alpha), ) .map(|(expected, actual)| u64::from(expected.abs_diff(*actual))) .sum(); let samples = u64::try_from(source.red.len() * 4).unwrap(); assert!( total_error > 0, "lossy mode unexpectedly reconstructed exactly" ); assert!(total_error / samples < 32, "mean sample error is too high"); } #[test] fn openjpeg_2_5_4_golden_retains_sixteen_bit_samples() { // Generated by OpenJPEG 2.5.4 `opj_compress` from the deterministic // 64x64 unsigned 16-bit gradient asserted below. const GOLDEN_HEX: &str = "ff4fff5100290000000000400000004000000000000000000000004000000040000000000000000000010f0101ff52000c00000001000504040001ff5c00134080888890888890888890888890888890ff640025000143726561746564206279204f70656e4a5045472076657273696f6e20322e352e34ff90000a00000000011c0001ff93dff890500c58e2753ee5f7dd26b3c7fe1811000fa8120007ce080d020629b05f7dbf0c199c5f0c44c3ff0306c001f3858000f90180221a085d6dcb5f4686076b4a7f00d09a4b7f01627fc1ff4005e000f9c440007c80c036a199ae63a3628986621949a89ff8bdd889db3a01813f439e44760735fc3f3e871dc0ff0278001f20f80007c2385fa7c788a00db0790644473e98d48fd92b5faff3c71e10932fde0e8f81be8b5bafccacfdd7cf9f9eab0ab177092bc3e079cc1037c0679d9aecfa3be703c07f2670001f09700007438b6201aba8dfc3791d615480235541001a680dce59014e5d9a88d45e9373f9519239613060c182ef7f7816874a3e947ceeaf7fe19cdb5b3899ff0636ff7fe18c4fffd9"; let encoded = GOLDEN_HEX .as_bytes() .chunks_exact(2) .map(|pair| { let pair = std::str::from_utf8(pair).unwrap(); u8::from_str_radix(pair, 16).unwrap() }) .collect::>(); let decoded = J2kCodec::decode_interleaved(&encoded, J2kDecodeOptions::default()) .expect("decode OpenJPEG golden"); assert_eq!((decoded.width(), decoded.height()), (64, 64)); assert_eq!(decoded.number_of_components(), 1); let component = &decoded.components()[0]; assert_eq!((component.precision(), component.is_signed()), (16, false)); let expected = (0..64) .flat_map(|y| (0..64).map(move |x| (x * 65_535 / 64) ^ ((y * 3) & 0xffff))) .collect::>(); assert_eq!(component.samples(), expected); } } #[cfg(all(test, feature = "rust-j2k"))] mod rust_tests { use super::*; use std::io::Cursor; fn rgba(width: i32, height: i32) -> ManagedImage { let mut image = ManagedImage::new( width, height, ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA, ) .unwrap(); for pixel in 0..image.red.len() { image.red[pixel] = u8::try_from((pixel * 17) & 255).unwrap(); image.green[pixel] = u8::try_from((pixel * 29 + 3) & 255).unwrap(); image.blue[pixel] = u8::try_from((pixel * 43 + 7) & 255).unwrap(); image.alpha[pixel] = u8::try_from((pixel * 11 + 101) & 255).unwrap(); } image } #[test] fn lossless_codestream_and_jp2_round_trip_all_channels() { let source = rgba(17, 9); for format in [J2kFormat::Codestream, J2kFormat::Jp2] { let encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format)) .expect("encode"); let decoded = RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).expect("decode"); assert_eq!(decoded, source); } } #[test] fn alpha_only_substitution_matches_core_j2k() { let mut source = ManagedImage::new(2, 1, ManagedImageImageChannels::ALPHA).unwrap(); source.alpha.copy_from_slice(&[17, 231]); let encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); let decoded = RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap(); assert_eq!(decoded.red, [17, 231]); assert_eq!(decoded.green, [17, 231]); assert_eq!(decoded.blue, [17, 231]); assert_eq!(decoded.alpha, [255, 255]); } #[test] fn reduced_resolution_and_stream_limits_are_enforced() { let source = rgba(64, 32); let encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); let reduced = RustJ2kCodec::decode_bytes( &encoded, J2kDecodeOptions::default().with_discard_levels(1), ) .unwrap(); assert_eq!((reduced.width, reduced.height), (32, 16)); let codec = RustJ2kCodec::new(J2kDecodeOptions::default().with_limits(8, 64)); assert_eq!( codec.decode(Box::new(Cursor::new(encoded))), Err(Error::Argument) ); } #[test] fn malformed_limits_and_output_caps_fail_without_panicking() { assert!(matches!( RustJ2kCodec::decode_bytes(b"not jpeg2000", J2kDecodeOptions::default()), Err(Error::Parse { .. }) )); let source = rgba(2, 2); assert_eq!( RustJ2kCodec::encode( &source, J2kEncodeOptions::default().with_compression(J2kCompression::Lossy { compression_ratio: f32::NAN, }), ), Err(Error::Argument) ); assert_eq!( RustJ2kCodec::encode( &source, J2kEncodeOptions::default().with_max_encoded_bytes(32), ), Err(Error::Argument) ); } #[test] fn lossy_ratios_preserve_layout_and_reduce_size() { let source = rgba(128, 128); let low = RustJ2kCodec::encode( &source, J2kEncodeOptions::default().with_compression(J2kCompression::Lossy { compression_ratio: 4.0, }), ) .unwrap(); let high = RustJ2kCodec::encode( &source, J2kEncodeOptions::default().with_compression(J2kCompression::Lossy { compression_ratio: 12.0, }), ) .unwrap(); assert!(high.len() < low.len()); let decoded = RustJ2kCodec::decode_bytes(&high, J2kDecodeOptions::default().with_quality_layers(1)) .unwrap(); assert_eq!( (decoded.width, decoded.height, decoded.channels), (source.width, source.height, source.channels) ); assert_ne!(decoded, source); } #[test] fn progressive_quality_layers_are_applied_in_order() { let source = rgba(128, 128); let encoded = RustJ2kCodec::encode( &source, J2kEncodeOptions::default().with_compression(J2kCompression::Lossy { compression_ratio: 8.0, }), ) .unwrap(); let first = RustJ2kCodec::decode_bytes( &encoded, J2kDecodeOptions::default().with_quality_layers(1), ) .unwrap(); let second = RustJ2kCodec::decode_bytes( &encoded, J2kDecodeOptions::default().with_quality_layers(2), ) .unwrap(); let complete = RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap(); let error = |decoded: &ManagedImage| -> u64 { source .red .iter() .chain(&source.green) .chain(&source.blue) .chain(&source.alpha) .zip( decoded .red .iter() .chain(&decoded.green) .chain(&decoded.blue) .chain(&decoded.alpha), ) .map(|(expected, actual)| u64::from(expected.abs_diff(*actual))) .sum() }; assert!(error(&first) >= error(&second)); assert!(error(&second) >= error(&complete)); assert_ne!(first, complete); } #[test] fn strict_rejects_missing_eoc_and_permissive_repairs_it() { let source = rgba(16, 16); for format in [J2kFormat::Codestream, J2kFormat::Jp2] { let mut encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format)) .unwrap(); assert!(encoded.ends_with(&[0xff, 0xd9])); encoded.truncate(encoded.len() - 2); assert!(RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).is_err()); assert_eq!( RustJ2kCodec::decode_bytes( &encoded, J2kDecodeOptions::default().with_strict_mode(false), ) .unwrap(), source ); } } #[test] fn native_planes_preserve_mixed_precision_signedness_and_five_component_order() { let width = 8_u32; let height = 4_u32; let pixels = usize::try_from(width * height).unwrap(); let specifications = [ (8_u8, false), (12_u8, true), (10_u8, false), (8_u8, false), (16_u8, true), ]; let mut plane_bytes = Vec::new(); let mut expected = Vec::new(); for (component, (precision, signed)) in specifications.iter().copied().enumerate() { let mut bytes = Vec::new(); let mut values = Vec::new(); for pixel in 0..pixels { let value = if signed { i32::try_from(pixel).unwrap() - 15 - i32::try_from(component).unwrap() } else { i32::try_from(pixel * (component + 1)).unwrap() }; values.push(value); let width = usize::from(precision).div_ceil(8); bytes.extend_from_slice(&value.to_le_bytes()[..width]); } plane_bytes.push(bytes); expected.push(values); } let planes = plane_bytes .iter() .zip(specifications) .map( |(data, (bit_depth, signed))| rust_j2k::J2kLosslessTypedComponentPlane { data, x_rsiz: 1, y_rsiz: 1, bit_depth, signed, }, ) .collect::>(); let samples = rust_j2k::J2kLosslessTypedComponentSamples::new(&planes, width, height).unwrap(); let encoded = rust_j2k::encode_j2k_lossless_typed_components( samples, &rust_j2k::J2kLosslessEncodeOptions::default() .with_cpu_only_backend() .with_reversible_transform(rust_j2k::ReversibleTransform::None53), ) .unwrap(); let decoded = RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default()) .unwrap(); assert_eq!(decoded.number_of_components(), 5); for (index, component) in decoded.components().iter().enumerate() { assert_eq!(component.precision(), specifications[index].0); assert_eq!(component.is_signed(), specifications[index].1); assert_eq!(component.samples(), expected[index]); } } #[test] fn independent_gray_and_rgb_codestreams_decode_without_rgba_assumptions() { for components in [1_u16, 3] { let width = 11_u32; let height = 7_u32; let samples = (0..usize::try_from(width * height).unwrap() * usize::from(components)) .map(|index| u8::try_from((index * 23 + 7) & 255).unwrap()) .collect::>(); let input = rust_j2k::J2kLosslessSamples::new(&samples, width, height, components, 8, false) .unwrap(); let encoded = rust_j2k::encode_j2k_lossless( input, &rust_j2k::J2kLosslessEncodeOptions::default().with_cpu_only_backend(), ) .unwrap(); let decoded = RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default()) .unwrap(); assert_eq!(decoded.number_of_components(), usize::from(components)); let managed = interleaved_to_managed(&decoded).unwrap(); assert_eq!((managed.width, managed.height), (11, 7)); } } #[test] fn unsupported_subsampling_and_excess_components_are_rejected() { let width = 8_u32; let height = 8_u32; let full = vec![37_u8; usize::try_from(width * height).unwrap()]; let half_width = vec![91_u8; usize::try_from(width / 2 * height).unwrap()]; let planes = [ rust_j2k::J2kLosslessTypedComponentPlane { data: &full, x_rsiz: 1, y_rsiz: 1, bit_depth: 8, signed: false, }, rust_j2k::J2kLosslessTypedComponentPlane { data: &half_width, x_rsiz: 2, y_rsiz: 1, bit_depth: 8, signed: false, }, ]; let samples = rust_j2k::J2kLosslessTypedComponentSamples::new(&planes, width, height).unwrap(); let subsampled = rust_j2k::encode_j2k_lossless_typed_components( samples, &rust_j2k::J2kLosslessEncodeOptions::default() .with_cpu_only_backend() .with_reversible_transform(rust_j2k::ReversibleTransform::None53), ) .unwrap(); assert!(matches!( RustJ2kCodec::decode_interleaved(&subsampled.codestream, J2kDecodeOptions::default()), Err(Error::Argument) )); let six_components = vec![19_u8; usize::try_from(width * height).unwrap() * 6]; let samples = rust_j2k::J2kLosslessSamples::new(&six_components, width, height, 6, 8, false).unwrap(); let encoded = rust_j2k::encode_j2k_lossless( samples, &rust_j2k::J2kLosslessEncodeOptions::default().with_cpu_only_backend(), ) .unwrap(); assert_eq!( RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default()), Err(Error::Argument) ); } #[cfg(feature = "jpeg2000")] #[test] fn openjpeg_and_rust_backends_interoperate_both_directions() { let source = rgba(32, 24); let rust = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); assert_eq!( J2kCodec::decode_bytes(&rust, J2kDecodeOptions::default()).unwrap(), source ); let native = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap(); assert_eq!( RustJ2kCodec::decode_bytes(&native, J2kDecodeOptions::default()).unwrap(), source ); } }