use std::path::Path;
use bevy::{
app::{App, Plugin},
asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages},
prelude::*,
};
use bevy_reflect::TypePath;
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
use jpeg2k::{error, DecodeParameters, Image, ImageData, ImagePixelData};
#[cfg(feature = "path_lod")]
use std::path::Component;
#[cfg(feature = "path_lod")]
use std::sync::Arc;
#[cfg(feature = "path_lod")]
use bevy::{
asset::{
io::{
AssetReaderError, AssetSource, AssetSourceBuilder, AssetSourceId, ErasedAssetReader,
PathStream,
},
AssetApp,
},
tasks::BoxedFuture,
};
#[cfg(feature = "path_lod")]
fn get_top_folder(path: &Path) -> Option<&str> {
path.components().next().and_then(|top| {
if let Component::Normal(top) = top {
top.to_str()
} else {
None
}
})
}
#[cfg(feature = "path_lod")]
fn strip_lod(path: &Path) -> (&Path, Option<u32>) {
if let Some(top) = get_top_folder(path) {
if top.starts_with("LOD_") {
let lod = top[4..].parse().ok();
return (path.strip_prefix(top).unwrap(), lod);
}
}
(path, None)
}
#[cfg(feature = "path_lod")]
fn lod_params(path: &Path) -> DecodeParameters {
let (_, lod) = strip_lod(path);
lod
.map(|lod| DecodeParameters::new().reduce(lod))
.unwrap_or_default()
}
#[cfg(not(feature = "path_lod"))]
fn lod_params(_path: &Path) -> DecodeParameters {
DecodeParameters::new()
}
#[cfg(feature = "path_lod")]
struct LodFileAssetReader {
inner: Arc<dyn ErasedAssetReader>,
}
#[cfg(feature = "path_lod")]
impl LodFileAssetReader {
fn new(inner: Arc<dyn ErasedAssetReader>) -> Self {
Self { inner }
}
}
#[cfg(feature = "path_lod")]
impl ErasedAssetReader for LodFileAssetReader {
fn read<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
let (stripped, _) = strip_lod(path);
self.inner.read(stripped)
}
fn read_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
let (stripped, _) = strip_lod(path);
self.inner.read_meta(stripped)
}
fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
let (stripped, _) = strip_lod(path);
self.inner.read_directory(stripped)
}
fn is_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<bool, AssetReaderError>> {
let (stripped, _) = strip_lod(path);
self.inner.is_directory(stripped)
}
fn read_meta_bytes<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>> {
let (stripped, _) = strip_lod(path);
self.inner.read_meta_bytes(stripped)
}
}
#[cfg(feature = "path_lod")]
fn lod_default_source(file_path: &str) -> AssetSourceBuilder {
let reader_path = file_path.to_string();
AssetSourceBuilder::platform_default(file_path, None).with_reader(move || {
let inner = AssetSource::get_default_reader(reader_path.clone())();
Box::new(LodFileAssetReader::new(inner.into()))
})
}
#[cfg(feature = "path_lod")]
#[derive(Debug, Clone)]
pub struct Jpeg2KPathLodPlugin {
pub file_path: String,
}
#[cfg(feature = "path_lod")]
impl Default for Jpeg2KPathLodPlugin {
fn default() -> Self {
Self {
file_path: "assets".to_string(),
}
}
}
#[cfg(feature = "path_lod")]
impl Plugin for Jpeg2KPathLodPlugin {
fn build(&self, app: &mut App) {
app.register_asset_source(
AssetSourceId::Default,
lod_default_source(&self.file_path),
);
}
}
#[derive(Default, TypePath)]
pub struct Jpeg2KAssetLoader;
impl AssetLoader for Jpeg2KAssetLoader {
type Asset = bevy::image::Image;
type Settings = ();
type Error = anyhow::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let params = lod_params(load_context.path().path());
let image = Image::from_bytes_with(&bytes, params)?;
let txt = image_to_texture(image)?;
Ok(txt)
}
fn extensions(&self) -> &[&str] {
&["j2k", "jp2", "j2c", "jpc"]
}
}
pub fn image_to_texture(img: Image) -> error::Result<bevy::image::Image> {
let ImageData {
width,
height,
data,
..
} = match img.num_components() {
1 | 4 => img.get_pixels(None)?,
3 => img.get_pixels(Some(u32::MAX))?,
num => {
return Err(error::Error::UnsupportedComponentsError(num));
}
};
let (format, data) = match data {
ImagePixelData::L8(data) => (TextureFormat::R8Unorm, data),
ImagePixelData::La8(data) => (TextureFormat::Rg8Unorm, data),
ImagePixelData::Rgba8(data) => (TextureFormat::Rgba8UnormSrgb, data),
_ => {
return Err(error::Error::UnknownFormatError(format!(
"Unsupported pixel data."
)));
}
};
Ok(bevy::image::Image::new(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
format,
RenderAssetUsages::all(),
))
}
#[derive(Default, Clone, Debug)]
pub struct Jpeg2KPlugin;
impl Plugin for Jpeg2KPlugin {
fn build(&self, app: &mut App) {
app.init_asset_loader::<Jpeg2KAssetLoader>();
}
}