use crate::api::AstcBlock;
use crate::basislz::etc1s::{
Endpoint, Etc1sTranscoder, Selector, VideoState, MAX_PREV_FRAME_LEVELS,
};
use crate::dispatch;
use alloc::vec::Vec;
const BASIS_SIG: u16 = 0x4273;
const BASIS_VERSION: u16 = 0x13;
const HEADER_SIZE: u16 = 77;
const SLICE_DESC_SIZE: usize = 23;
const TEX_FORMAT_ETC1S: u8 = 0;
const TEX_FORMAT_UASTC_LDR_4X4: u8 = 1;
const TEX_FORMAT_UASTC_HDR_4X4: u8 = 2;
const TEX_FORMAT_ASTC_HDR_6X6: u8 = 3;
const TEX_FORMAT_UASTC_HDR_6X6_INTERMEDIATE: u8 = 4;
const TEX_FORMAT_XUASTC_LDR_FIRST: u8 = 5;
const TEX_FORMAT_XUASTC_LDR_LAST: u8 = 18;
const TEX_FORMAT_ASTC_LDR_FIRST: u8 = 19;
const TEX_FORMAT_ASTC_LDR_LAST: u8 = 32;
const FLAG_HAS_ALPHA_SLICES: u16 = 4;
const FLAG_USES_GLOBAL_CODEBOOK: u16 = 8;
const FLAG_SRGB: u16 = 16;
const SLICE_FLAG_HAS_ALPHA: u8 = 1;
const SLICE_FLAG_IS_IFRAME: u8 = 2;
const TEX_TYPE_VIDEO_FRAMES: u8 = 3;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BasisSourceFormat {
Etc1s,
Uastc,
UastcHdr4x4,
AstcLdr(AstcBlock),
AstcHdr6x6,
UastcHdr6x6,
XuastcLdr(AstcBlock),
}
#[derive(Clone, Copy, Debug)]
struct SliceDesc {
image_index: u32,
level_index: u32,
flags: u8,
orig_width: u32,
orig_height: u32,
num_blocks_x: u32,
num_blocks_y: u32,
file_ofs: u32,
file_size: u32,
}
fn rd16(d: &[u8], o: usize) -> u32 {
u16::from_le_bytes([d[o], d[o + 1]]) as u32
}
fn rd24(d: &[u8], o: usize) -> u32 {
u32::from_le_bytes([d[o], d[o + 1], d[o + 2], 0])
}
fn rd32(d: &[u8], o: usize) -> u32 {
u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
}
pub struct BasisTranscoder<'a> {
data: &'a [u8],
format: BasisSourceFormat,
has_alpha: bool,
is_video: bool,
astc_is_srgb: bool,
base_width: u32,
base_height: u32,
level_count: u32,
total_images: u32,
slices: Vec<SliceDesc>,
etc1s: Option<Etc1sTranscoder>,
}
impl<'a> BasisTranscoder<'a> {
pub fn is_basis(data: &[u8]) -> bool {
data.len() >= 2 && rd16(data, 0) as u16 == BASIS_SIG
}
pub fn new(data: &'a [u8]) -> Option<Self> {
Self::new_with_codebook(data, None)
}
pub fn codebook(&self) -> Option<(&[Endpoint], &[Selector])> {
self.etc1s
.as_ref()
.map(|d| (d.endpoints.as_slice(), d.selectors.as_slice()))
}
pub fn new_with_codebook(
data: &'a [u8],
codebook: Option<(&[Endpoint], &[Selector])>,
) -> Option<Self> {
if data.len() <= HEADER_SIZE as usize {
return None;
}
if rd16(data, 0) as u16 != BASIS_SIG
|| rd16(data, 2) as u16 != BASIS_VERSION
|| rd16(data, 4) as u16 != HEADER_SIZE
{
return None;
}
let data_size = rd32(data, 8) as usize;
if data.len() < HEADER_SIZE as usize + data_size {
return None;
}
let total_slices = rd24(data, 14);
let total_images = rd24(data, 17);
if total_slices == 0 || total_images == 0 {
return None;
}
let tex_format = data[20];
let flags = rd16(data, 21) as u16;
let tex_type = data[23];
let slice_desc_ofs = rd32(data, 65) as usize;
let descs_bytes = SLICE_DESC_SIZE.checked_mul(total_slices as usize)?;
if slice_desc_ofs >= data.len() || data.len() - slice_desc_ofs < descs_bytes {
return None;
}
let format = match tex_format {
TEX_FORMAT_ETC1S => BasisSourceFormat::Etc1s,
TEX_FORMAT_UASTC_LDR_4X4 => BasisSourceFormat::Uastc,
TEX_FORMAT_UASTC_HDR_4X4 => BasisSourceFormat::UastcHdr4x4,
TEX_FORMAT_ASTC_LDR_FIRST..=TEX_FORMAT_ASTC_LDR_LAST => BasisSourceFormat::AstcLdr(
AstcBlock::ALL[(tex_format - TEX_FORMAT_ASTC_LDR_FIRST) as usize],
),
TEX_FORMAT_ASTC_HDR_6X6 => BasisSourceFormat::AstcHdr6x6,
TEX_FORMAT_UASTC_HDR_6X6_INTERMEDIATE => BasisSourceFormat::UastcHdr6x6,
TEX_FORMAT_XUASTC_LDR_FIRST..=TEX_FORMAT_XUASTC_LDR_LAST => {
BasisSourceFormat::XuastcLdr(
AstcBlock::ALL[(tex_format - TEX_FORMAT_XUASTC_LDR_FIRST) as usize],
)
}
_ => return None,
};
let uses_global_codebook =
format == BasisSourceFormat::Etc1s && (flags & FLAG_USES_GLOBAL_CODEBOOK) != 0;
let mut slices = Vec::with_capacity(total_slices as usize);
for i in 0..total_slices as usize {
let o = slice_desc_ofs + i * SLICE_DESC_SIZE;
slices.push(SliceDesc {
image_index: rd24(data, o),
level_index: data[o + 3] as u32,
flags: data[o + 4],
orig_width: rd16(data, o + 5),
orig_height: rd16(data, o + 7),
num_blocks_x: rd16(data, o + 9),
num_blocks_y: rd16(data, o + 11),
file_ofs: rd32(data, o + 13),
file_size: rd32(data, o + 17),
});
}
let has_alpha = match format {
BasisSourceFormat::Etc1s => (flags & FLAG_HAS_ALPHA_SLICES) != 0,
BasisSourceFormat::Uastc => slices.iter().any(|s| s.flags & SLICE_FLAG_HAS_ALPHA != 0),
BasisSourceFormat::UastcHdr4x4
| BasisSourceFormat::AstcHdr6x6
| BasisSourceFormat::UastcHdr6x6 => false,
BasisSourceFormat::XuastcLdr(_) => (flags & FLAG_HAS_ALPHA_SLICES) != 0,
BasisSourceFormat::AstcLdr(_) => (flags & FLAG_HAS_ALPHA_SLICES) != 0,
};
let astc_is_srgb = (flags & FLAG_SRGB) != 0;
let is_video = tex_type == TEX_TYPE_VIDEO_FRAMES
|| slices.iter().any(|s| s.flags & SLICE_FLAG_IS_IFRAME != 0);
let base = slices
.iter()
.find(|s| s.image_index == 0 && s.level_index == 0)?;
let base_width = base.orig_width;
let base_height = base.orig_height;
let level_count = Self::image_levels(&slices, 0);
let etc1s = if format == BasisSourceFormat::Etc1s {
let t_ofs = rd32(data, 57) as usize;
let t_size = rd32(data, 61) as usize;
let num_e = rd16(data, 39);
let num_s = rd16(data, 48);
if t_size == 0 {
return None;
}
let t = data.get(t_ofs..t_ofs.checked_add(t_size)?)?;
if uses_global_codebook {
let (endpoints, selectors) = codebook?;
if endpoints.len() as u32 != num_e || selectors.len() as u32 != num_s {
return None;
}
Some(Etc1sTranscoder::with_global_codebook(
endpoints.to_vec(),
selectors.to_vec(),
t,
)?)
} else {
let e_ofs = rd32(data, 41) as usize;
let e_size = rd24(data, 45) as usize;
let s_ofs = rd32(data, 50) as usize;
let s_size = rd24(data, 54) as usize;
if e_size == 0 || s_size == 0 {
return None;
}
let e = data.get(e_ofs..e_ofs.checked_add(e_size)?)?;
let s = data.get(s_ofs..s_ofs.checked_add(s_size)?)?;
let mut dec = Etc1sTranscoder::new();
if !dec.decode_palettes(num_e, e, num_s, s) || !dec.decode_tables(t) {
return None;
}
Some(dec)
}
} else {
None
};
Some(Self {
data,
format,
has_alpha,
is_video,
astc_is_srgb,
base_width,
base_height,
level_count,
total_images,
slices,
etc1s,
})
}
fn image_levels(slices: &[SliceDesc], image_index: u32) -> u32 {
let Some(start) = slices
.iter()
.position(|s| s.image_index == image_index && s.level_index == 0)
else {
return 1;
};
let mut total = 1;
for s in &slices[start + 1..] {
if s.image_index == image_index {
total = total.max(s.level_index + 1);
} else {
break;
}
}
total
}
pub fn source_format(&self) -> BasisSourceFormat {
self.format
}
pub fn has_alpha(&self) -> bool {
self.has_alpha
}
pub fn is_video(&self) -> bool {
self.is_video
}
pub fn base_dimensions(&self) -> (u32, u32) {
(self.base_width, self.base_height)
}
pub fn level_count(&self) -> u32 {
self.level_count
}
pub fn image_count(&self) -> u32 {
self.total_images
}
pub fn image_level_info(&self, image: u32, level: u32) -> Option<(u32, u32, u32, u32)> {
let s = self.find_first_slice(image, level)?;
(s.orig_width, s.orig_height, s.num_blocks_x, s.num_blocks_y).into()
}
fn find_first_slice(&self, image: u32, level: u32) -> Option<&SliceDesc> {
self.slices
.iter()
.find(|s| s.image_index == image && s.level_index == level)
}
fn etc1s_slices(&self, image: u32, level: u32) -> Option<(&'a [u8], Option<&'a [u8]>)> {
let idx = self
.slices
.iter()
.position(|s| s.image_index == image && s.level_index == level)?;
let color = &self.slices[idx];
if color.flags & SLICE_FLAG_HAS_ALPHA != 0 {
return None;
}
let co = color.file_ofs as usize;
let rgb = self
.data
.get(co..co.checked_add(color.file_size as usize)?)?;
let alpha = if self.has_alpha {
let a = self.slices.get(idx + 1)?;
if a.flags & SLICE_FLAG_HAS_ALPHA == 0
|| a.num_blocks_x != color.num_blocks_x
|| a.num_blocks_y != color.num_blocks_y
{
return None;
}
let ao = a.file_ofs as usize;
Some(self.data.get(ao..ao.checked_add(a.file_size as usize)?)?)
} else {
None
};
Some((rgb, alpha))
}
fn uastc_slice(&self, image: u32, level: u32) -> Option<&'a [u8]> {
let s = self.find_first_slice(image, level)?;
let o = s.file_ofs as usize;
self.data.get(o..o.checked_add(s.file_size as usize)?)
}
pub fn transcode_image_level(
&self,
image: u32,
level: u32,
fmt: i32,
flags: u32,
out: &mut [u8],
) -> Option<()> {
self.transcode_image_level_inner(image, level, fmt, flags, None, out)
}
pub fn transcode_image_level_video(
&self,
image: u32,
level: u32,
fmt: i32,
flags: u32,
state: &mut VideoState,
out: &mut [u8],
) -> Option<()> {
self.transcode_image_level_inner(image, level, fmt, flags, Some(state), out)
}
fn transcode_image_level_inner(
&self,
image: u32,
level: u32,
fmt: i32,
flags: u32,
video: Option<&mut VideoState>,
out: &mut [u8],
) -> Option<()> {
let s = self.find_first_slice(image, level)?;
let (bx, by) = (s.num_blocks_x, s.num_blocks_y);
let (lw, lh) = (s.orig_width, s.orig_height);
match self.format {
BasisSourceFormat::Etc1s => {
let t = self.etc1s.as_ref()?;
let (rgb, alpha) = self.etc1s_slices(image, level)?;
let vid = if self.is_video && (level as usize) < MAX_PREV_FRAME_LEVELS {
video.map(|st| (st, level))
} else {
None
};
dispatch::transcode_etc1s(t, rgb, alpha, bx, by, lw, lh, fmt, flags, vid, out)
}
BasisSourceFormat::Uastc => {
let img = self.uastc_slice(image, level)?;
dispatch::transcode_uastc(img, self.has_alpha, bx, by, lw, lh, fmt, flags, out)
}
BasisSourceFormat::UastcHdr4x4 => {
let img = self.uastc_slice(image, level)?;
dispatch::transcode_uastc_hdr(img, bx, by, lw, lh, fmt, out)
}
BasisSourceFormat::AstcLdr(b) => {
let img = self.uastc_slice(image, level)?;
dispatch::transcode_astc_ldr(
img,
b,
bx,
by,
lw,
lh,
fmt,
flags,
self.astc_is_srgb,
self.has_alpha,
out,
)
}
BasisSourceFormat::AstcHdr6x6 => {
let img = self.uastc_slice(image, level)?;
dispatch::transcode_astc_hdr_6x6(img, bx, by, lw, lh, fmt, flags, out)
}
BasisSourceFormat::UastcHdr6x6 => {
let stream = self.uastc_slice(image, level)?;
let (blocks, dec_w, dec_h) = crate::uastc_hdr_6x6::decode_6x6_hdr(stream)?;
if dec_w != lw || dec_h != lh {
return None;
}
dispatch::transcode_astc_hdr_6x6(&blocks, bx, by, lw, lh, fmt, flags, out)
}
BasisSourceFormat::XuastcLdr(b) => {
let stream = self.uastc_slice(image, level)?;
crate::xuastc::transcode::transcode_xuastc(
stream, b, bx, by, lw, lh, fmt, flags, out,
)
}
}
}
}