1912 lines
67 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|