use core::fmt;
use crate::bytes::{Cursor, OutOfBounds};
use crate::Rgb6;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Slab {
pub ztop: u8,
pub vis: u8,
pub colors: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct Kvx {
pub xsiz: u32,
pub ysiz: u32,
pub zsiz: u32,
pub xpivot: u32,
pub ypivot: u32,
pub zpivot: u32,
pub xoffset: Vec<u32>,
pub xyoffset: Vec<Vec<u16>>,
pub columns: Vec<Vec<Vec<Slab>>>,
pub palette: [Rgb6; 256],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
TooSmall { got: usize },
Truncated { at: usize, need: usize },
NonMonotonicOffsets { x: u32, y: u32 },
SlabOverrun { x: u32, y: u32 },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::TooSmall { got } => write!(
f,
"kvx file too small ({got} bytes; need at least 28 byte header + 768 byte palette)"
),
Self::Truncated { at, need } => {
write!(f, "kvx truncated: need {need} bytes at offset {at}")
}
Self::NonMonotonicOffsets { x, y } => write!(
f,
"kvx column (x={x}, y={y}): xyoffset table is non-monotonic"
),
Self::SlabOverrun { x, y } => write!(
f,
"kvx column (x={x}, y={y}): slab declared zleng overruns column byte budget"
),
}
}
}
impl std::error::Error for ParseError {}
impl From<OutOfBounds> for ParseError {
fn from(e: OutOfBounds) -> Self {
Self::Truncated {
at: e.at,
need: e.need,
}
}
}
const HEADER_LEN: usize = 28;
const PALETTE_LEN: usize = 768;
pub fn parse(bytes: &[u8]) -> Result<Kvx, ParseError> {
if bytes.len() < HEADER_LEN + PALETTE_LEN {
return Err(ParseError::TooSmall { got: bytes.len() });
}
let mut cur = Cursor::new(bytes);
let _numbytes = cur.read_u32()?;
let xsiz = cur.read_u32()?;
let ysiz = cur.read_u32()?;
let zsiz = cur.read_u32()?;
let xpivot = cur.read_u32()?;
let ypivot = cur.read_u32()?;
let zpivot = cur.read_u32()?;
let xoff_len = xsiz as usize + 1;
let mut xoffset = Vec::with_capacity(xoff_len);
for _ in 0..xoff_len {
xoffset.push(cur.read_u32()?);
}
let yoff_len = ysiz as usize + 1;
let mut xyoffset = Vec::with_capacity(xsiz as usize);
for _ in 0..xsiz {
let mut row = Vec::with_capacity(yoff_len);
for _ in 0..yoff_len {
row.push(cur.read_u16()?);
}
xyoffset.push(row);
}
let slab_region_start = cur.pos;
let slab_region_end = bytes
.len()
.checked_sub(PALETTE_LEN)
.ok_or(ParseError::TooSmall { got: bytes.len() })?;
let mut columns = Vec::with_capacity(xsiz as usize);
let mut slabs_cur = Cursor::new(&bytes[..slab_region_end]);
slabs_cur.pos = slab_region_start;
for x in 0..xsiz {
let mut col = Vec::with_capacity(ysiz as usize);
for y in 0..ysiz {
let lo = u32::from(xyoffset[x as usize][y as usize]);
let hi = u32::from(xyoffset[x as usize][y as usize + 1]);
if hi < lo {
return Err(ParseError::NonMonotonicOffsets { x, y });
}
let nbytes = (hi - lo) as usize;
let mut budget = nbytes;
let mut slabs = Vec::new();
while budget > 0 {
if budget < 3 {
return Err(ParseError::SlabOverrun { x, y });
}
let ztop = slabs_cur.read_u8()?;
let zleng = slabs_cur.read_u8()? as usize;
let vis = slabs_cur.read_u8()?;
budget -= 3;
if zleng > budget {
return Err(ParseError::SlabOverrun { x, y });
}
let mut colors = vec![0u8; zleng];
for c in &mut colors {
*c = slabs_cur.read_u8()?;
}
budget -= zleng;
slabs.push(Slab { ztop, vis, colors });
}
col.push(slabs);
}
columns.push(col);
}
let mut palette = [Rgb6 { r: 0, g: 0, b: 0 }; 256];
let pal = &bytes[bytes.len() - PALETTE_LEN..];
for (i, e) in palette.iter_mut().enumerate() {
e.r = pal[i * 3];
e.g = pal[i * 3 + 1];
e.b = pal[i * 3 + 2];
}
Ok(Kvx {
xsiz,
ysiz,
zsiz,
xpivot,
ypivot,
zpivot,
xoffset,
xyoffset,
columns,
palette,
})
}
#[must_use]
pub fn serialize(kvx: &Kvx) -> Vec<u8> {
let slab_bytes_total: usize = kvx
.columns
.iter()
.flatten()
.flatten()
.map(|s| 3 + s.colors.len())
.sum();
let offset_table_bytes =
kvx.xoffset.len() * 4 + kvx.xyoffset.iter().map(|row| row.len() * 2).sum::<usize>();
let numbytes = HEADER_LEN - 4 + offset_table_bytes + slab_bytes_total;
let numbytes_u32 = u32::try_from(numbytes).expect("kvx file >= 4 GiB is not representable");
let mut out =
Vec::with_capacity(HEADER_LEN + offset_table_bytes + slab_bytes_total + PALETTE_LEN);
out.extend_from_slice(&numbytes_u32.to_le_bytes());
out.extend_from_slice(&kvx.xsiz.to_le_bytes());
out.extend_from_slice(&kvx.ysiz.to_le_bytes());
out.extend_from_slice(&kvx.zsiz.to_le_bytes());
out.extend_from_slice(&kvx.xpivot.to_le_bytes());
out.extend_from_slice(&kvx.ypivot.to_le_bytes());
out.extend_from_slice(&kvx.zpivot.to_le_bytes());
for v in &kvx.xoffset {
out.extend_from_slice(&v.to_le_bytes());
}
for row in &kvx.xyoffset {
for v in row {
out.extend_from_slice(&v.to_le_bytes());
}
}
for col in &kvx.columns {
for slabs in col {
for s in slabs {
let zleng = u8::try_from(s.colors.len())
.expect("kvx slab zleng must fit in u8 (file format limit)");
out.push(s.ztop);
out.push(zleng);
out.push(s.vis);
out.extend_from_slice(&s.colors);
}
}
}
for e in &kvx.palette {
out.push(e.r);
out.push(e.g);
out.push(e.b);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const COCO_KVX: &[u8] = include_bytes!("../../../assets/coco.kvx");
#[test]
fn parse_coco_header() {
let kvx = parse(COCO_KVX).expect("parse coco.kvx");
assert_eq!(kvx.xsiz, 9);
assert_eq!(kvx.ysiz, 11);
assert_eq!(kvx.zsiz, 9);
assert_eq!(kvx.xpivot, 0x200);
assert_eq!(kvx.ypivot, 0x300);
assert_eq!(kvx.zpivot, 0x900);
assert_eq!(kvx.columns.len(), 9);
assert_eq!(kvx.columns[0].len(), 11);
}
#[test]
fn coco_palette_widening() {
let kvx = parse(COCO_KVX).expect("parse coco.kvx");
let p0 = kvx.palette[0];
assert_eq!((p0.r, p0.g, p0.b), (0x3f, 0x19, 0x19));
let want = 0x8000_0000u32 | (0x3fu32 << 18) | (0x19u32 << 10) | (0x19u32 << 2);
assert_eq!(p0.to_voxlap_argb(), want);
}
#[test]
fn coco_roundtrips_byte_equal() {
let kvx = parse(COCO_KVX).expect("parse coco.kvx");
let out = serialize(&kvx);
assert_eq!(out.len(), COCO_KVX.len(), "length differs");
assert_eq!(out.as_slice(), COCO_KVX, "byte content differs");
}
#[test]
fn parse_truncated_fails() {
let r = parse(&[0u8; 16]);
assert!(matches!(r, Err(ParseError::TooSmall { .. })));
}
}