Implement optional JPEG2000 codec adapter (#40)
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled

This commit is contained in:
2026-08-09 03:42:18 +00:00
parent 5d1573fc82
commit 16501f2331
24 changed files with 2776 additions and 53 deletions

View 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
View File

@@ -1,4 +1,6 @@
/target/ /target/
/crates/libremetaverse-openjpeg/target/
/crates/libremetaverse-openjpeg/Cargo.lock
/tests/api-compile/target/ /tests/api-compile/target/
.env .env
.env.* .env.*

21
Cargo.lock generated
View File

@@ -218,6 +218,7 @@ dependencies = [
name = "libremetaverse-imaging" name = "libremetaverse-imaging"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"libremetaverse-openjpeg",
"libremetaverse-types", "libremetaverse-types",
] ]
@@ -236,6 +237,14 @@ dependencies = [
"libremetaverse-types", "libremetaverse-types",
] ]
[[package]]
name = "libremetaverse-openjpeg"
version = "0.0.1"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "libremetaverse-prim-mesher" name = "libremetaverse-prim-mesher"
version = "0.0.1" version = "0.0.1"
@@ -359,6 +368,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -510,6 +525,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"

View File

@@ -1,5 +1,6 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
exclude = ["crates/libremetaverse-openjpeg"]
members = [ members = [
"crates/libremetaverse-types", "crates/libremetaverse-types",
"crates/libremetaverse-structured-data", "crates/libremetaverse-structured-data",

View File

@@ -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; feature adds BC6H/BC7 decoding through the safe, pure-Rust `bcdec_rs` crate;
disabling default features removes that optional dependency and makes those disabling default features removes that optional dependency and makes those
two formats return a typed unsupported-format error. 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# The controlled audit aggregates every expected failure by standardized C#
member ID and rejects unrelated fixture, assertion, compile, or symbol errors. member ID and rejects unrelated fixture, assertion, compile, or symbol errors.

View File

@@ -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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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. |

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status | | 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.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | callable failure-only shim | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | callable failure-only shim |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -7,8 +7,13 @@ license.workspace = true
repository.workspace = true repository.workspace = true
description = "Texture codec abstractions for the MetaCrate LibreMetaverse rewrite" description = "Texture codec abstractions for the MetaCrate LibreMetaverse rewrite"
[features]
default = []
jpeg2000 = ["dep:libremetaverse-openjpeg"]
[dependencies] [dependencies]
libremetaverse-types = { path = "../libremetaverse-types" } libremetaverse-types = { path = "../libremetaverse-types" }
libremetaverse-openjpeg = { path = "../libremetaverse-openjpeg", optional = true }
[lints] [lints]
workspace = true workspace = true

View 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)
);
}
}

View 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);
}
}

View File

@@ -2,17 +2,16 @@
extern crate self as libremetaverse_imaging; extern crate self as libremetaverse_imaging;
pub mod codec { pub mod codec;
pub trait IImage {}
pub trait IImageCreator {} #[cfg(feature = "jpeg2000")]
pub struct ImageCreator<T>(pub std::marker::PhantomData<T>); mod jpeg2000;
pub struct InterleavedImage;
pub trait BlkImgDataSrc {}
}
mod generated; mod generated;
mod managed_image; mod managed_image;
pub use generated::*; pub use generated::*;
#[cfg(feature = "jpeg2000")]
pub use jpeg2000::{J2kCodec, J2kCompression, J2kDecodeOptions, J2kEncodeOptions, J2kFormat};
pub use libremetaverse_types::Error; pub use libremetaverse_types::Error;
pub use managed_image::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS}; pub use managed_image::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS};

View 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 }

View 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.

View 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");
}
}

View 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;
}

View 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(())
}

View File

@@ -1694,7 +1694,7 @@ fn object_to_osd(value: Object) -> Result<OSD, Error> {
Object::Vector4(value) => OSD::from_vector4(value)?, Object::Vector4(value) => OSD::from_vector4(value)?,
Object::Quaternion(value) => OSD::from_quaternion(value)?, Object::Quaternion(value) => OSD::from_quaternion(value)?,
Object::Color4(value) => OSD::from_color4(value)?, Object::Color4(value) => OSD::from_color4(value)?,
Object::Matrix4(_) => OSD::Undefined, Object::Matrix4(_) | Object::Opaque(_) => OSD::Undefined,
}) })
} }

View File

