Implement optional JPEG2000 codec adapter (#40)
This commit is contained in:
53
.gitea/workflows/jpeg2000.yml
Normal file
53
.gitea/workflows/jpeg2000.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
name: JPEG 2000 feature
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install OpenJPEG 2.5.4
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes build-essential cmake pkg-config
|
||||
git clone --branch v2.5.4 --depth 1 https://github.com/uclouvain/openjpeg.git /tmp/openjpeg-2.5.4
|
||||
cmake -S /tmp/openjpeg-2.5.4 -B /tmp/openjpeg-build -DBUILD_CODEC=OFF -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build /tmp/openjpeg-build --parallel
|
||||
sudo cmake --install /tmp/openjpeg-build
|
||||
sudo ldconfig
|
||||
- name: Build and test optional feature
|
||||
run: |
|
||||
cargo test -p libremetaverse-imaging --features jpeg2000
|
||||
cargo check -p libremetaverse --features jpeg2000
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install OpenJPEG
|
||||
run: brew install openjpeg pkg-config
|
||||
- name: Build and test optional feature
|
||||
run: |
|
||||
cargo test -p libremetaverse-imaging --features jpeg2000
|
||||
cargo check -p libremetaverse --features jpeg2000
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
VCPKGRS_DYNAMIC: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install OpenJPEG
|
||||
shell: pwsh
|
||||
run: vcpkg install openjpeg:x64-windows
|
||||
- name: Build and test optional feature
|
||||
shell: pwsh
|
||||
run: |
|
||||
cargo test -p libremetaverse-imaging --features jpeg2000
|
||||
cargo check -p libremetaverse --features jpeg2000
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
/target/
|
||||
/crates/libremetaverse-openjpeg/target/
|
||||
/crates/libremetaverse-openjpeg/Cargo.lock
|
||||
/tests/api-compile/target/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -218,6 +218,7 @@ dependencies = [
|
||||
name = "libremetaverse-imaging"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"libremetaverse-openjpeg",
|
||||
"libremetaverse-types",
|
||||
]
|
||||
|
||||
@@ -236,6 +237,14 @@ dependencies = [
|
||||
"libremetaverse-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libremetaverse-openjpeg"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libremetaverse-prim-mesher"
|
||||
version = "0.0.1"
|
||||
@@ -359,6 +368,12 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -510,6 +525,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
exclude = ["crates/libremetaverse-openjpeg"]
|
||||
members = [
|
||||
"crates/libremetaverse-types",
|
||||
"crates/libremetaverse-structured-data",
|
||||
|
||||
@@ -141,5 +141,14 @@ Legacy and DX10 BC1 through BC5 decoding is built in. The default `dds-bc67`
|
||||
feature adds BC6H/BC7 decoding through the safe, pure-Rust `bcdec_rs` crate;
|
||||
disabling default features removes that optional dependency and makes those
|
||||
two formats return a typed unsupported-format error.
|
||||
JPEG 2000 is available through the opt-in `jpeg2000` feature. It provides raw
|
||||
J2K and JP2 lossless/lossy encoding and decoding, preserves one through five
|
||||
component order, precision, signedness, and alpha metadata until explicit byte
|
||||
conversion, and bounds encoded input, output, dimensions, and decoded samples.
|
||||
The compatibility encoder reproduces CoreJ2K's four-plane RGB/alpha view,
|
||||
including its alpha-only and opaque-alpha substitutions. See
|
||||
[`crates/libremetaverse-openjpeg/README.md`](crates/libremetaverse-openjpeg/README.md)
|
||||
for OpenJPEG prerequisites, licensing, and deployment details. Default builds
|
||||
do not discover or link OpenJPEG.
|
||||
The controlled audit aggregates every expected failure by standardized C#
|
||||
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.
|
||||
|
||||
@@ -413,7 +413,7 @@ MSRV, features, and licenses at adoption time.
|
||||
| rate limiting | [`governor` 0.10.4](https://crates.io/crates/governor/0.10.4) | Candidate for caps categories. First reproduce burst/refill/cancellation behavior with deterministic clock tests. |
|
||||
| compression and tar archives | [`flate2` 1.1.9](https://crates.io/crates/flate2/1.1.9), [`tar` 0.4.46](https://crates.io/crates/tar/0.4.46) | Enforce decompressed-size, path traversal, and entry-count limits on untrusted OAR data. |
|
||||
| XML and URL | [`quick-xml` 0.41.0](https://crates.io/crates/quick-xml/0.41.0), [`url` 2.5.8](https://crates.io/crates/url/2.5.8), [`base64` 0.23.1](https://crates.io/crates/base64/0.23.1) | Streaming XML for LLSD/login; retain exact URL escaping behavior with fixtures. |
|
||||
| CoreJ2K 2.3.3.91 | [`jpeg2k` 0.10.1](https://crates.io/crates/jpeg2k/0.10.1) | Defaults to OpenJPEG/native bindings. Keep behind a codec trait and feature; test channel order, alpha, dimensions, discard levels, and malformed input. |
|
||||
| CoreJ2K 2.3.3.91 | system [OpenJPEG](https://www.openjpeg.org/) 2.5.4+ through the private `libremetaverse-openjpeg` adapter | Opt-in `jpeg2000` feature; checked-in minimal bindings avoid a libclang build dependency. Bounded memory streams and header validation isolate native code. Golden tests cover channel order, alpha, 16-bit precision, dimensions, discard levels, lossless/lossy modes, and malformed input. |
|
||||
| SkiaSharp 4.150.1 | [`skia-safe` 0.99.0](https://crates.io/crates/skia-safe/0.99.0) | MSRV 1.85 and native/binary-cache build. Optional adapter only; core image APIs must not leak Skia types. |
|
||||
| Pfim 0.11.4 | [`image` 0.25.10](https://crates.io/crates/image/0.25.10), [`ddsfile` 0.6.0](https://crates.io/crates/ddsfile/0.6.0) | `image` covers TGA and common DDS decoding; `ddsfile` exposes DDS container details. Golden files decide whether both are needed. |
|
||||
| OggVorbisEncoder 1.2.2 | [`vorbis_rs` 0.5.6](https://crates.io/crates/vorbis_rs/0.5.6) | BSD-3-Clause, MSRV 1.82, backed by C libraries. Feature-gate native audio encoding. |
|
||||
|
||||
@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
||||
|
||||
| Assembly | Types | Members | Status |
|
||||
|---|---:|---:|---|
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 1 type / 3 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 3 types / 7 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
|
||||
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | callable failure-only shim |
|
||||
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
|
||||
|
||||
@@ -7,8 +7,13 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Texture codec abstractions for the MetaCrate LibreMetaverse rewrite"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
jpeg2000 = ["dep:libremetaverse-openjpeg"]
|
||||
|
||||
[dependencies]
|
||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||
libremetaverse-openjpeg = { path = "../libremetaverse-openjpeg", optional = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
298
crates/libremetaverse-imaging/src/codec.rs
Normal file
298
crates/libremetaverse-imaging/src/codec.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! Project-owned boundaries for JPEG 2000 codec types exposed by the C# API.
|
||||
|
||||
use crate::{DEFAULT_MAX_PIXELS, Error, ManagedImage};
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A type-erased decoded image returned by an image creator.
|
||||
pub trait IImage: Any + Debug + Send + Sync {
|
||||
/// Returns the concrete image wrapper for checked downcasting.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Returns the already-created managed image when this is a compatible
|
||||
/// decode target.
|
||||
fn as_managed_image(&self) -> Option<&ManagedImage> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker corresponding to `CoreJ2K`'s image-creator boundary.
|
||||
pub trait IImageCreator: Send + Sync {}
|
||||
|
||||
/// Typed marker corresponding to `CoreJ2K`'s generic image creator.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ImageCreator<T>(pub PhantomData<T>);
|
||||
|
||||
/// One decoded JPEG 2000 component at its original integer precision.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InterleavedComponent {
|
||||
precision: u8,
|
||||
signed: bool,
|
||||
alpha: bool,
|
||||
samples: Vec<i32>,
|
||||
}
|
||||
|
||||
impl InterleavedComponent {
|
||||
/// Creates a checked component without reducing its sample precision.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::Argument`] for precision outside 1 through 31 bits or
|
||||
/// for samples outside the declared signed or unsigned range.
|
||||
pub fn new(precision: u8, signed: bool, alpha: bool, samples: Vec<i32>) -> Result<Self, Error> {
|
||||
if !(1..=31).contains(&precision) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (minimum, maximum) = component_range(precision, signed);
|
||||
if samples
|
||||
.iter()
|
||||
.any(|sample| i64::from(*sample) < minimum || i64::from(*sample) > maximum)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
precision,
|
||||
signed,
|
||||
alpha,
|
||||
samples,
|
||||
})
|
||||
}
|
||||
|
||||
/// Declared component precision in bits.
|
||||
#[must_use]
|
||||
pub const fn precision(&self) -> u8 {
|
||||
self.precision
|
||||
}
|
||||
|
||||
/// Whether component samples use signed representation.
|
||||
#[must_use]
|
||||
pub const fn is_signed(&self) -> bool {
|
||||
self.signed
|
||||
}
|
||||
|
||||
/// Whether the container marks this component as alpha.
|
||||
#[must_use]
|
||||
pub const fn is_alpha(&self) -> bool {
|
||||
self.alpha
|
||||
}
|
||||
|
||||
/// Original integer samples, in top-left row-major order.
|
||||
#[must_use]
|
||||
pub fn samples(&self) -> &[i32] {
|
||||
&self.samples
|
||||
}
|
||||
}
|
||||
|
||||
/// A bounded decoded image retaining component precision and order.
|
||||
///
|
||||
/// The historical external type calls this representation interleaved, while
|
||||
/// its public conversion API is component-oriented. Keeping the components in
|
||||
/// separate owned planes avoids a second full-image allocation and retains the
|
||||
/// original JPEG 2000 integer samples until byte conversion is requested.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InterleavedImage {
|
||||
width: i32,
|
||||
height: i32,
|
||||
components: Vec<InterleavedComponent>,
|
||||
}
|
||||
|
||||
impl InterleavedImage {
|
||||
/// Creates a checked decoded component image.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::Argument`] for invalid/over-limit dimensions, zero or
|
||||
/// more than five components, or a plane whose size differs from
|
||||
/// `width * height`.
|
||||
pub fn new(
|
||||
width: i32,
|
||||
height: i32,
|
||||
components: Vec<InterleavedComponent>,
|
||||
) -> Result<Self, Error> {
|
||||
let pixels = checked_pixels(width, height)?;
|
||||
if components.is_empty()
|
||||
|| components.len() > 5
|
||||
|| components
|
||||
.iter()
|
||||
.any(|component| component.samples.len() != pixels)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
pixels
|
||||
.checked_mul(components.len())
|
||||
.filter(|samples| *samples <= DEFAULT_MAX_PIXELS * 5)
|
||||
.ok_or(Error::Argument)?;
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
components,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decoded width after any discard-level reduction.
|
||||
#[must_use]
|
||||
pub const fn width(&self) -> i32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Decoded height after any discard-level reduction.
|
||||
#[must_use]
|
||||
pub const fn height(&self) -> i32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// Number of decoded components in codestream order.
|
||||
#[must_use]
|
||||
pub fn number_of_components(&self) -> usize {
|
||||
self.components.len()
|
||||
}
|
||||
|
||||
/// Component metadata and original samples.
|
||||
#[must_use]
|
||||
pub fn components(&self) -> &[InterleavedComponent] {
|
||||
&self.components
|
||||
}
|
||||
|
||||
/// Scales one component into an 8-bit plane using CoreJ2K-compatible full
|
||||
/// range conversion.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::IndexOutOfRange`] for an invalid component index and
|
||||
/// [`Error::Argument`] when `destination` is not exactly one image plane.
|
||||
pub fn to_component_bytes(
|
||||
&self,
|
||||
component_index: usize,
|
||||
destination: &mut [u8],
|
||||
) -> Result<(), Error> {
|
||||
let component = self
|
||||
.components
|
||||
.get(component_index)
|
||||
.ok_or(Error::IndexOutOfRange)?;
|
||||
if destination.len() != component.samples.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
for (destination, sample) in destination.iter_mut().zip(&component.samples) {
|
||||
*destination = scale_sample(*sample, component.precision, component.signed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode-side sample source corresponding to `CoreJ2K`'s block image source.
|
||||
pub trait BlkImgDataSrc: Debug + Send + Sync {
|
||||
/// Image width.
|
||||
fn width(&self) -> i32;
|
||||
/// Image height.
|
||||
fn height(&self) -> i32;
|
||||
/// Number of presented components.
|
||||
fn number_of_components(&self) -> usize;
|
||||
/// Nominal range bits for one component.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
|
||||
fn nominal_range_bits(&self, component_index: usize) -> Result<u8, Error>;
|
||||
/// Fixed-point fractional bits for one component.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
|
||||
fn fixed_point(&self, component_index: usize) -> Result<u8, Error>;
|
||||
/// Whether one presented component is signed at the source.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::IndexOutOfRange`] for an invalid component index.
|
||||
fn is_original_signed(&self, component_index: usize) -> Result<bool, Error>;
|
||||
/// Returns a checked top-left row-major rectangle with the source DC offset
|
||||
/// applied, exactly as `CoreJ2K` expects.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a typed index or argument error for an invalid component or
|
||||
/// rectangle.
|
||||
fn component_block(
|
||||
&self,
|
||||
component_index: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<Vec<i32>, Error>;
|
||||
}
|
||||
|
||||
fn checked_pixels(width: i32, height: i32) -> Result<usize, Error> {
|
||||
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
|
||||
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
|
||||
if width == 0 || height == 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
width
|
||||
.checked_mul(height)
|
||||
.filter(|pixels| *pixels <= DEFAULT_MAX_PIXELS)
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
fn component_range(precision: u8, signed: bool) -> (i64, i64) {
|
||||
if signed {
|
||||
let magnitude = 1_i64 << (precision - 1);
|
||||
(-magnitude, magnitude - 1)
|
||||
} else {
|
||||
(0, (1_i64 << precision) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_sample(sample: i32, precision: u8, signed: bool) -> u8 {
|
||||
if signed {
|
||||
let old_max = 1_i64 << (precision - 1);
|
||||
let scaled = (i64::from(sample) * 128) / old_max + 128;
|
||||
u8::try_from(scaled.clamp(0, 255)).unwrap_or_default()
|
||||
} else {
|
||||
let old_max = (1_u64 << precision) - 1;
|
||||
let scaled = (u64::try_from(sample).unwrap_or_default() * 255) / old_max;
|
||||
u8::try_from(scaled).unwrap_or(u8::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn component_conversion_preserves_precision_and_signed_range() {
|
||||
let unsigned = InterleavedComponent::new(16, false, false, vec![0, 32_768, 65_535])
|
||||
.expect("16-bit component");
|
||||
let signed = InterleavedComponent::new(8, true, false, vec![-128, 0, 127])
|
||||
.expect("signed component");
|
||||
let unsigned = InterleavedImage::new(3, 1, vec![unsigned]).expect("unsigned image");
|
||||
let signed = InterleavedImage::new(3, 1, vec![signed]).expect("signed image");
|
||||
let mut bytes = [0; 3];
|
||||
unsigned
|
||||
.to_component_bytes(0, &mut bytes)
|
||||
.expect("unsigned bytes");
|
||||
assert_eq!(bytes, [0, 127, 255]);
|
||||
signed
|
||||
.to_component_bytes(0, &mut bytes)
|
||||
.expect("signed bytes");
|
||||
assert_eq!(bytes, [0, 128, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaved_image_rejects_bad_layouts_and_ranges() {
|
||||
assert_eq!(
|
||||
InterleavedComponent::new(0, false, false, vec![]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
InterleavedComponent::new(8, false, false, vec![256]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
let component = InterleavedComponent::new(8, false, false, vec![0]).unwrap();
|
||||
assert_eq!(
|
||||
InterleavedImage::new(2, 1, vec![component]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
}
|
||||
707
crates/libremetaverse-imaging/src/jpeg2000.rs
Normal file
707
crates/libremetaverse-imaging/src/jpeg2000.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
//! Optional JPEG 2000 codec backed by a bounded system `OpenJPEG` adapter.
|
||||
|
||||
use crate::codec::{InterleavedComponent, InterleavedImage};
|
||||
use crate::{
|
||||
DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS, Error, ITextureCodec, ManagedImage,
|
||||
ManagedImageImageChannels,
|
||||
};
|
||||
use libremetaverse_openjpeg as openjpeg;
|
||||
use libremetaverse_types::compat::ReadWrite;
|
||||
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)]
|
||||
pub struct J2kCodec {
|
||||
decode_options: J2kDecodeOptions,
|
||||
}
|
||||
|
||||
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 {
|
||||
if !compression_ratio.is_finite() || compression_ratio < 1.0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
}
|
||||
encode_with_openjpeg(image, options)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
const fn backend_format(format: J2kFormat) -> openjpeg::Format {
|
||||
match format {
|
||||
J2kFormat::Codestream => openjpeg::Format::J2k,
|
||||
J2kFormat::Jp2 => openjpeg::Format::Jp2,
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
const fn map_encode_error(error: openjpeg::Error) -> Error {
|
||||
match error {
|
||||
openjpeg::Error::InvalidInput | openjpeg::Error::LimitExceeded => Error::Argument,
|
||||
openjpeg::Error::Allocation | openjpeg::Error::Codec => 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(test)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
extern crate self as libremetaverse_imaging;
|
||||
|
||||
pub mod codec {
|
||||
pub trait IImage {}
|
||||
pub trait IImageCreator {}
|
||||
pub struct ImageCreator<T>(pub std::marker::PhantomData<T>);
|
||||
pub struct InterleavedImage;
|
||||
pub trait BlkImgDataSrc {}
|
||||
}
|
||||
pub mod codec;
|
||||
|
||||
#[cfg(feature = "jpeg2000")]
|
||||
mod jpeg2000;
|
||||
|
||||
mod generated;
|
||||
mod managed_image;
|
||||
|
||||
pub use generated::*;
|
||||
#[cfg(feature = "jpeg2000")]
|
||||
pub use jpeg2000::{J2kCodec, J2kCompression, J2kDecodeOptions, J2kEncodeOptions, J2kFormat};
|
||||
pub use libremetaverse_types::Error;
|
||||
pub use managed_image::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS};
|
||||
|
||||
22
crates/libremetaverse-openjpeg/Cargo.toml
Normal file
22
crates/libremetaverse-openjpeg/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "libremetaverse-openjpeg"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
license = "BSD-3-Clause"
|
||||
publish = false
|
||||
description = "Safe, bounded OpenJPEG 2.5.4 adapter for MetaCrate"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3"
|
||||
vcpkg = "0.2"
|
||||
|
||||
# Unsafe code is permitted only in this workspace member: it is the reviewed
|
||||
# native ABI boundary. All callers use its safe, owned Rust API.
|
||||
[lints.rust]
|
||||
unsafe_code = "allow"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
38
crates/libremetaverse-openjpeg/README.md
Normal file
38
crates/libremetaverse-openjpeg/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# MetaCrate OpenJPEG adapter
|
||||
|
||||
This private crate is the unsafe implementation boundary for MetaCrate's
|
||||
optional `jpeg2000` feature. Its public surface is safe Rust: native handles,
|
||||
callbacks, and OpenJPEG image structures never cross into the imaging API.
|
||||
The adapter uses checked arithmetic and bounded memory streams, validates
|
||||
header dimensions and component counts before decoded sample-plane allocation,
|
||||
and copies decoded samples into owned Rust vectors.
|
||||
|
||||
## Native prerequisite
|
||||
|
||||
OpenJPEG **2.5.4 or newer** is required. Versions 2.5.1 through 2.5.3 are not
|
||||
accepted because they are affected by CVE-2025-54874. No bindgen or libclang
|
||||
installation is needed; the crate contains the minimal public ABI declarations
|
||||
used by the adapter.
|
||||
|
||||
- Linux and other Unix targets: install OpenJPEG development headers/libraries
|
||||
and `pkg-config`; `pkg-config --modversion libopenjp2` must report 2.5.4 or
|
||||
newer.
|
||||
- macOS: install `openjpeg` and `pkg-config` (for example with Homebrew). This
|
||||
is the same system-library path used on Linux; no Apple-only API is used.
|
||||
- Windows MSVC: install `openjpeg` with vcpkg and expose `VCPKG_ROOT` to Cargo.
|
||||
Use the triplet matching the Rust target and deploy the resulting OpenJPEG
|
||||
DLL beside the application or on its normal DLL search path when using a
|
||||
dynamic triplet.
|
||||
|
||||
Enable the adapter with `--features jpeg2000` on either
|
||||
`libremetaverse-imaging` or the umbrella `libremetaverse` crate. Without that
|
||||
feature, neither the build script nor native library discovery runs.
|
||||
|
||||
## License and redistribution
|
||||
|
||||
OpenJPEG is BSD-2-Clause licensed. MetaCrate discovers an installation supplied
|
||||
by the builder and does not redistribute OpenJPEG source or binaries. A product
|
||||
that redistributes an OpenJPEG shared or static library must also satisfy the
|
||||
OpenJPEG license's copyright and notice requirements. The ABI declarations in
|
||||
this crate follow OpenJPEG's public `openjpeg.h`; MetaCrate's adapter code is
|
||||
licensed under the repository's BSD-3-Clause license.
|
||||
16
crates/libremetaverse-openjpeg/build.rs
Normal file
16
crates/libremetaverse-openjpeg/build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
let target = env::var("TARGET").expect("Cargo provides TARGET");
|
||||
if target.contains("msvc") {
|
||||
vcpkg::Config::new()
|
||||
.find_package("openjpeg")
|
||||
.expect("jpeg2000 requires OpenJPEG 2.5.4 or newer from vcpkg");
|
||||
} else {
|
||||
pkg_config::Config::new()
|
||||
.atleast_version("2.5.4")
|
||||
.probe("libopenjp2")
|
||||
.expect("jpeg2000 requires pkg-config and OpenJPEG 2.5.4 or newer");
|
||||
}
|
||||
}
|
||||
279
crates/libremetaverse-openjpeg/src/ffi.rs
Normal file
279
crates/libremetaverse-openjpeg/src/ffi.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
//! Minimal `OpenJPEG` 2.5 public ABI used by this crate.
|
||||
//!
|
||||
//! These declarations follow `openjpeg.h` from `OpenJPEG` 2.5.4. Keeping the
|
||||
//! bindings here avoids requiring libclang merely to enable the codec.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_uint, c_void};
|
||||
|
||||
pub type OPJ_BOOL = c_int;
|
||||
pub type OPJ_CHAR = c_char;
|
||||
pub type OPJ_UINT16 = u16;
|
||||
pub type OPJ_INT32 = i32;
|
||||
pub type OPJ_UINT32 = u32;
|
||||
pub type OPJ_UINT64 = u64;
|
||||
pub type OPJ_OFF_T = i64;
|
||||
pub type OPJ_SIZE_T = usize;
|
||||
|
||||
pub type OPJ_RSIZ_CAPABILITIES = u32;
|
||||
pub type OPJ_CINEMA_MODE = u32;
|
||||
pub type OPJ_PROG_ORDER = i32;
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct OPJ_COLOR_SPACE(i32);
|
||||
|
||||
impl OPJ_COLOR_SPACE {
|
||||
pub const OPJ_CLRSPC_SRGB: Self = Self(1);
|
||||
pub const OPJ_CLRSPC_GRAY: Self = Self(2);
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct OPJ_CODEC_FORMAT(i32);
|
||||
|
||||
impl OPJ_CODEC_FORMAT {
|
||||
pub const OPJ_CODEC_J2K: Self = Self(0);
|
||||
pub const OPJ_CODEC_JP2: Self = Self(2);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_poc_t {
|
||||
pub resno0: OPJ_UINT32,
|
||||
pub compno0: OPJ_UINT32,
|
||||
pub layno1: OPJ_UINT32,
|
||||
pub resno1: OPJ_UINT32,
|
||||
pub compno1: OPJ_UINT32,
|
||||
pub layno0: OPJ_UINT32,
|
||||
pub precno0: OPJ_UINT32,
|
||||
pub precno1: OPJ_UINT32,
|
||||
pub prg1: OPJ_PROG_ORDER,
|
||||
pub prg: OPJ_PROG_ORDER,
|
||||
pub progorder: [OPJ_CHAR; 5],
|
||||
pub tile: OPJ_UINT32,
|
||||
pub tx0: OPJ_INT32,
|
||||
pub tx1: OPJ_INT32,
|
||||
pub ty0: OPJ_INT32,
|
||||
pub ty1: OPJ_INT32,
|
||||
pub layS: OPJ_UINT32,
|
||||
pub resS: OPJ_UINT32,
|
||||
pub compS: OPJ_UINT32,
|
||||
pub prcS: OPJ_UINT32,
|
||||
pub layE: OPJ_UINT32,
|
||||
pub resE: OPJ_UINT32,
|
||||
pub compE: OPJ_UINT32,
|
||||
pub prcE: OPJ_UINT32,
|
||||
pub txS: OPJ_UINT32,
|
||||
pub txE: OPJ_UINT32,
|
||||
pub tyS: OPJ_UINT32,
|
||||
pub tyE: OPJ_UINT32,
|
||||
pub dx: OPJ_UINT32,
|
||||
pub dy: OPJ_UINT32,
|
||||
pub lay_t: OPJ_UINT32,
|
||||
pub res_t: OPJ_UINT32,
|
||||
pub comp_t: OPJ_UINT32,
|
||||
pub prc_t: OPJ_UINT32,
|
||||
pub tx0_t: OPJ_UINT32,
|
||||
pub ty0_t: OPJ_UINT32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_cparameters_t {
|
||||
pub tile_size_on: OPJ_BOOL,
|
||||
pub cp_tx0: c_int,
|
||||
pub cp_ty0: c_int,
|
||||
pub cp_tdx: c_int,
|
||||
pub cp_tdy: c_int,
|
||||
pub cp_disto_alloc: c_int,
|
||||
pub cp_fixed_alloc: c_int,
|
||||
pub cp_fixed_quality: c_int,
|
||||
pub cp_matrice: *mut c_int,
|
||||
pub cp_comment: *mut c_char,
|
||||
pub csty: c_int,
|
||||
pub prog_order: OPJ_PROG_ORDER,
|
||||
pub POC: [opj_poc_t; 32],
|
||||
pub numpocs: OPJ_UINT32,
|
||||
pub tcp_numlayers: c_int,
|
||||
pub tcp_rates: [f32; 100],
|
||||
pub tcp_distoratio: [f32; 100],
|
||||
pub numresolution: c_int,
|
||||
pub cblockw_init: c_int,
|
||||
pub cblockh_init: c_int,
|
||||
pub mode: c_int,
|
||||
pub irreversible: c_int,
|
||||
pub roi_compno: c_int,
|
||||
pub roi_shift: c_int,
|
||||
pub res_spec: c_int,
|
||||
pub prcw_init: [c_int; 33],
|
||||
pub prch_init: [c_int; 33],
|
||||
pub infile: [c_char; 4096],
|
||||
pub outfile: [c_char; 4096],
|
||||
pub index_on: c_int,
|
||||
pub index: [c_char; 4096],
|
||||
pub image_offset_x0: c_int,
|
||||
pub image_offset_y0: c_int,
|
||||
pub subsampling_dx: c_int,
|
||||
pub subsampling_dy: c_int,
|
||||
pub decod_format: c_int,
|
||||
pub cod_format: c_int,
|
||||
pub jpwl_epc_on: OPJ_BOOL,
|
||||
pub jpwl_hprot_MH: c_int,
|
||||
pub jpwl_hprot_TPH_tileno: [c_int; 16],
|
||||
pub jpwl_hprot_TPH: [c_int; 16],
|
||||
pub jpwl_pprot_tileno: [c_int; 16],
|
||||
pub jpwl_pprot_packno: [c_int; 16],
|
||||
pub jpwl_pprot: [c_int; 16],
|
||||
pub jpwl_sens_size: c_int,
|
||||
pub jpwl_sens_addr: c_int,
|
||||
pub jpwl_sens_range: c_int,
|
||||
pub jpwl_sens_MH: c_int,
|
||||
pub jpwl_sens_TPH_tileno: [c_int; 16],
|
||||
pub jpwl_sens_TPH: [c_int; 16],
|
||||
pub cp_cinema: OPJ_CINEMA_MODE,
|
||||
pub max_comp_size: c_int,
|
||||
pub cp_rsiz: OPJ_RSIZ_CAPABILITIES,
|
||||
pub tp_on: c_char,
|
||||
pub tp_flag: c_char,
|
||||
pub tcp_mct: c_char,
|
||||
pub jpip_on: OPJ_BOOL,
|
||||
pub mct_data: *mut c_void,
|
||||
pub max_cs_size: c_int,
|
||||
pub rsiz: OPJ_UINT16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_dparameters_t {
|
||||
pub cp_reduce: OPJ_UINT32,
|
||||
pub cp_layer: OPJ_UINT32,
|
||||
pub infile: [c_char; 4096],
|
||||
pub outfile: [c_char; 4096],
|
||||
pub decod_format: c_int,
|
||||
pub cod_format: c_int,
|
||||
pub DA_x0: OPJ_UINT32,
|
||||
pub DA_x1: OPJ_UINT32,
|
||||
pub DA_y0: OPJ_UINT32,
|
||||
pub DA_y1: OPJ_UINT32,
|
||||
pub m_verbose: OPJ_BOOL,
|
||||
pub tile_index: OPJ_UINT32,
|
||||
pub nb_tile_to_decode: OPJ_UINT32,
|
||||
pub jpwl_correct: OPJ_BOOL,
|
||||
pub jpwl_exp_comps: c_int,
|
||||
pub jpwl_max_tiles: c_int,
|
||||
pub flags: c_uint,
|
||||
}
|
||||
|
||||
pub type opj_codec_t = *mut c_void;
|
||||
pub type opj_stream_t = *mut c_void;
|
||||
|
||||
pub type opj_stream_read_fn =
|
||||
Option<unsafe extern "C" fn(*mut c_void, OPJ_SIZE_T, *mut c_void) -> OPJ_SIZE_T>;
|
||||
pub type opj_stream_write_fn =
|
||||
Option<unsafe extern "C" fn(*mut c_void, OPJ_SIZE_T, *mut c_void) -> OPJ_SIZE_T>;
|
||||
pub type opj_stream_skip_fn = Option<unsafe extern "C" fn(OPJ_OFF_T, *mut c_void) -> OPJ_OFF_T>;
|
||||
pub type opj_stream_seek_fn = Option<unsafe extern "C" fn(OPJ_OFF_T, *mut c_void) -> OPJ_BOOL>;
|
||||
pub type opj_stream_free_user_data_fn = Option<unsafe extern "C" fn(*mut c_void)>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_image_comp_t {
|
||||
pub dx: OPJ_UINT32,
|
||||
pub dy: OPJ_UINT32,
|
||||
pub w: OPJ_UINT32,
|
||||
pub h: OPJ_UINT32,
|
||||
pub x0: OPJ_UINT32,
|
||||
pub y0: OPJ_UINT32,
|
||||
pub prec: OPJ_UINT32,
|
||||
pub bpp: OPJ_UINT32,
|
||||
pub sgnd: OPJ_UINT32,
|
||||
pub resno_decoded: OPJ_UINT32,
|
||||
pub factor: OPJ_UINT32,
|
||||
pub data: *mut OPJ_INT32,
|
||||
pub alpha: OPJ_UINT16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_image_t {
|
||||
pub x0: OPJ_UINT32,
|
||||
pub y0: OPJ_UINT32,
|
||||
pub x1: OPJ_UINT32,
|
||||
pub y1: OPJ_UINT32,
|
||||
pub numcomps: OPJ_UINT32,
|
||||
pub color_space: OPJ_COLOR_SPACE,
|
||||
pub comps: *mut opj_image_comp_t,
|
||||
pub icc_profile_buf: *mut u8,
|
||||
pub icc_profile_len: OPJ_UINT32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct opj_image_cmptparm_t {
|
||||
pub dx: OPJ_UINT32,
|
||||
pub dy: OPJ_UINT32,
|
||||
pub w: OPJ_UINT32,
|
||||
pub h: OPJ_UINT32,
|
||||
pub x0: OPJ_UINT32,
|
||||
pub y0: OPJ_UINT32,
|
||||
pub prec: OPJ_UINT32,
|
||||
pub bpp: OPJ_UINT32,
|
||||
pub sgnd: OPJ_UINT32,
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
pub fn opj_version() -> *const c_char;
|
||||
pub fn opj_stream_default_create(is_input: OPJ_BOOL) -> *mut opj_stream_t;
|
||||
pub fn opj_stream_destroy(stream: *mut opj_stream_t);
|
||||
pub fn opj_stream_set_read_function(stream: *mut opj_stream_t, function: opj_stream_read_fn);
|
||||
pub fn opj_stream_set_write_function(stream: *mut opj_stream_t, function: opj_stream_write_fn);
|
||||
pub fn opj_stream_set_skip_function(stream: *mut opj_stream_t, function: opj_stream_skip_fn);
|
||||
pub fn opj_stream_set_seek_function(stream: *mut opj_stream_t, function: opj_stream_seek_fn);
|
||||
pub fn opj_stream_set_user_data(
|
||||
stream: *mut opj_stream_t,
|
||||
data: *mut c_void,
|
||||
free_function: opj_stream_free_user_data_fn,
|
||||
);
|
||||
pub fn opj_stream_set_user_data_length(stream: *mut opj_stream_t, length: OPJ_UINT64);
|
||||
|
||||
pub fn opj_create_decompress(format: OPJ_CODEC_FORMAT) -> *mut opj_codec_t;
|
||||
pub fn opj_create_compress(format: OPJ_CODEC_FORMAT) -> *mut opj_codec_t;
|
||||
pub fn opj_destroy_codec(codec: *mut opj_codec_t);
|
||||
pub fn opj_set_default_decoder_parameters(parameters: *mut opj_dparameters_t);
|
||||
pub fn opj_setup_decoder(
|
||||
codec: *mut opj_codec_t,
|
||||
parameters: *mut opj_dparameters_t,
|
||||
) -> OPJ_BOOL;
|
||||
pub fn opj_decoder_set_strict_mode(codec: *mut opj_codec_t, strict: OPJ_BOOL) -> OPJ_BOOL;
|
||||
pub fn opj_read_header(
|
||||
stream: *mut opj_stream_t,
|
||||
codec: *mut opj_codec_t,
|
||||
image: *mut *mut opj_image_t,
|
||||
) -> OPJ_BOOL;
|
||||
pub fn opj_decode(
|
||||
codec: *mut opj_codec_t,
|
||||
stream: *mut opj_stream_t,
|
||||
image: *mut opj_image_t,
|
||||
) -> OPJ_BOOL;
|
||||
pub fn opj_end_decompress(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
|
||||
|
||||
pub fn opj_image_create(
|
||||
component_count: OPJ_UINT32,
|
||||
component_parameters: *mut opj_image_cmptparm_t,
|
||||
color_space: OPJ_COLOR_SPACE,
|
||||
) -> *mut opj_image_t;
|
||||
pub fn opj_image_destroy(image: *mut opj_image_t);
|
||||
pub fn opj_set_default_encoder_parameters(parameters: *mut opj_cparameters_t);
|
||||
pub fn opj_setup_encoder(
|
||||
codec: *mut opj_codec_t,
|
||||
parameters: *mut opj_cparameters_t,
|
||||
image: *mut opj_image_t,
|
||||
) -> OPJ_BOOL;
|
||||
pub fn opj_start_compress(
|
||||
codec: *mut opj_codec_t,
|
||||
image: *mut opj_image_t,
|
||||
stream: *mut opj_stream_t,
|
||||
) -> OPJ_BOOL;
|
||||
pub fn opj_encode(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
|
||||
pub fn opj_end_compress(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
|
||||
}
|
||||
770
crates/libremetaverse-openjpeg/src/lib.rs
Normal file
770
crates/libremetaverse-openjpeg/src/lib.rs
Normal file
@@ -0,0 +1,770 @@
|
||||
//! Safe, bounded access to the system `OpenJPEG` codec.
|
||||
//!
|
||||
//! All foreign calls and callback pointer handling are contained here. The
|
||||
//! build requires `OpenJPEG` 2.5.4 or newer, excluding releases affected by
|
||||
//! CVE-2025-54874 and earlier fixed decoder defects.
|
||||
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::{CStr, c_void};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
const MAX_PIXELS: usize = 4096 * 4096;
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
rustdoc::bare_urls,
|
||||
rustdoc::broken_intra_doc_links
|
||||
)]
|
||||
mod ffi;
|
||||
|
||||
/// Codec/container selection.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Format {
|
||||
J2k,
|
||||
Jp2,
|
||||
}
|
||||
|
||||
/// `OpenJPEG` rate-control selection.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Compression {
|
||||
Lossless,
|
||||
Lossy { compression_ratio: f32 },
|
||||
}
|
||||
|
||||
/// Decoder controls passed directly to `OpenJPEG`.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct DecodeOptions {
|
||||
pub discard_levels: u32,
|
||||
pub quality_layers: u32,
|
||||
pub strict: bool,
|
||||
pub max_pixels: usize,
|
||||
}
|
||||
|
||||
/// One decoded component with original integer precision.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Component {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub precision: u8,
|
||||
pub signed: bool,
|
||||
pub alpha: bool,
|
||||
pub samples: Vec<i32>,
|
||||
}
|
||||
|
||||
/// Decoded `OpenJPEG` image.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Image {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub components: Vec<Component>,
|
||||
}
|
||||
|
||||
/// Input image component for encoding.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ComponentRef<'a> {
|
||||
pub precision: u8,
|
||||
pub signed: bool,
|
||||
pub alpha: bool,
|
||||
pub samples: &'a [i32],
|
||||
}
|
||||
|
||||
/// Stable error categories; `OpenJPEG` diagnostics never cross the safe API.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Error {
|
||||
InvalidInput,
|
||||
LimitExceeded,
|
||||
Allocation,
|
||||
Codec,
|
||||
}
|
||||
|
||||
/// Decodes J2K or JP2 after validating header dimensions and components and
|
||||
/// before allowing `OpenJPEG` to allocate decoded sample planes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a stable validation, limit, allocation, or codec error without
|
||||
/// exposing native handles or diagnostics.
|
||||
pub fn decode(bytes: &[u8], format: Format, options: DecodeOptions) -> Result<Image, Error> {
|
||||
ensure_supported_version()?;
|
||||
if bytes.is_empty() || options.max_pixels == 0 {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
validate_encoded_header(bytes, format, options.max_pixels)?;
|
||||
let stream = InputStream::new(bytes)?;
|
||||
let codec = Codec::decoder(format)?;
|
||||
let mut parameters = MaybeUninit::<ffi::opj_dparameters_t>::zeroed();
|
||||
unsafe { ffi::opj_set_default_decoder_parameters(parameters.as_mut_ptr()) };
|
||||
let mut parameters = unsafe { parameters.assume_init() };
|
||||
parameters.cp_reduce = options.discard_levels;
|
||||
parameters.cp_layer = options.quality_layers;
|
||||
if unsafe { ffi::opj_setup_decoder(codec.raw.as_ptr(), &raw mut parameters) } != 1
|
||||
|| unsafe {
|
||||
ffi::opj_decoder_set_strict_mode(codec.raw.as_ptr(), i32::from(options.strict))
|
||||
} != 1
|
||||
{
|
||||
return Err(Error::Codec);
|
||||
}
|
||||
|
||||
// Initializing this pointer is security-significant: OpenJPEG <=2.5.3 had
|
||||
// an out-of-bounds write on short JP2 input when it was uninitialized.
|
||||
let mut raw_image = std::ptr::null_mut();
|
||||
if unsafe { ffi::opj_read_header(stream.raw.as_ptr(), codec.raw.as_ptr(), &raw mut raw_image) }
|
||||
!= 1
|
||||
{
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let mut image = RawImage::new(raw_image)?;
|
||||
validate_header(image.raw.as_ptr(), options.max_pixels)?;
|
||||
if unsafe { ffi::opj_decode(codec.raw.as_ptr(), stream.raw.as_ptr(), image.raw.as_ptr()) } != 1
|
||||
|| unsafe { ffi::opj_end_decompress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
|
||||
{
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
image.copy(options.max_pixels)
|
||||
}
|
||||
|
||||
/// Encodes one through five equal-sized component planes to memory.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a stable validation, limit, allocation, or codec error. Dimensions,
|
||||
/// sample ranges, and output capacity are checked before native encoding.
|
||||
pub fn encode(
|
||||
width: u32,
|
||||
height: u32,
|
||||
components: &[ComponentRef<'_>],
|
||||
format: Format,
|
||||
compression: Compression,
|
||||
max_encoded_bytes: usize,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
ensure_supported_version()?;
|
||||
let pixels = checked_pixels(width, height, MAX_PIXELS)?;
|
||||
if components.is_empty()
|
||||
|| components.len() > 5
|
||||
|| max_encoded_bytes == 0
|
||||
|| components.iter().any(|component| {
|
||||
!(1..=31).contains(&component.precision) || component.samples.len() != pixels
|
||||
})
|
||||
{
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
if let Compression::Lossy { compression_ratio } = compression {
|
||||
if !compression_ratio.is_finite() || compression_ratio < 1.0 {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
}
|
||||
|
||||
let image = RawImage::from_components(width, height, components)?;
|
||||
let codec = Codec::encoder(format)?;
|
||||
let stream = OutputStream::new(max_encoded_bytes)?;
|
||||
let mut parameters = MaybeUninit::<ffi::opj_cparameters_t>::zeroed();
|
||||
unsafe { ffi::opj_set_default_encoder_parameters(parameters.as_mut_ptr()) };
|
||||
let mut parameters = unsafe { parameters.assume_init() };
|
||||
parameters.tcp_numlayers = 1;
|
||||
parameters.cp_disto_alloc = 1;
|
||||
parameters.tcp_mct = (components.len() >= 3).into();
|
||||
parameters.numresolution = maximum_resolution_levels(width, height);
|
||||
parameters.max_cs_size = i32::try_from(max_encoded_bytes).unwrap_or(i32::MAX);
|
||||
match compression {
|
||||
Compression::Lossless => {
|
||||
parameters.tcp_rates[0] = 0.0;
|
||||
parameters.irreversible = 0;
|
||||
}
|
||||
Compression::Lossy { compression_ratio } => {
|
||||
parameters.tcp_rates[0] = compression_ratio;
|
||||
parameters.irreversible = 1;
|
||||
}
|
||||
}
|
||||
if unsafe {
|
||||
ffi::opj_setup_encoder(codec.raw.as_ptr(), &raw mut parameters, image.raw.as_ptr())
|
||||
} != 1
|
||||
|| unsafe {
|
||||
ffi::opj_start_compress(codec.raw.as_ptr(), image.raw.as_ptr(), stream.raw.as_ptr())
|
||||
} != 1
|
||||
|| unsafe { ffi::opj_encode(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
|
||||
|| unsafe { ffi::opj_end_compress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1
|
||||
{
|
||||
return Err(if stream.state.limit_exceeded {
|
||||
Error::LimitExceeded
|
||||
} else {
|
||||
Error::Codec
|
||||
});
|
||||
}
|
||||
stream.into_bytes()
|
||||
}
|
||||
|
||||
struct Codec {
|
||||
raw: NonNull<ffi::opj_codec_t>,
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
fn decoder(format: Format) -> Result<Self, Error> {
|
||||
let raw = unsafe { ffi::opj_create_decompress(codec_format(format)) };
|
||||
NonNull::new(raw)
|
||||
.map(|raw| Self { raw })
|
||||
.ok_or(Error::Codec)
|
||||
}
|
||||
|
||||
fn encoder(format: Format) -> Result<Self, Error> {
|
||||
let raw = unsafe { ffi::opj_create_compress(codec_format(format)) };
|
||||
NonNull::new(raw)
|
||||
.map(|raw| Self { raw })
|
||||
.ok_or(Error::Codec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Codec {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::opj_destroy_codec(self.raw.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
struct RawImage {
|
||||
raw: NonNull<ffi::opj_image_t>,
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
fn new(raw: *mut ffi::opj_image_t) -> Result<Self, Error> {
|
||||
NonNull::new(raw)
|
||||
.map(|raw| Self { raw })
|
||||
.ok_or(Error::Codec)
|
||||
}
|
||||
|
||||
fn from_components(
|
||||
width: u32,
|
||||
height: u32,
|
||||
components: &[ComponentRef<'_>],
|
||||
) -> Result<Self, Error> {
|
||||
let mut parameters: Vec<_> = components
|
||||
.iter()
|
||||
.map(|component| ffi::opj_image_cmptparm_t {
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
w: width,
|
||||
h: height,
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
prec: u32::from(component.precision),
|
||||
bpp: u32::from(component.precision),
|
||||
sgnd: u32::from(component.signed),
|
||||
})
|
||||
.collect();
|
||||
let color_space = if components.len() >= 3 {
|
||||
ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_SRGB
|
||||
} else {
|
||||
ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_GRAY
|
||||
};
|
||||
let raw = unsafe {
|
||||
ffi::opj_image_create(
|
||||
u32::try_from(parameters.len()).map_err(|_| Error::InvalidInput)?,
|
||||
parameters.as_mut_ptr(),
|
||||
color_space,
|
||||
)
|
||||
};
|
||||
let mut image = Self::new(raw)?;
|
||||
unsafe {
|
||||
let raw = image.raw.as_mut();
|
||||
raw.x0 = 0;
|
||||
raw.y0 = 0;
|
||||
raw.x1 = width;
|
||||
raw.y1 = height;
|
||||
let output = std::slice::from_raw_parts_mut(raw.comps, raw.numcomps as usize);
|
||||
for (output, input) in output.iter_mut().zip(components) {
|
||||
if output.data.is_null() {
|
||||
return Err(Error::Allocation);
|
||||
}
|
||||
output.alpha = u16::from(input.alpha);
|
||||
let destination = std::slice::from_raw_parts_mut(output.data, input.samples.len());
|
||||
destination.copy_from_slice(input.samples);
|
||||
}
|
||||
}
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn copy(&mut self, max_pixels: usize) -> Result<Image, Error> {
|
||||
let raw = unsafe { self.raw.as_ref() };
|
||||
let width = raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?;
|
||||
let height = raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?;
|
||||
checked_pixels(width, height, max_pixels)?;
|
||||
let components = unsafe { std::slice::from_raw_parts(raw.comps, raw.numcomps as usize) };
|
||||
if !(1..=5).contains(&components.len()) {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let first = components.first().ok_or(Error::InvalidInput)?;
|
||||
let mut copied = Vec::new();
|
||||
copied
|
||||
.try_reserve_exact(components.len())
|
||||
.map_err(|_| Error::Allocation)?;
|
||||
for component in components {
|
||||
if component.w != first.w
|
||||
|| component.h != first.h
|
||||
|| component.data.is_null()
|
||||
|| !(1..=31).contains(&component.prec)
|
||||
{
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
checked_pixels(component.w, component.h, max_pixels)?;
|
||||
let length = checked_pixels(component.w, component.h, max_pixels)?;
|
||||
let source = unsafe { std::slice::from_raw_parts(component.data, length) };
|
||||
let mut samples = Vec::new();
|
||||
samples
|
||||
.try_reserve_exact(length)
|
||||
.map_err(|_| Error::Allocation)?;
|
||||
samples.extend_from_slice(source);
|
||||
copied.push(Component {
|
||||
width: component.w,
|
||||
height: component.h,
|
||||
precision: u8::try_from(component.prec).map_err(|_| Error::InvalidInput)?,
|
||||
signed: component.sgnd != 0,
|
||||
alpha: component.alpha != 0,
|
||||
samples,
|
||||
});
|
||||
}
|
||||
Ok(Image {
|
||||
width: first.w,
|
||||
height: first.h,
|
||||
components: copied,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawImage {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::opj_image_destroy(self.raw.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
struct InputState<'a> {
|
||||
bytes: &'a [u8],
|
||||
position: usize,
|
||||
}
|
||||
|
||||
struct InputStream<'a> {
|
||||
raw: NonNull<ffi::opj_stream_t>,
|
||||
state: Box<InputState<'a>>,
|
||||
_lifetime: PhantomData<&'a [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> InputStream<'a> {
|
||||
fn new(bytes: &'a [u8]) -> Result<Self, Error> {
|
||||
let raw = unsafe { ffi::opj_stream_default_create(1) };
|
||||
let raw = NonNull::new(raw).ok_or(Error::Allocation)?;
|
||||
let mut state = Box::new(InputState { bytes, position: 0 });
|
||||
unsafe {
|
||||
ffi::opj_stream_set_read_function(raw.as_ptr(), Some(input_read));
|
||||
ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(input_skip));
|
||||
ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(input_seek));
|
||||
ffi::opj_stream_set_user_data(
|
||||
raw.as_ptr(),
|
||||
std::ptr::from_mut(state.as_mut()).cast(),
|
||||
None,
|
||||
);
|
||||
ffi::opj_stream_set_user_data_length(raw.as_ptr(), bytes.len() as u64);
|
||||
}
|
||||
Ok(Self {
|
||||
raw,
|
||||
state,
|
||||
_lifetime: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputStream<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
|
||||
// Keep the callback state observably alive until after stream destroy.
|
||||
let _ = &self.state;
|
||||
}
|
||||
}
|
||||
|
||||
struct OutputState {
|
||||
bytes: Vec<u8>,
|
||||
position: usize,
|
||||
limit: usize,
|
||||
limit_exceeded: bool,
|
||||
}
|
||||
|
||||
struct OutputStream {
|
||||
raw: NonNull<ffi::opj_stream_t>,
|
||||
state: Box<OutputState>,
|
||||
}
|
||||
|
||||
impl OutputStream {
|
||||
fn new(limit: usize) -> Result<Self, Error> {
|
||||
let raw = unsafe { ffi::opj_stream_default_create(0) };
|
||||
let raw = NonNull::new(raw).ok_or(Error::Allocation)?;
|
||||
let mut state = Box::new(OutputState {
|
||||
bytes: Vec::new(),
|
||||
position: 0,
|
||||
limit,
|
||||
limit_exceeded: false,
|
||||
});
|
||||
unsafe {
|
||||
ffi::opj_stream_set_write_function(raw.as_ptr(), Some(output_write));
|
||||
ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(output_skip));
|
||||
ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(output_seek));
|
||||
ffi::opj_stream_set_user_data(
|
||||
raw.as_ptr(),
|
||||
std::ptr::from_mut(state.as_mut()).cast(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
Ok(Self { raw, state })
|
||||
}
|
||||
|
||||
fn into_bytes(mut self) -> Result<Vec<u8>, Error> {
|
||||
unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
|
||||
self.raw = NonNull::dangling();
|
||||
if self.state.limit_exceeded || self.state.bytes.is_empty() {
|
||||
return Err(if self.state.limit_exceeded {
|
||||
Error::LimitExceeded
|
||||
} else {
|
||||
Error::Codec
|
||||
});
|
||||
}
|
||||
Ok(std::mem::take(&mut self.state.bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OutputStream {
|
||||
fn drop(&mut self) {
|
||||
if self.raw != NonNull::dangling() {
|
||||
unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn input_read(
|
||||
output: *mut c_void,
|
||||
length: usize,
|
||||
user_data: *mut c_void,
|
||||
) -> usize {
|
||||
if output.is_null() || user_data.is_null() || length == 0 {
|
||||
return usize::MAX;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
|
||||
let remaining = state.bytes.len().saturating_sub(state.position);
|
||||
if remaining == 0 {
|
||||
return usize::MAX;
|
||||
}
|
||||
let count = remaining.min(length);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
state.bytes.as_ptr().add(state.position),
|
||||
output.cast(),
|
||||
count,
|
||||
);
|
||||
}
|
||||
state.position += count;
|
||||
count
|
||||
}
|
||||
|
||||
unsafe extern "C" fn input_skip(offset: i64, user_data: *mut c_void) -> i64 {
|
||||
if user_data.is_null() || offset < 0 {
|
||||
return -1;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
|
||||
let Ok(offset) = usize::try_from(offset) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(position) = state.position.checked_add(offset) else {
|
||||
return -1;
|
||||
};
|
||||
if position > state.bytes.len() {
|
||||
return -1;
|
||||
}
|
||||
state.position = position;
|
||||
i64::try_from(offset).unwrap_or(-1)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn input_seek(position: i64, user_data: *mut c_void) -> i32 {
|
||||
if user_data.is_null() || position < 0 {
|
||||
return 0;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<InputState<'_>>() };
|
||||
let Ok(position) = usize::try_from(position) else {
|
||||
return 0;
|
||||
};
|
||||
if position > state.bytes.len() {
|
||||
return 0;
|
||||
}
|
||||
state.position = position;
|
||||
1
|
||||
}
|
||||
|
||||
unsafe extern "C" fn output_write(
|
||||
input: *mut c_void,
|
||||
length: usize,
|
||||
user_data: *mut c_void,
|
||||
) -> usize {
|
||||
if input.is_null() || user_data.is_null() {
|
||||
return usize::MAX;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<OutputState>() };
|
||||
let Some(end) = state.position.checked_add(length) else {
|
||||
state.limit_exceeded = true;
|
||||
return usize::MAX;
|
||||
};
|
||||
if end > state.limit {
|
||||
state.limit_exceeded = true;
|
||||
return usize::MAX;
|
||||
}
|
||||
if end > state.bytes.len() {
|
||||
if state.bytes.try_reserve(end - state.bytes.len()).is_err() {
|
||||
return usize::MAX;
|
||||
}
|
||||
state.bytes.resize(end, 0);
|
||||
}
|
||||
let source = unsafe { std::slice::from_raw_parts(input.cast::<u8>(), length) };
|
||||
state.bytes[state.position..end].copy_from_slice(source);
|
||||
state.position = end;
|
||||
length
|
||||
}
|
||||
|
||||
unsafe extern "C" fn output_skip(offset: i64, user_data: *mut c_void) -> i64 {
|
||||
if user_data.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<OutputState>() };
|
||||
let Ok(offset_isize) = isize::try_from(offset) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(position) = state.position.checked_add_signed(offset_isize) else {
|
||||
return -1;
|
||||
};
|
||||
if position > state.limit {
|
||||
state.limit_exceeded = true;
|
||||
return -1;
|
||||
}
|
||||
state.position = position;
|
||||
offset
|
||||
}
|
||||
|
||||
unsafe extern "C" fn output_seek(position: i64, user_data: *mut c_void) -> i32 {
|
||||
if user_data.is_null() || position < 0 {
|
||||
return 0;
|
||||
}
|
||||
let state = unsafe { &mut *user_data.cast::<OutputState>() };
|
||||
let Ok(position) = usize::try_from(position) else {
|
||||
return 0;
|
||||
};
|
||||
if position > state.limit {
|
||||
state.limit_exceeded = true;
|
||||
return 0;
|
||||
}
|
||||
state.position = position;
|
||||
1
|
||||
}
|
||||
|
||||
fn validate_header(raw: *mut ffi::opj_image_t, max_pixels: usize) -> Result<(), Error> {
|
||||
let raw = unsafe { raw.as_ref() }.ok_or(Error::InvalidInput)?;
|
||||
checked_pixels(
|
||||
raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?,
|
||||
raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?,
|
||||
max_pixels,
|
||||
)?;
|
||||
let count = usize::try_from(raw.numcomps).map_err(|_| Error::InvalidInput)?;
|
||||
if !(1..=5).contains(&count) || raw.comps.is_null() {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let components = unsafe { std::slice::from_raw_parts(raw.comps, count) };
|
||||
for component in components {
|
||||
checked_pixels(component.w, component.h, max_pixels)?;
|
||||
if !(1..=31).contains(&component.prec) {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checked_pixels(width: u32, height: u32, maximum: usize) -> Result<usize, Error> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
usize::try_from(width)
|
||||
.ok()
|
||||
.and_then(|width| {
|
||||
usize::try_from(height)
|
||||
.ok()
|
||||
.and_then(|height| width.checked_mul(height))
|
||||
})
|
||||
.filter(|pixels| *pixels <= maximum)
|
||||
.ok_or(Error::LimitExceeded)
|
||||
}
|
||||
|
||||
fn validate_encoded_header(bytes: &[u8], format: Format, max_pixels: usize) -> Result<(), Error> {
|
||||
match format {
|
||||
Format::J2k => validate_siz_marker(bytes, max_pixels),
|
||||
Format::Jp2 => {
|
||||
let mut position = 0_usize;
|
||||
while bytes.len().saturating_sub(position) >= 8 {
|
||||
let box_length = read_u32(bytes, position)?;
|
||||
let box_type = bytes
|
||||
.get(position + 4..position + 8)
|
||||
.ok_or(Error::InvalidInput)?;
|
||||
let (header_length, length) = if box_length == 1 {
|
||||
let extended = usize::try_from(read_u64(bytes, position + 8)?)
|
||||
.map_err(|_| Error::LimitExceeded)?;
|
||||
(16_usize, extended)
|
||||
} else if box_length == 0 {
|
||||
(8_usize, bytes.len() - position)
|
||||
} else {
|
||||
(
|
||||
8_usize,
|
||||
usize::try_from(box_length).map_err(|_| Error::LimitExceeded)?,
|
||||
)
|
||||
};
|
||||
if length < header_length {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let body = position
|
||||
.checked_add(header_length)
|
||||
.ok_or(Error::LimitExceeded)?;
|
||||
let end = position.checked_add(length).ok_or(Error::LimitExceeded)?;
|
||||
if end > bytes.len() {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
if box_type == b"jp2c" {
|
||||
return validate_siz_marker(&bytes[body..end], max_pixels);
|
||||
}
|
||||
position = end;
|
||||
}
|
||||
Err(Error::InvalidInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_siz_marker(codestream: &[u8], max_pixels: usize) -> Result<(), Error> {
|
||||
if codestream.get(..4) != Some(&[0xff, 0x4f, 0xff, 0x51]) {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let length = usize::from(read_u16(codestream, 4)?);
|
||||
let end = 4_usize.checked_add(length).ok_or(Error::LimitExceeded)?;
|
||||
if length < 38 || end > codestream.len() {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let x1 = read_u32(codestream, 8)?;
|
||||
let y1 = read_u32(codestream, 12)?;
|
||||
let x0 = read_u32(codestream, 16)?;
|
||||
let y0 = read_u32(codestream, 20)?;
|
||||
if read_u32(codestream, 24)? == 0 || read_u32(codestream, 28)? == 0 {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let width = x1.checked_sub(x0).ok_or(Error::InvalidInput)?;
|
||||
let height = y1.checked_sub(y0).ok_or(Error::InvalidInput)?;
|
||||
checked_pixels(width, height, max_pixels)?;
|
||||
|
||||
let component_count = usize::from(read_u16(codestream, 40)?);
|
||||
if !(1..=5).contains(&component_count)
|
||||
|| length != 38 + component_count.checked_mul(3).ok_or(Error::LimitExceeded)?
|
||||
{
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
for component in 0..component_count {
|
||||
let offset = 42 + component * 3;
|
||||
let precision = codestream.get(offset).copied().ok_or(Error::InvalidInput)? & 0x7f;
|
||||
let dx = u32::from(
|
||||
codestream
|
||||
.get(offset + 1)
|
||||
.copied()
|
||||
.ok_or(Error::InvalidInput)?,
|
||||
);
|
||||
let dy = u32::from(
|
||||
codestream
|
||||
.get(offset + 2)
|
||||
.copied()
|
||||
.ok_or(Error::InvalidInput)?,
|
||||
);
|
||||
if precision >= 31 || dx == 0 || dy == 0 {
|
||||
return Err(Error::InvalidInput);
|
||||
}
|
||||
let component_width = ceiling_div(x1, dx)?
|
||||
.checked_sub(ceiling_div(x0, dx)?)
|
||||
.ok_or(Error::InvalidInput)?;
|
||||
let component_height = ceiling_div(y1, dy)?
|
||||
.checked_sub(ceiling_div(y0, dy)?)
|
||||
.ok_or(Error::InvalidInput)?;
|
||||
checked_pixels(component_width, component_height, max_pixels)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ceiling_div(value: u32, divisor: u32) -> Result<u32, Error> {
|
||||
value
|
||||
.checked_add(divisor - 1)
|
||||
.map(|value| value / divisor)
|
||||
.ok_or(Error::LimitExceeded)
|
||||
}
|
||||
|
||||
fn read_u16(bytes: &[u8], position: usize) -> Result<u16, Error> {
|
||||
let bytes: [u8; 2] = bytes
|
||||
.get(position..position.checked_add(2).ok_or(Error::LimitExceeded)?)
|
||||
.ok_or(Error::InvalidInput)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidInput)?;
|
||||
Ok(u16::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_u32(bytes: &[u8], position: usize) -> Result<u32, Error> {
|
||||
let bytes: [u8; 4] = bytes
|
||||
.get(position..position.checked_add(4).ok_or(Error::LimitExceeded)?)
|
||||
.ok_or(Error::InvalidInput)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidInput)?;
|
||||
Ok(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_u64(bytes: &[u8], position: usize) -> Result<u64, Error> {
|
||||
let bytes: [u8; 8] = bytes
|
||||
.get(position..position.checked_add(8).ok_or(Error::LimitExceeded)?)
|
||||
.ok_or(Error::InvalidInput)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidInput)?;
|
||||
Ok(u64::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
fn maximum_resolution_levels(width: u32, height: u32) -> i32 {
|
||||
let minimum = width.min(height);
|
||||
let levels = u32::BITS - minimum.leading_zeros();
|
||||
i32::try_from(levels.min(6)).unwrap_or(1).max(1)
|
||||
}
|
||||
|
||||
const fn codec_format(format: Format) -> ffi::OPJ_CODEC_FORMAT {
|
||||
match format {
|
||||
Format::J2k => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_J2K,
|
||||
Format::Jp2 => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_JP2,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_supported_version() -> Result<(), Error> {
|
||||
let raw = unsafe { ffi::opj_version() };
|
||||
if raw.is_null() {
|
||||
return Err(Error::Codec);
|
||||
}
|
||||
let version = unsafe { CStr::from_ptr(raw) }
|
||||
.to_str()
|
||||
.map_err(|_| Error::Codec)?;
|
||||
let mut components = version.split('.').map(|component| {
|
||||
component
|
||||
.bytes()
|
||||
.take_while(u8::is_ascii_digit)
|
||||
.try_fold(0_u32, |value, digit| {
|
||||
value.checked_mul(10)?.checked_add(u32::from(digit - b'0'))
|
||||
})
|
||||
});
|
||||
let parsed = (
|
||||
components.next().flatten().ok_or(Error::Codec)?,
|
||||
components.next().flatten().ok_or(Error::Codec)?,
|
||||
components.next().flatten().ok_or(Error::Codec)?,
|
||||
);
|
||||
if parsed < (2, 5, 4) {
|
||||
return Err(Error::Codec);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1694,7 +1694,7 @@ fn object_to_osd(value: Object) -> Result<OSD, Error> {
|
||||
Object::Vector4(value) => OSD::from_vector4(value)?,
|
||||
Object::Quaternion(value) => OSD::from_quaternion(value)?,
|
||||
Object::Color4(value) => OSD::from_color4(value)?,
|
||||
Object::Matrix4(_) => OSD::Undefined,
|
||||
Object::Matrix4(_) | Object::Opaque(_) => OSD::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
//! surfaces. Their behavior is implemented only when the owning API slice is
|
||||
//! ported.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::marker::PhantomData;
|
||||
@@ -20,6 +22,54 @@ pub trait ReadWrite: std::io::Read + std::io::Write + std::io::Seek {}
|
||||
|
||||
impl<T: std::io::Read + std::io::Write + std::io::Seek> ReadWrite for T {}
|
||||
|
||||
trait OpaqueValue: Any + fmt::Debug + Send + Sync {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
|
||||
}
|
||||
|
||||
impl<T: Any + fmt::Debug + Send + Sync> OpaqueValue for T {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased reference value used when a mapped `System.Object` carries a
|
||||
/// project type rather than one of the protocol scalar variants.
|
||||
///
|
||||
/// Equality and hashing use reference identity, matching the default behavior
|
||||
/// of arbitrary CLR reference objects. The contained value can be recovered
|
||||
/// with [`Object::downcast_ref`].
|
||||
#[derive(Clone)]
|
||||
pub struct OpaqueObject(Arc<dyn OpaqueValue>);
|
||||
|
||||
impl fmt::Debug for OpaqueObject {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_tuple("OpaqueObject")
|
||||
.field(&self.0)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for OpaqueObject {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for OpaqueObject {}
|
||||
|
||||
impl Hash for OpaqueObject {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
let pointer = Arc::as_ptr(&self.0).cast::<()>();
|
||||
pointer.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Object {
|
||||
Undefined,
|
||||
@@ -43,6 +93,34 @@ pub enum Object {
|
||||
Vector3(crate::Vector3),
|
||||
Vector3d(crate::Vector3d),
|
||||
Vector4(crate::Vector4),
|
||||
/// A project-owned value passed through a mapped `System.Object` boundary.
|
||||
Opaque(OpaqueObject),
|
||||
}
|
||||
|
||||
impl Object {
|
||||
/// Boxes a project-owned value for a mapped `System.Object` parameter.
|
||||
#[must_use]
|
||||
pub fn opaque<T: Any + fmt::Debug + Send + Sync>(value: T) -> Self {
|
||||
Self::Opaque(OpaqueObject(Arc::new(value)))
|
||||
}
|
||||
|
||||
/// Borrows an opaque value when its concrete type is `T`.
|
||||
#[must_use]
|
||||
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
|
||||
match self {
|
||||
Self::Opaque(value) => value.0.as_ref().as_any().downcast_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clones the opaque reference and recovers its concrete shared value.
|
||||
#[must_use]
|
||||
pub fn downcast_arc<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
|
||||
match self {
|
||||
Self::Opaque(value) => value.0.clone().into_any().downcast().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Object {
|
||||
@@ -69,6 +147,7 @@ impl PartialEq for Object {
|
||||
(Self::Vector3(lhs), Self::Vector3(rhs)) => crate::Vector3::eq(*lhs, *rhs),
|
||||
(Self::Vector3d(lhs), Self::Vector3d(rhs)) => crate::Vector3d::eq(*lhs, *rhs),
|
||||
(Self::Vector4(lhs), Self::Vector4(rhs)) => crate::Vector4::eq(*lhs, *rhs),
|
||||
(Self::Opaque(lhs), Self::Opaque(rhs)) => lhs == rhs,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -105,6 +184,7 @@ impl std::hash::Hash for Object {
|
||||
Self::Vector3(value) => value.get_hash_code().hash(state),
|
||||
Self::Vector3d(value) => value.get_hash_code().hash(state),
|
||||
Self::Vector4(value) => value.get_hash_code().hash(state),
|
||||
Self::Opaque(value) => value.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,9 +459,10 @@ pub struct SocketException;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Uri};
|
||||
use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Object, Uri};
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
#[test]
|
||||
@@ -427,4 +508,22 @@ mod tests {
|
||||
.is_success_status_code()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_objects_downcast_and_preserve_reference_identity() {
|
||||
let object = Object::opaque(String::from("managed image boundary"));
|
||||
assert_eq!(
|
||||
object.downcast_ref::<String>().map(String::as_str),
|
||||
Some("managed image boundary")
|
||||
);
|
||||
assert!(object.downcast_ref::<Vec<u8>>().is_none());
|
||||
let first = object.downcast_arc::<String>().expect("shared string");
|
||||
let second = object.downcast_arc::<String>().expect("shared string");
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
assert_eq!(object, object.clone());
|
||||
assert_ne!(
|
||||
object,
|
||||
Object::opaque(String::from("managed image boundary"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ description = "Rust rewrite shell for the LibreMetaverse client library"
|
||||
[features]
|
||||
default = ["dds-bc67"]
|
||||
dds-bc67 = ["dep:bcdec_rs"]
|
||||
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
|
||||
|
||||
[dependencies]
|
||||
bcdec_rs = { version = "0.2.0", optional = true }
|
||||
|
||||
@@ -34036,49 +34036,14 @@ pub mod imaging {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.ManagedImageCreator`.
|
||||
pub struct ManagedImageCreator;
|
||||
impl ManagedImageCreator {
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])`.
|
||||
pub fn create(
|
||||
&self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
num_components: i32,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<Box<dyn libremetaverse_imaging::codec::IImage>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)`.
|
||||
pub fn to_portable_image_source(
|
||||
&self,
|
||||
image_object: libremetaverse_types::compat::Object,
|
||||
) -> Result<Box<dyn libremetaverse_imaging::codec::BlkImgDataSrc>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)",
|
||||
)
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.Create(System.Int32,System.Int32,System.Int32,System.Byte[])`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)`.
|
||||
pub use crate::j2k::ManagedImageCreator;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions`.
|
||||
pub struct ManagedImageInterleavedExtensions;
|
||||
impl ManagedImageInterleavedExtensions {
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`.
|
||||
pub fn to_managed_image(
|
||||
image: libremetaverse_imaging::codec::InterleavedImage,
|
||||
) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)",
|
||||
)
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`.
|
||||
pub use crate::j2k::ManagedImageInterleavedExtensions;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Imaging.Targa`.
|
||||
/// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`.
|
||||
|
||||
435
crates/libremetaverse/src/j2k.rs
Normal file
435
crates/libremetaverse/src/j2k.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
//! LibreMetaverse-specific adapters around the format-neutral imaging boundary.
|
||||
|
||||
use libremetaverse_imaging::codec::{BlkImgDataSrc, IImage, IImageCreator, InterleavedImage};
|
||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||
use libremetaverse_types::Error;
|
||||
use libremetaverse_types::compat::Object;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
const DC_OFFSET: i32 = 128;
|
||||
|
||||
/// CoreJ2K-compatible creator that writes directly into managed image planes.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ManagedImageCreator;
|
||||
|
||||
impl ManagedImageCreator {
|
||||
/// Creates a creator.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This fixed compatibility signature cannot fail.
|
||||
pub const fn new() -> Result<Self, Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
/// Builds a managed image from already byte-scaled interleaved samples.
|
||||
///
|
||||
/// Component order follows the reference exactly: gray; gray/alpha; RGB;
|
||||
/// RGBA; or RGB/bump/alpha.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::Argument`] for invalid dimensions, component counts,
|
||||
/// or byte lengths and [`Error::InvalidOperation`] on bounded allocation
|
||||
/// failure.
|
||||
#[allow(clippy::needless_pass_by_value)] // Fixed mapped `byte[]` ownership signature.
|
||||
pub fn create(
|
||||
&self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
num_components: i32,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<Box<dyn IImage>, Error> {
|
||||
let components = usize::try_from(num_components).map_err(|_| Error::Argument)?;
|
||||
let channels = channels_for_components(components)?;
|
||||
let image = managed_from_interleaved(width, height, channels, components, &bytes)?;
|
||||
Ok(Box::new(ManagedImageJ2kImage(image)))
|
||||
}
|
||||
|
||||
/// Adapts a boxed managed image passed through the mapped `System.Object`
|
||||
/// boundary into the four-component encode source used by `CoreJ2K`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::ArgumentNull`] for `Object::Undefined`,
|
||||
/// [`Error::Argument`] for another object type, or a validation/allocation
|
||||
/// failure for an inconsistent image.
|
||||
#[allow(clippy::needless_pass_by_value)] // Fixed mapped `System.Object` ownership signature.
|
||||
pub fn to_portable_image_source(
|
||||
&self,
|
||||
image_object: Object,
|
||||
) -> Result<Box<dyn BlkImgDataSrc>, Error> {
|
||||
if image_object == Object::Undefined {
|
||||
return Err(Error::ArgumentNull);
|
||||
}
|
||||
let image = image_object
|
||||
.downcast_arc::<ManagedImage>()
|
||||
.ok_or(Error::Argument)?;
|
||||
image.validate()?;
|
||||
Ok(Box::new(ManagedImageBlockSource { image }))
|
||||
}
|
||||
|
||||
/// Rust-native strongly typed form of [`Self::to_portable_image_source`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::InvalidOperation`] for an inconsistent public image
|
||||
/// layout.
|
||||
pub fn to_portable_image_source_from_managed(
|
||||
&self,
|
||||
image: ManagedImage,
|
||||
) -> Result<Box<dyn BlkImgDataSrc>, Error> {
|
||||
image.validate()?;
|
||||
Ok(Box::new(ManagedImageBlockSource {
|
||||
image: Arc::new(image),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl IImageCreator for ManagedImageCreator {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ManagedImageJ2kImage(ManagedImage);
|
||||
|
||||
impl IImage for ManagedImageJ2kImage {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_managed_image(&self) -> Option<&ManagedImage> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversion helpers for the external `CoreJ2K` interleaved-image boundary.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ManagedImageInterleavedExtensions;
|
||||
|
||||
impl ManagedImageInterleavedExtensions {
|
||||
/// Converts original-precision component planes to managed 8-bit planes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a typed component/layout/allocation error. Component order is
|
||||
/// gray; gray/alpha; RGB; RGBA; or RGB/bump/alpha.
|
||||
#[allow(clippy::needless_pass_by_value)] // Fixed mapped external-type ownership signature.
|
||||
pub fn to_managed_image(image: InterleavedImage) -> Result<ManagedImage, Error> {
|
||||
let components = image.number_of_components();
|
||||
let channels = channels_for_components(components)?;
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
let pixels = checked_pixels(width, height)?;
|
||||
let length = pixels.checked_mul(components).ok_or(Error::Argument)?;
|
||||
let mut bytes = Vec::new();
|
||||
bytes
|
||||
.try_reserve_exact(length)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
bytes.resize(length, 0);
|
||||
let mut plane = vec![0; pixels];
|
||||
for component in 0..components {
|
||||
image.to_component_bytes(component, &mut plane)?;
|
||||
for (pixel, sample) in plane.iter().enumerate() {
|
||||
bytes[pixel * components + component] = *sample;
|
||||
}
|
||||
}
|
||||
managed_from_interleaved(width, height, channels, components, &bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ManagedImageBlockSource {
|
||||
image: Arc<ManagedImage>,
|
||||
}
|
||||
|
||||
impl BlkImgDataSrc for ManagedImageBlockSource {
|
||||
fn width(&self) -> i32 {
|
||||
self.image.width
|
||||
}
|
||||
|
||||
fn height(&self) -> i32 {
|
||||
self.image.height
|
||||
}
|
||||
|
||||
fn number_of_components(&self) -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn nominal_range_bits(&self, component_index: usize) -> Result<u8, Error> {
|
||||
validate_component_index(component_index)?;
|
||||
Ok(8)
|
||||
}
|
||||
|
||||
fn fixed_point(&self, component_index: usize) -> Result<u8, Error> {
|
||||
validate_component_index(component_index)?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn is_original_signed(&self, component_index: usize) -> Result<bool, Error> {
|
||||
validate_component_index(component_index)?;
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn component_block(
|
||||
&self,
|
||||
component_index: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<Vec<i32>, Error> {
|
||||
validate_component_index(component_index)?;
|
||||
self.image.validate()?;
|
||||
let x = usize::try_from(x).map_err(|_| Error::Argument)?;
|
||||
let y = usize::try_from(y).map_err(|_| Error::Argument)?;
|
||||
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
|
||||
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
|
||||
let image_width = usize::try_from(self.image.width).map_err(|_| Error::Argument)?;
|
||||
let image_height = usize::try_from(self.image.height).map_err(|_| Error::Argument)?;
|
||||
let Some(x_end) = x.checked_add(width) else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let Some(y_end) = y.checked_add(height) else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
if width == 0 || height == 0 || x_end > image_width || y_end > image_height {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let sample_count = width.checked_mul(height).ok_or(Error::Argument)?;
|
||||
let mut samples = Vec::new();
|
||||
samples
|
||||
.try_reserve_exact(sample_count)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
for row in y..y + height {
|
||||
for column in x..x + width {
|
||||
let pixel = row * image_width + column;
|
||||
samples.push(self.sample(component_index, pixel)? - DC_OFFSET);
|
||||
}
|
||||
}
|
||||
Ok(samples)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedImageBlockSource {
|
||||
fn sample(&self, component: usize, pixel: usize) -> Result<i32, Error> {
|
||||
let has_color = self
|
||||
.image
|
||||
.channels
|
||||
.contains(ManagedImageImageChannels::COLOR);
|
||||
let has_alpha = self
|
||||
.image
|
||||
.channels
|
||||
.contains(ManagedImageImageChannels::ALPHA);
|
||||
let value = if has_alpha {
|
||||
if has_color {
|
||||
match component {
|
||||
0 => self.image.red[pixel],
|
||||
1 => self.image.green[pixel],
|
||||
2 => self.image.blue[pixel],
|
||||
3 => self.image.alpha[pixel],
|
||||
_ => return Err(Error::IndexOutOfRange),
|
||||
}
|
||||
} else if component == 3 {
|
||||
u8::MAX
|
||||
} else {
|
||||
self.image.alpha[pixel]
|
||||
}
|
||||
} else if has_color {
|
||||
match component {
|
||||
0 => self.image.red[pixel],
|
||||
1 => self.image.green[pixel],
|
||||
2 => self.image.blue[pixel],
|
||||
3 => u8::MAX,
|
||||
_ => return Err(Error::IndexOutOfRange),
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
};
|
||||
Ok(i32::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_from_interleaved(
|
||||
width: i32,
|
||||
height: i32,
|
||||
channels: ManagedImageImageChannels,
|
||||
components: usize,
|
||||
bytes: &[u8],
|
||||
) -> Result<ManagedImage, Error> {
|
||||
let pixels = checked_pixels(width, height)?;
|
||||
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 checked_pixels(width: i32, height: i32) -> Result<usize, Error> {
|
||||
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
|
||||
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
|
||||
if width == 0 || height == 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
width
|
||||
.checked_mul(height)
|
||||
.filter(|pixels| *pixels <= libremetaverse_imaging::DEFAULT_MAX_PIXELS)
|
||||
.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
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 validate_component_index(component_index: usize) -> Result<(), Error> {
|
||||
if component_index < 4 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::IndexOutOfRange)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libremetaverse_imaging::codec::InterleavedComponent;
|
||||
|
||||
#[test]
|
||||
fn creator_maps_all_reference_component_orders() {
|
||||
let creator = ManagedImageCreator::new().unwrap();
|
||||
let cases = [
|
||||
(1, vec![1], ManagedImageImageChannels::GRAY),
|
||||
(
|
||||
2,
|
||||
vec![1, 2],
|
||||
ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
|
||||
),
|
||||
(3, vec![1, 2, 3], ManagedImageImageChannels::COLOR),
|
||||
(
|
||||
4,
|
||||
vec![1, 2, 3, 4],
|
||||
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
||||
),
|
||||
(
|
||||
5,
|
||||
vec![1, 2, 3, 4, 5],
|
||||
ManagedImageImageChannels::COLOR
|
||||
| ManagedImageImageChannels::BUMP
|
||||
| ManagedImageImageChannels::ALPHA,
|
||||
),
|
||||
];
|
||||
for (components, bytes, channels) in cases {
|
||||
let wrapper = creator.create(1, 1, components, bytes).unwrap();
|
||||
let image = wrapper.as_managed_image().expect("managed image");
|
||||
assert_eq!(image.channels, channels);
|
||||
assert_eq!(image.red, [1]);
|
||||
if components >= 3 {
|
||||
assert_eq!(image.green, [2]);
|
||||
assert_eq!(image.blue, [3]);
|
||||
}
|
||||
if components == 2 || components == 4 {
|
||||
assert_eq!(image.alpha[0], u8::try_from(components).unwrap());
|
||||
}
|
||||
if components == 5 {
|
||||
assert_eq!(image.bump, [4]);
|
||||
assert_eq!(image.alpha, [5]);
|
||||
}
|
||||
}
|
||||
assert!(matches!(
|
||||
creator.create(1, 1, 4, vec![0; 3]),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaved_extension_scales_samples_and_keeps_bump_before_alpha() {
|
||||
let components = vec![
|
||||
InterleavedComponent::new(16, false, false, vec![65_535]).unwrap(),
|
||||
InterleavedComponent::new(8, false, false, vec![2]).unwrap(),
|
||||
InterleavedComponent::new(8, false, false, vec![3]).unwrap(),
|
||||
InterleavedComponent::new(8, false, false, vec![4]).unwrap(),
|
||||
InterleavedComponent::new(8, false, true, vec![5]).unwrap(),
|
||||
];
|
||||
let image = InterleavedImage::new(1, 1, components).unwrap();
|
||||
let image = ManagedImageInterleavedExtensions::to_managed_image(image).unwrap();
|
||||
assert_eq!(image.red, [255]);
|
||||
assert_eq!(image.green, [2]);
|
||||
assert_eq!(image.blue, [3]);
|
||||
assert_eq!(image.bump, [4]);
|
||||
assert_eq!(image.alpha, [5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_source_matches_dc_offset_and_channel_substitution() {
|
||||
let creator = ManagedImageCreator::new().unwrap();
|
||||
let mut image = ManagedImage::new(
|
||||
2,
|
||||
1,
|
||||
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
||||
)
|
||||
.unwrap();
|
||||
image.red.copy_from_slice(&[0, 255]);
|
||||
image.green.copy_from_slice(&[1, 2]);
|
||||
image.blue.copy_from_slice(&[3, 4]);
|
||||
image.alpha.copy_from_slice(&[5, 6]);
|
||||
let source = creator
|
||||
.to_portable_image_source(Object::opaque(image))
|
||||
.unwrap();
|
||||
assert_eq!(source.component_block(0, 0, 0, 2, 1).unwrap(), [-128, 127]);
|
||||
assert_eq!(source.component_block(3, 0, 0, 2, 1).unwrap(), [-123, -122]);
|
||||
assert_eq!(source.nominal_range_bits(0), Ok(8));
|
||||
assert_eq!(source.fixed_point(0), Ok(0));
|
||||
assert_eq!(source.is_original_signed(0), Ok(false));
|
||||
assert_eq!(
|
||||
source.component_block(4, 0, 0, 1, 1),
|
||||
Err(Error::IndexOutOfRange)
|
||||
);
|
||||
|
||||
let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
|
||||
alpha.alpha[0] = 42;
|
||||
let source = creator
|
||||
.to_portable_image_source(Object::opaque(alpha))
|
||||
.unwrap();
|
||||
assert_eq!(source.component_block(0, 0, 0, 1, 1).unwrap(), [-86]);
|
||||
assert_eq!(source.component_block(3, 0, 0, 1, 1).unwrap(), [127]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
extern crate self as libremetaverse;
|
||||
|
||||
mod generated;
|
||||
mod j2k;
|
||||
mod targa;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -101,5 +101,5 @@
|
||||
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
|
||||
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
|
||||
],
|
||||
"support_passes": 109
|
||||
"support_passes": 115
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ TARGETS = {
|
||||
# implementations. The generated module keeps catalog markers and re-exports
|
||||
# the hand-written type so coverage remains deterministic.
|
||||
NATIVE_TYPES = {
|
||||
"T:LibreMetaverse.Imaging.ManagedImageCreator": "crate::j2k::ManagedImageCreator",
|
||||
"T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions": "crate::j2k::ManagedImageInterleavedExtensions",
|
||||
"T:LibreMetaverse.Imaging.Targa": "crate::targa::Targa",
|
||||
"T:LibreMetaverse.Imaging.ITextureCodec": "crate::managed_image::ITextureCodec",
|
||||
"T:LibreMetaverse.Imaging.ManagedImage": "crate::managed_image::ManagedImage",
|
||||
|
||||
Reference in New Issue
Block a user