feat(imaging): add pure-Rust JPEG 2000 backend (#109)
Some checks failed
CI / required (push) Failing after 3m18s

This commit is contained in:
2026-08-13 05:44:30 +00:00
parent 27c860c225
commit 0bab5b4c8d
28 changed files with 2770 additions and 109 deletions

View File

@@ -11,6 +11,7 @@ description = "Rust rewrite shell for the LibreMetaverse client library"
default = ["dds-bc67"]
dds-bc67 = ["dep:bcdec_rs"]
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
rust-j2k = ["libremetaverse-imaging/rust-j2k"]
vorbis = ["dep:vorbis_rs"]
[dependencies]

View File

@@ -15,10 +15,10 @@ they use those APIs directly.
| Crate | Choose it for | Features or system boundary |
| --- | --- | --- |
| `libremetaverse` | Grid client, login, network, agents, inventory, assets, appearance, world, and social APIs | Default pure-Rust BC6H/BC7; optional `jpeg2000` and `vorbis` |
| `libremetaverse` | Grid client, login, network, agents, inventory, assets, appearance, world, and social APIs | Default pure-Rust BC6H/BC7; optional `jpeg2000`, `rust-j2k`, and `vorbis` |
| `libremetaverse-types` | UUIDs, vectors, matrices, colors, cancellation, compatibility collections, and boundary types | Pure Rust |
| `libremetaverse-structured-data` | LLSD/OSD XML, JSON, binary, and notation | Pure Rust |
| `libremetaverse-imaging` | Managed images, TGA/DDS, codec traits | Optional `jpeg2000` |
| `libremetaverse-imaging` | Managed images, TGA/DDS, codec traits | Optional `jpeg2000` or pure-Rust `rust-j2k` |
| `libremetaverse-imaging-skia` | Common raster formats through Skia | Optional `skia`; native Skia build/cache |
| `libremetaverse-prim-mesher` | Legacy prim and sculpt geometry | Pure Rust |
| `libremetaverse-rendering-simple` | Deterministic reference geometry | Pure Rust |
@@ -190,6 +190,7 @@ The default client build needs no image-codec system library. Optional native
features require:
- `OpenJPEG` 2.5.4 or newer for `jpeg2000`;
- no native prerequisite for the pure-Rust `rust-j2k` feature;
- the rust-skia prerequisites/cache for `skia`;
- system libopus for WebRTC voice and the `libremetaverse-opus` adapter;
- ALSA development headers on Linux, `CoreAudio` on macOS, or WASAPI on Windows

View File

@@ -150,7 +150,7 @@ impl Baker {
let texture = crate::assets::AssetTexture::new_with_managed_image(output)?;
// JPEG2000 is the protocol representation. Keep the decoded image even
// when a build intentionally excludes that optional codec.
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
texture.encode()?;
self.baked_texture = Some(texture);
Ok(())

View File

@@ -9,7 +9,11 @@
)]
use crate::{Error, Permissions};
#[cfg(feature = "jpeg2000")]
use libremetaverse_imaging::J2kCodec as TextureJ2kCodec;
use libremetaverse_imaging::ManagedImage;
#[cfg(all(not(feature = "jpeg2000"), feature = "rust-j2k"))]
use libremetaverse_imaging::RustJ2kCodec as TextureJ2kCodec;
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::{AssetType, SaleType, UUID, Vector3, WearableType};
use std::collections::HashMap;
@@ -526,9 +530,9 @@ impl AssetTexture {
if self.image.is_some() {
return Ok(true);
}
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
{
self.image = Some(libremetaverse_imaging::J2kCodec::decode_bytes(
self.image = Some(TextureJ2kCodec::decode_bytes(
&self.raw.bytes(),
libremetaverse_imaging::J2kDecodeOptions::default(),
)?);
@@ -543,7 +547,7 @@ impl AssetTexture {
};
Ok(true)
}
#[cfg(not(feature = "jpeg2000"))]
#[cfg(not(any(feature = "jpeg2000", feature = "rust-j2k")))]
{
Err(Error::InvalidOperation)
}
@@ -570,15 +574,15 @@ impl AssetTexture {
})
}
pub fn decode(&self) -> Result<bool, Error> {
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
{
Ok(libremetaverse_imaging::J2kCodec::decode_bytes(
Ok(TextureJ2kCodec::decode_bytes(
&self.raw.bytes(),
libremetaverse_imaging::J2kDecodeOptions::default(),
)
.is_ok())
}
#[cfg(not(feature = "jpeg2000"))]
#[cfg(not(any(feature = "jpeg2000", feature = "rust-j2k")))]
{
validate_bytes(&self.raw.bytes())?;
Err(Error::InvalidOperation)
@@ -586,15 +590,15 @@ impl AssetTexture {
}
pub fn encode(&self) -> Result<(), Error> {
let image = self.image.as_ref().ok_or(Error::InvalidOperation)?;
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
{
let encoded = libremetaverse_imaging::J2kCodec::encode(
let encoded = TextureJ2kCodec::encode(
image,
libremetaverse_imaging::J2kEncodeOptions::default(),
)?;
self.raw.replace(encoded)
}
#[cfg(not(feature = "jpeg2000"))]
#[cfg(not(any(feature = "jpeg2000", feature = "rust-j2k")))]
{
let _ = image;
Err(Error::InvalidOperation)

View File

@@ -1,10 +1,10 @@
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
use crate::assets::AssetTexture;
use crate::assets::{AssetMesh, AssetMutable, AssetSound};
use crate::{
AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
};
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_types::{AssetType, UUID};
use std::fs;
@@ -58,7 +58,7 @@ fn sound_codec_is_explicitly_feature_gated() {
}
#[test]
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
fn texture_codec_produces_real_valid_payload() {
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
image.red.fill(255);

View File

@@ -11,6 +11,10 @@ use std::path::{Component, Path, PathBuf};
use flate2::{Compression, write::ZlibEncoder};
use libremetaverse_imaging::ITextureCodec;
#[cfg(feature = "jpeg2000")]
use libremetaverse_imaging::J2kCodec as TextureJ2kCodec;
#[cfg(all(not(feature = "jpeg2000"), feature = "rust-j2k"))]
use libremetaverse_imaging::RustJ2kCodec as TextureJ2kCodec;
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use libremetaverse_types::compat::{CancellationToken, Uri};
use libremetaverse_types::{AssetType, Color4, Error, Quaternion, UUID, Vector2, Vector3};
@@ -956,14 +960,14 @@ fn load_texture(
return Err(Error::InvalidOperation);
};
let image = codec.decode(Box::new(std::io::Cursor::new(bytes)))?;
#[cfg(feature = "jpeg2000")]
#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))]
{
return libremetaverse_imaging::J2kCodec::encode(
return TextureJ2kCodec::encode(
&image,
libremetaverse_imaging::J2kEncodeOptions::default(),
);
}
#[cfg(not(feature = "jpeg2000"))]
#[cfg(not(any(feature = "jpeg2000", feature = "rust-j2k")))]
{
let _ = image;
return Err(Error::InvalidOperation);