use std::io::{self, Read};
use crate::dxt::{self, DxtFlags};
use crate::error::BlpError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ColorEncoding {
Jpeg = 0,
Palette = 1,
Dxt = 2,
Argb8888 = 3,
Argb8888Dup = 4,
}
impl TryFrom<u8> for ColorEncoding {
type Error = BlpError;
fn try_from(v: u8) -> Result<Self, Self::Error> {
match v {
0 => Ok(Self::Jpeg),
1 => Ok(Self::Palette),
2 => Ok(Self::Dxt),
3 => Ok(Self::Argb8888),
4 => Ok(Self::Argb8888Dup),
_ => Err(BlpError::UnsupportedEncoding(v)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PixelFormat {
Dxt1 = 0,
Dxt3 = 1,
Argb8888 = 2,
Argb1555 = 3,
Argb4444 = 4,
Rgb565 = 5,
A8 = 6,
Dxt5 = 7,
Unspecified = 8,
Argb2565 = 9,
Bc5 = 11,
}
impl TryFrom<u8> for PixelFormat {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
match v {
0 => Ok(Self::Dxt1),
1 => Ok(Self::Dxt3),
2 => Ok(Self::Argb8888),
3 => Ok(Self::Argb1555),
4 => Ok(Self::Argb4444),
5 => Ok(Self::Rgb565),
6 => Ok(Self::A8),
7 => Ok(Self::Dxt5),
8 => Ok(Self::Unspecified),
9 => Ok(Self::Argb2565),
11 => Ok(Self::Bc5),
_ => Err(()),
}
}
}
#[derive(Clone, Copy, Default)]
struct Rgba8 {
r: u8,
g: u8,
b: u8,
a: u8,
}
const MAX_IMAGE_BYTES: usize = 256 * 1024 * 1024;
const MAX_JPEG_HEADER: usize = 64 * 1024;
pub struct BlpFile {
pub color_encoding: ColorEncoding,
pub alpha_size: u8,
pub preferred_format: PixelFormat,
pub width: u32,
pub height: u32,
mip_offsets: [u32; 16],
mip_sizes: [u32; 16],
palette: [Rgba8; 256],
jpeg_header: Vec<u8>,
data: Vec<u8>,
}
impl BlpFile {
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, BlpError> {
Self::from_bytes(std::fs::read(path)?)
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self, BlpError> {
parse_header(io::Cursor::new(data))
}
}
impl BlpFile {
pub fn mipmap_count(&self) -> usize {
self.mip_offsets.iter().take_while(|&&o| o != 0).count()
}
pub fn get_pixels(&self, mipmap_level: u32) -> Result<(Vec<u8>, u32, u32), BlpError> {
let count = self.mipmap_count();
if count == 0 {
return Err(BlpError::NoMipmaps);
}
let level = (mipmap_level as usize).min(count - 1);
let scale = 1u32 << level;
let w = (self.width / scale).max(1);
let h = (self.height / scale).max(1);
let byte_count = (w as usize)
.checked_mul(h as usize)
.and_then(|n| n.checked_mul(4))
.ok_or(BlpError::ImageTooLarge)?;
if byte_count > MAX_IMAGE_BYTES {
return Err(BlpError::ImageTooLarge);
}
let raw = self.mip_data(level)?;
let pixels = self.decode(w, h, raw)?;
Ok((pixels, w, h))
}
}
impl BlpFile {
fn mip_data(&self, level: usize) -> Result<&[u8], BlpError> {
let off = self.mip_offsets[level] as usize;
let size = self.mip_sizes[level] as usize;
let end = off.checked_add(size).ok_or(BlpError::OutOfBounds)?;
self.data.get(off..end).ok_or(BlpError::OutOfBounds)
}
fn decode(&self, w: u32, h: u32, data: &[u8]) -> Result<Vec<u8>, BlpError> {
match self.color_encoding {
ColorEncoding::Jpeg => self.decode_jpeg(data),
ColorEncoding::Palette => self.decode_palette(w, h, data),
ColorEncoding::Dxt => self.decode_dxt(w, h, data),
ColorEncoding::Argb8888 | ColorEncoding::Argb8888Dup => Ok(bgra_to_rgba(data)),
}
}
fn decode_jpeg(&self, data: &[u8]) -> Result<Vec<u8>, BlpError> {
let combined: Vec<u8> = if self.jpeg_header.is_empty() {
data.to_vec()
} else {
let mut v = Vec::with_capacity(self.jpeg_header.len() + data.len());
v.extend_from_slice(&self.jpeg_header);
v.extend_from_slice(data);
v
};
let img =
image::load_from_memory(&combined).map_err(|e| BlpError::JpegDecode(e.to_string()))?;
Ok(img.to_rgba8().into_raw())
}
fn decode_palette(&self, w: u32, h: u32, data: &[u8]) -> Result<Vec<u8>, BlpError> {
let n_pixels = (w as usize)
.checked_mul(h as usize)
.ok_or(BlpError::ImageTooLarge)?;
if data.len() < n_pixels {
return Err(BlpError::DataTooShort);
}
if self.alpha_size != 0 {
let alpha_bytes: usize = match self.alpha_size {
1 => n_pixels.div_ceil(8), 4 => n_pixels.div_ceil(2), 8 => n_pixels, _ => 0,
};
let required = n_pixels
.checked_add(alpha_bytes)
.ok_or(BlpError::DataTooShort)?;
if data.len() < required {
return Err(BlpError::DataTooShort);
}
}
let mut out = vec![0u8; n_pixels * 4];
for i in 0..n_pixels {
let c = self.palette[data[i] as usize];
out[i * 4] = c.r;
out[i * 4 + 1] = c.g;
out[i * 4 + 2] = c.b;
out[i * 4 + 3] = palette_alpha(data, i, n_pixels, self.alpha_size);
}
Ok(out)
}
fn decode_dxt(&self, w: u32, h: u32, data: &[u8]) -> Result<Vec<u8>, BlpError> {
let flag = if self.alpha_size > 1 {
if self.preferred_format == PixelFormat::Dxt5 {
DxtFlags::Dxt5
} else {
DxtFlags::Dxt3
}
} else {
DxtFlags::Dxt1
};
dxt::decompress_image(w, h, data, flag).ok_or(BlpError::ImageTooLarge)
}
}
fn parse_header(mut cur: io::Cursor<Vec<u8>>) -> Result<BlpFile, BlpError> {
const MAGIC_BLP0: u32 = 0x30504c42; const MAGIC_BLP1: u32 = 0x31504c42; const MAGIC_BLP2: u32 = 0x32504c42;
let magic = read_u32(&mut cur)?;
if magic != MAGIC_BLP0 && magic != MAGIC_BLP1 && magic != MAGIC_BLP2 {
return Err(BlpError::InvalidMagic);
}
let (color_encoding, alpha_size, preferred_format, width, height) = match magic {
MAGIC_BLP0 | MAGIC_BLP1 => {
let enc = ColorEncoding::try_from(read_i32(&mut cur)? as u8)?;
let alpha = read_i32(&mut cur)? as u8;
let w = read_i32(&mut cur)? as u32;
let h = read_i32(&mut cur)? as u32;
let pf = PixelFormat::try_from(read_i32(&mut cur)? as u8)
.unwrap_or(PixelFormat::Unspecified);
let _hm = read_i32(&mut cur)?; (enc, alpha, pf, w, h)
}
MAGIC_BLP2 => {
let ver = read_u32(&mut cur)?;
if ver != 1 {
return Err(BlpError::InvalidFormatVersion(ver));
}
let enc = ColorEncoding::try_from(read_u8(&mut cur)?)?;
let alpha = read_u8(&mut cur)?;
let pf = PixelFormat::try_from(read_u8(&mut cur)?).unwrap_or(PixelFormat::Unspecified);
let _hm = read_u8(&mut cur)?;
let w = read_i32(&mut cur)? as u32;
let h = read_i32(&mut cur)? as u32;
(enc, alpha, pf, w, h)
}
_ => unreachable!(),
};
let mut mip_offsets = [0u32; 16];
for o in &mut mip_offsets {
*o = read_u32(&mut cur)?;
}
let mut mip_sizes = [0u32; 16];
for s in &mut mip_sizes {
*s = read_u32(&mut cur)?;
}
let mut palette = [Rgba8::default(); 256];
let mut jpeg_header = Vec::new();
match color_encoding {
ColorEncoding::Palette => {
for c in &mut palette {
let v = read_i32(&mut cur)?;
c.b = (v & 0xFF) as u8;
c.g = ((v >> 8) & 0xFF) as u8;
c.r = ((v >> 16) & 0xFF) as u8;
c.a = ((v >> 24) & 0xFF) as u8;
}
}
ColorEncoding::Jpeg => {
let hdr_size = read_i32(&mut cur)? as usize;
if hdr_size > MAX_JPEG_HEADER {
return Err(BlpError::DataTooShort);
}
let mut hdr = vec![0u8; hdr_size];
cur.read_exact(&mut hdr)?;
jpeg_header = hdr;
}
_ => {}
}
let data = cur.into_inner();
Ok(BlpFile {
color_encoding,
alpha_size,
preferred_format,
width,
height,
mip_offsets,
mip_sizes,
palette,
jpeg_header,
data,
})
}
fn palette_alpha(data: &[u8], index: usize, alpha_start: usize, alpha_size: u8) -> u8 {
match alpha_size {
1 => {
let byte = data[alpha_start + index / 8];
if byte & (0x01 << (index % 8)) != 0 {
0xFF
} else {
0x00
}
}
4 => {
let byte = data[alpha_start + index / 2];
if index.is_multiple_of(2) {
(byte & 0x0F) << 4
} else {
byte & 0xF0
}
}
8 => data[alpha_start + index],
_ => 0xFF,
}
}
fn bgra_to_rgba(src: &[u8]) -> Vec<u8> {
let mut out = src.to_vec();
for chunk in out.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
out
}
fn read_u8(r: &mut impl Read) -> io::Result<u8> {
let mut b = [0u8; 1];
r.read_exact(&mut b)?;
Ok(b[0])
}
fn read_u32(r: &mut impl Read) -> io::Result<u32> {
let mut b = [0u8; 4];
r.read_exact(&mut b)?;
Ok(u32::from_le_bytes(b))
}
fn read_i32(r: &mut impl Read) -> io::Result<i32> {
let mut b = [0u8; 4];
r.read_exact(&mut b)?;
Ok(i32::from_le_bytes(b))
}