@@ -4,7 +4,9 @@
//! surfaces. Their behavior is implemented only when the owning API slice is //! surfaces. Their behavior is implemented only when the owning API slice is
//! ported. //! ported.
use std::any::Any;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt;
use std::future::Future; use std::future::Future;
use std::hash::Hash; use std::hash::Hash;
use std::marker::PhantomData; 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 {} 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)] #[derive(Clone, Debug)]
pub enum Object { pub enum Object {
Undefined, Undefined,
@@ -43,6 +93,34 @@ pub enum Object {
Vector3(crate::Vector3), Vector3(crate::Vector3),
Vector3d(crate::Vector3d), Vector3d(crate::Vector3d),
Vector4(crate::Vector4), 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 { impl PartialEq for Object {
@@ -69,6 +147,7 @@ impl PartialEq for Object {
(Self::Vector3(lhs), Self::Vector3(rhs)) => crate::Vector3::eq(*lhs, *rhs), (Self::Vector3(lhs), Self::Vector3(rhs)) => crate::Vector3::eq(*lhs, *rhs),
(Self::Vector3d(lhs), Self::Vector3d(rhs)) => crate::Vector3d::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::Vector4(lhs), Self::Vector4(rhs)) => crate::Vector4::eq(*lhs, *rhs),
(Self::Opaque(lhs), Self::Opaque(rhs)) => lhs == rhs,
_ => false, _ => false,
} }
} }
@@ -105,6 +184,7 @@ impl std::hash::Hash for Object {
Self::Vector3(value) => value.get_hash_code().hash(state), Self::Vector3(value) => value.get_hash_code().hash(state),
Self::Vector3d(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::Vector4(value) => value.get_hash_code().hash(state),
Self::Opaque(value) => value.hash(state),
} }
} }
} }
@@ -379,9 +459,10 @@ pub struct SocketException;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Uri}; use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Object, Uri};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::future::Future; use std::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll, Waker}; use std::task::{Context, Poll, Waker};
#[test] #[test]
@@ -427,4 +508,22 @@ mod tests {
.is_success_status_code() .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"))
);
}
} }

View File

@@ -10,6 +10,7 @@ description = "Rust rewrite shell for the LibreMetaverse client library"
[features] [features]
default = ["dds-bc67"] default = ["dds-bc67"]
dds-bc67 = ["dep:bcdec_rs"] dds-bc67 = ["dep:bcdec_rs"]
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
[dependencies] [dependencies]
bcdec_rs = { version = "0.2.0", optional = true } bcdec_rs = { version = "0.2.0", optional = true }

View File

@@ -34036,49 +34036,14 @@ pub mod imaging {
} }
/// C# type: `T:LibreMetaverse.Imaging.ManagedImageCreator`. /// C# type: `T:LibreMetaverse.Imaging.ManagedImageCreator`.
pub struct ManagedImageCreator;
impl ManagedImageCreator {
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.#ctor`. /// 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[])`. /// 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)`. /// C# member: `M:LibreMetaverse.Imaging.ManagedImageCreator.ToPortableImageSource(System.Object)`.
pub fn to_portable_image_source( pub use crate::j2k::ManagedImageCreator;
&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# type: `T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions`. /// C# type: `T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions`.
pub struct ManagedImageInterleavedExtensions;
impl ManagedImageInterleavedExtensions {
/// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`. /// C# member: `M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)`.
pub fn to_managed_image( pub use crate::j2k::ManagedImageInterleavedExtensions;
image: libremetaverse_imaging::codec::InterleavedImage,
) -> Result<libremetaverse_imaging::ManagedImage, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions.ToManagedImage(CoreJ2K.Util.InterleavedImage)",
)
}
}
/// C# type: `T:LibreMetaverse.Imaging.Targa`. /// C# type: `T:LibreMetaverse.Imaging.Targa`.
/// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`. /// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`.

View 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]);
}
}

View File

@@ -3,6 +3,7 @@
extern crate self as libremetaverse; extern crate self as libremetaverse;
mod generated; mod generated;
mod j2k;
mod targa; mod targa;
#[cfg(test)] #[cfg(test)]

View File

@@ -101,5 +101,5 @@
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
], ],
"support_passes": 109 "support_passes": 115
} }

View File

@@ -38,6 +38,8 @@ TARGETS = {
# implementations. The generated module keeps catalog markers and re-exports # implementations. The generated module keeps catalog markers and re-exports
# the hand-written type so coverage remains deterministic. # the hand-written type so coverage remains deterministic.
NATIVE_TYPES = { 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.Targa": "crate::targa::Targa",
"T:LibreMetaverse.Imaging.ITextureCodec": "crate::managed_image::ITextureCodec", "T:LibreMetaverse.Imaging.ITextureCodec": "crate::managed_image::ITextureCodec",
"T:LibreMetaverse.Imaging.ManagedImage": "crate::managed_image::ManagedImage", "T:LibreMetaverse.Imaging.ManagedImage": "crate::managed_image::ManagedImage",