Files
MetaCrate/crates/libremetaverse-imaging/src/jpeg2000.rs
Chili Palmer 0bab5b4c8d
Some checks failed
CI / required (push) Failing after 3m18s
feat(imaging): add pure-Rust JPEG 2000 backend (#109)
2026-08-13 05:44:30 +00:00

1912 lines
67 KiB
Rust

//! 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<InterleavedImage, Error> {
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<ManagedImage, Error> {
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<Vec<u8>, 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<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
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<InterleavedImage, Error> {
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<ManagedImage, Error> {
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<Vec<u8>, 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<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
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<Vec<u8>, 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<InterleavedImage, Error> {
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<InterleavedImage, Error> {
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<Vec<i32>, 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<InterleavedImage, Error> {
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<InterleavedImage, Error> {
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<Vec<i32>, 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<InterleavedImage, Error> {
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<ManagedImage, Error> {
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<Vec<u8>, 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<Vec<u8>, 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<Cow<'_, [u8]>, 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<Vec<u8>, 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<MainHeader, Error> {
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<u8>,
) -> Result<usize, Error> {
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<usize>) -> 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<u16, Error> {
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<i32>; 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<ManagedImage, Error> {
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<ManagedImageImageChannels, Error> {
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<J2kFormat, Error> {
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<u64, Error> {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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
);
}
}