use crate::error::IoError;
use kornia_image::{Image, ImageSize};
use std::{fs, path::Path};
const MAGIC: &[u8; 4] = b"RVL1";
const MAGIC_DELTA: &[u8; 4] = b"RVLD";
const HEADER_LEN: usize = 12;
const MAX_PIXELS: usize = 8192 * 8192;
struct NibbleWriter {
buf: Vec<u8>,
pending: Option<u8>,
}
impl NibbleWriter {
fn with_capacity(cap: usize) -> Self {
Self {
buf: Vec::with_capacity(cap),
pending: None,
}
}
#[inline(always)]
fn write_nibble(&mut self, n: u8) {
let n = n & 0xF;
match self.pending.take() {
None => self.pending = Some(n),
Some(hi) => self.buf.push((hi << 4) | n),
}
}
fn finish(mut self) -> Vec<u8> {
if let Some(hi) = self.pending.take() {
self.buf.push(hi << 4);
}
self.buf
}
}
struct NibbleReader<'a> {
data: &'a [u8],
pos: usize,
hi: bool,
}
impl<'a> NibbleReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: 0,
hi: true,
}
}
#[inline(always)]
fn next_nibble(&mut self) -> Option<u8> {
let byte = *self.data.get(self.pos)?;
if self.hi {
self.hi = false;
Some(byte >> 4)
} else {
self.hi = true;
self.pos += 1;
Some(byte & 0xF)
}
}
}
#[inline(always)]
fn encode_vle(writer: &mut NibbleWriter, mut val: u32) {
loop {
let low3 = (val & 0x7) as u8;
val >>= 3;
writer.write_nibble(if val != 0 { low3 | 0x8 } else { low3 });
if val == 0 {
break;
}
}
}
#[inline(always)]
fn decode_vle(reader: &mut NibbleReader) -> Result<u32, IoError> {
let mut val = 0u32;
let mut shift = 0u32;
loop {
let nibble = reader
.next_nibble()
.ok_or_else(|| IoError::RvlDecodeError("unexpected end of nibble stream".into()))?;
if shift == 30 && nibble & 0x4 != 0 {
return Err(IoError::RvlDecodeError(
"variable-length value exceeds u32 range".into(),
));
}
val |= ((nibble & 0x7) as u32) << shift;
shift += 3;
if nibble & 0x8 == 0 {
break;
}
if shift > 30 {
return Err(IoError::RvlDecodeError(
"variable-length value too long".into(),
));
}
}
Ok(val)
}
#[inline(always)]
fn zigzag(delta: i32) -> u32 {
((delta << 1) ^ (delta >> 31)) as u32
}
#[inline(always)]
fn unzigzag(v: u32) -> i32 {
((v >> 1) as i32) ^ -((v & 1) as i32)
}
pub fn encode_image_rvl(image: &Image<u16, 1>) -> Result<Vec<u8>, IoError> {
Ok(encode_rvl_stream(
image.as_slice(),
image.width() as u32,
image.height() as u32,
MAGIC,
))
}
pub fn encode_image_rvl_slice(
pixels: &[u16],
width: usize,
height: usize,
) -> Result<Vec<u8>, IoError> {
let expected = width.checked_mul(height).ok_or_else(|| {
IoError::RvlEncodeError(format!("image dimensions {width}x{height} overflow"))
})?;
if pixels.len() != expected {
return Err(IoError::RvlEncodeError(format!(
"{width}x{height} needs {expected} values, got {}",
pixels.len()
)));
}
Ok(encode_rvl_stream(
pixels,
width as u32,
height as u32,
MAGIC,
))
}
pub fn encode_image_rvl_delta(
image: &Image<u16, 1>,
previous: &Image<u16, 1>,
) -> Result<Vec<u8>, IoError> {
let (pixels, prev) = (image.as_slice(), previous.as_slice());
if pixels.len() != prev.len() {
return Err(IoError::RvlEncodeError(format!(
"delta reference is {} values, frame is {}",
prev.len(),
pixels.len()
)));
}
let mut deltas = Vec::with_capacity(pixels.len());
for (i, (&cur, &prev)) in pixels.iter().zip(prev).enumerate() {
let delta = cur as i32 - prev as i32;
if !(-32768..32768).contains(&delta) {
return Err(IoError::RvlEncodeError(format!(
"delta {delta} at pixel {i} ({prev} -> {cur}) is outside -32768..=32767; the \
zigzag would wrap and the payload would not be lossless — send a keyframe \
(`encode_image_rvl`) for that frame instead"
)));
}
deltas.push(zigzag(delta) as u16);
}
Ok(encode_rvl_stream(
&deltas,
image.width() as u32,
image.height() as u32,
MAGIC_DELTA,
))
}
fn encode_rvl_stream(pixels: &[u16], width: u32, height: u32, magic: &[u8; 4]) -> Vec<u8> {
let mut writer = NibbleWriter::with_capacity(pixels.len());
let mut previous: i32 = 0;
let mut i = 0usize;
let n = pixels.len();
while i < n {
let zeros_start = i;
while i < n && pixels[i] == 0 {
i += 1;
}
encode_vle(&mut writer, (i - zeros_start) as u32);
let nz_start = i;
while i < n && pixels[i] != 0 {
i += 1;
}
encode_vle(&mut writer, (i - nz_start) as u32);
for &d in &pixels[nz_start..i] {
let cur = d as i32;
encode_vle(&mut writer, zigzag(cur - previous));
previous = cur;
}
}
let stream = writer.finish();
let mut out = Vec::with_capacity(HEADER_LEN + stream.len());
out.extend_from_slice(magic);
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&height.to_le_bytes());
out.extend_from_slice(&stream);
out
}
pub fn decode_image_rvl(src: &[u8]) -> Result<Image<u16, 1>, IoError> {
let s = decode_rvl_stream(src, MAGIC, "RVL1")?;
let size = ImageSize {
width: s.width,
height: s.height,
};
Ok(Image::new(size, s.values)?)
}
pub fn decode_image_rvl_delta(
src: &[u8],
previous: &Image<u16, 1>,
) -> Result<Image<u16, 1>, IoError> {
let s = decode_rvl_stream(src, MAGIC_DELTA, "RVLD")?;
let prev = previous.as_slice();
if s.values.len() != prev.len() {
return Err(IoError::RvlDecodeError(format!(
"delta reference is {} values, payload is {}",
prev.len(),
s.values.len()
)));
}
let pixels = s
.values
.iter()
.zip(prev)
.map(|(&zz, &p)| (p as i32 + unzigzag(zz as u32)) as u16)
.collect();
let size = ImageSize {
width: s.width,
height: s.height,
};
Ok(Image::new(size, pixels)?)
}
struct RvlStream {
values: Vec<u16>,
width: usize,
height: usize,
}
fn decode_rvl_stream(src: &[u8], magic: &[u8; 4], what: &str) -> Result<RvlStream, IoError> {
if src.len() < HEADER_LEN {
return Err(IoError::RvlDecodeError(
"buffer too short for 12-byte RVL header".into(),
));
}
if &src[..4] != magic {
return Err(IoError::RvlDecodeError(format!(
"invalid magic bytes — expected a {what} payload"
)));
}
let width = u32::from_le_bytes(src[4..8].try_into().unwrap()) as usize;
let height = u32::from_le_bytes(src[8..12].try_into().unwrap()) as usize;
let n_pixels = width
.checked_mul(height)
.ok_or_else(|| IoError::RvlDecodeError("image dimensions overflow".into()))?;
if n_pixels > MAX_PIXELS {
return Err(IoError::RvlDecodeError(format!(
"image {width}x{height} exceeds max {MAX_PIXELS} pixels"
)));
}
let mut pixels: Vec<u16> = crate::limits::try_alloc_zeroed(n_pixels)?;
let mut reader = NibbleReader::new(&src[HEADER_LEN..]);
let mut previous: i32 = 0;
let mut i = 0usize;
while i < n_pixels {
let zeros = decode_vle(&mut reader)? as usize;
i = i
.checked_add(zeros)
.filter(|&i| i <= n_pixels)
.ok_or_else(|| {
IoError::RvlDecodeError("zero run overruns the declared image size".into())
})?;
if i == n_pixels {
break;
}
let nonzeros = decode_vle(&mut reader)? as usize;
let end = i
.checked_add(nonzeros)
.filter(|&e| e <= n_pixels)
.ok_or_else(|| {
IoError::RvlDecodeError("non-zero run overruns the declared image size".into())
})?;
if nonzeros == 0 && zeros == 0 {
return Err(IoError::RvlDecodeError(
"stream makes no progress (empty zero and non-zero runs)".into(),
));
}
for p in &mut pixels[i..end] {
let value = previous.wrapping_add(unzigzag(decode_vle(&mut reader)?)) as u16;
*p = value;
previous = value as i32;
}
i = end;
}
Ok(RvlStream {
values: pixels,
width,
height,
})
}
pub fn write_image_rvl(file_path: impl AsRef<Path>, image: &Image<u16, 1>) -> Result<(), IoError> {
let bytes = encode_image_rvl(image)?;
fs::write(file_path, bytes)?;
Ok(())
}
pub fn read_image_rvl(file_path: impl AsRef<Path>) -> Result<Image<u16, 1>, IoError> {
let bytes = fs::read(file_path)?;
decode_image_rvl(&bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_image(data: Vec<u16>, w: usize, h: usize) -> Image<u16, 1> {
Image::new(
ImageSize {
width: w,
height: h,
},
data,
)
.unwrap()
}
#[test]
fn decode_rejects_oversized_dimensions() {
let mut data = MAGIC.to_vec();
data.extend_from_slice(&0xFFFF_u32.to_le_bytes());
data.extend_from_slice(&0xFFFF_u32.to_le_bytes());
assert!(decode_image_rvl(&data).is_err());
}
#[test]
fn roundtrip_zeros() {
let img = make_image(vec![0u16; 64], 8, 8);
let enc = encode_image_rvl(&img).unwrap();
let dec = decode_image_rvl(&enc).unwrap();
assert_eq!(dec.as_slice(), img.as_slice());
}
#[test]
fn roundtrip_constant() {
let img = make_image(vec![1000u16; 100], 10, 10);
let enc = encode_image_rvl(&img).unwrap();
let dec = decode_image_rvl(&enc).unwrap();
assert_eq!(dec.as_slice(), img.as_slice());
}
#[test]
fn roundtrip_ramp() {
let data: Vec<u16> = (0..1024).map(|x| (x * 64) as u16).collect();
let img = make_image(data, 32, 32);
let enc = encode_image_rvl(&img).unwrap();
let dec = decode_image_rvl(&enc).unwrap();
assert_eq!(dec.as_slice(), img.as_slice());
}
#[test]
fn roundtrip_max_delta() {
let data: Vec<u16> = (0..64)
.map(|i: usize| if i.is_multiple_of(2) { 0 } else { 65535 })
.collect();
let img = make_image(data, 8, 8);
let enc = encode_image_rvl(&img).unwrap();
let dec = decode_image_rvl(&enc).unwrap();
assert_eq!(dec.as_slice(), img.as_slice());
}
#[test]
fn roundtrip_hd_frame() {
let data: Vec<u16> = (0..1280 * 720)
.map(|i| ((i as u32 * 7 + i as u32 / 100) % 4500 + 500) as u16)
.collect();
let img = make_image(data, 1280, 720);
let enc = encode_image_rvl(&img).unwrap();
let dec = decode_image_rvl(&enc).unwrap();
assert_eq!(dec.as_slice(), img.as_slice());
assert!(enc.len() < 1_843_200, "compressed={}", enc.len());
}
#[test]
fn roundtrip_sparse_depth_frame() {
let (w, h) = (320usize, 180usize);
let data: Vec<u16> = (0..w * h)
.map(|i| {
let (x, y) = (i % w, i / w);
if (60..140).contains(&y) && (80..240).contains(&x) {
0 } else {
(800 + x * 3 + y) as u16
}
})
.collect();
let img = make_image(data.clone(), w, h);
let enc = encode_image_rvl(&img).unwrap();
assert_eq!(decode_image_rvl(&enc).unwrap().as_slice(), data.as_slice());
}
#[test]
fn roundtrip_frame_ending_in_a_zero_run() {
let mut data = vec![1234u16; 10];
data.extend(std::iter::repeat_n(0u16, 22));
let img = make_image(data.clone(), 8, 4);
let enc = encode_image_rvl(&img).unwrap();
assert_eq!(decode_image_rvl(&enc).unwrap().as_slice(), data.as_slice());
}
#[test]
fn header_magic_validated() {
let mut bad = b"PNG\x89".to_vec();
bad.extend_from_slice(&[0u8; 8]);
assert!(decode_image_rvl(&bad).is_err());
}
#[test]
fn header_too_short() {
assert!(decode_image_rvl(b"RVL").is_err());
}
#[test]
fn decode_rejects_a_run_longer_than_the_frame() {
let mut data = MAGIC.to_vec();
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&8u32.to_le_bytes());
let mut w = NibbleWriter::with_capacity(8);
encode_vle(&mut w, 1 << 21);
data.extend_from_slice(&w.finish());
assert!(decode_image_rvl(&data).is_err());
}
#[test]
fn zigzag_inverse_identity() {
for &d in &[-65535i32, -32768, -1, 0, 1, 32767, 65535] {
assert_eq!(unzigzag(zigzag(d)), d, "zigzag failed for delta={d}");
}
}
#[test]
fn compression_ratio_zeros() {
let img = make_image(vec![0u16; 640 * 480], 640, 480);
let enc = encode_image_rvl(&img).unwrap();
assert!(
enc.len() < HEADER_LEN + 8,
"an all-zero frame should cost the header plus a single run count, got {}",
enc.len()
);
}
#[test]
fn compression_ratio_sparse_frame() {
let (w, h) = (320usize, 180usize);
let data: Vec<u16> = (0..w * h)
.map(|i| {
if i / w > (h * 36) / 100 {
0
} else {
(900 + (i % w) * 2) as u16
}
})
.collect();
let img = make_image(data, w, h);
let enc = encode_image_rvl(&img).unwrap();
let raw = w * h * 2;
assert!(
enc.len() * 4 < raw,
"expected >4x on a 64% invalid frame, got {:.2}x ({} bytes)",
raw as f64 / enc.len() as f64,
enc.len()
);
}
#[test]
fn slice_encoder_matches_the_image_encoder_byte_for_byte() {
let data = vec![0u16, 0, 1000, 1002, 0, 65535, 1, 0];
let img = make_image(data.clone(), 4, 2);
assert_eq!(
encode_image_rvl_slice(&data, 4, 2).unwrap(),
encode_image_rvl(&img).unwrap()
);
}
#[test]
fn slice_encoder_rejects_dims_that_disagree_with_the_buffer() {
let data = vec![0u16; 8];
assert!(encode_image_rvl_slice(&data, 4, 3).is_err());
assert!(encode_image_rvl_slice(&data, 4, 2).is_ok());
}
#[test]
fn roundtrip_delta_against_its_reference() {
let size = (4usize, 2usize);
let prev = make_image(vec![1000u16, 1000, 0, 500, 0, 0, 300, 301], size.0, size.1);
let cur = make_image(vec![1000u16, 1002, 0, 495, 0, 7, 300, 299], size.0, size.1);
let enc = encode_image_rvl_delta(&cur, &prev).unwrap();
assert_eq!(&enc[..4], MAGIC_DELTA, "delta must carry the RVLD magic");
let dec = decode_image_rvl_delta(&enc, &prev).unwrap();
assert_eq!(dec.as_slice(), cur.as_slice());
}
#[test]
fn an_unchanged_frame_collapses_to_almost_nothing() {
let (w, h) = (320usize, 180usize);
let data: Vec<u16> = (0..w * h).map(|i| (900 + i % 500) as u16).collect();
let img = make_image(data, w, h);
let enc = encode_image_rvl_delta(&img, &img).unwrap();
assert!(
enc.len() < 64,
"an identical frame should cost a handful of bytes, got {}",
enc.len()
);
}
#[test]
fn delta_rejects_a_swing_the_zigzag_cannot_represent() {
let one = |v: u16| make_image(vec![v], 1, 1);
assert!(
encode_image_rvl_delta(&one(0), &one(32768)).is_ok(),
"-32768 is representable and must encode"
);
assert!(
encode_image_rvl_delta(&one(32768), &one(0)).is_err(),
"+32768 would wrap the zigzag and must be refused, not silently truncated"
);
}
#[test]
fn delta_rejects_a_mismatched_reference_size() {
let cur = make_image(vec![1u16; 8], 4, 2);
let prev = make_image(vec![1u16; 6], 3, 2);
assert!(encode_image_rvl_delta(&cur, &prev).is_err());
let enc = encode_image_rvl_delta(&cur, &cur).unwrap();
assert!(decode_image_rvl_delta(&enc, &prev).is_err());
}
#[test]
fn the_two_magics_do_not_decode_as_each_other() {
let img = make_image(vec![1000u16, 1001, 0, 500], 4, 1);
let keyframe = encode_image_rvl(&img).unwrap();
let delta = encode_image_rvl_delta(&img, &img).unwrap();
assert!(decode_image_rvl(&delta).is_err());
assert!(decode_image_rvl_delta(&keyframe, &img).is_err());
}
#[test]
fn corrupt_deltas_do_not_overflow_the_accumulator() {
let mut w = NibbleWriter::with_capacity(32);
encode_vle(&mut w, 0); encode_vle(&mut w, 2); encode_vle(&mut w, 0xFFFF_FFFE); encode_vle(&mut w, 0xFFFF_FFFE);
let mut src = Vec::new();
src.extend_from_slice(MAGIC);
src.extend_from_slice(&2u32.to_le_bytes());
src.extend_from_slice(&1u32.to_le_bytes());
src.extend_from_slice(&w.finish());
let _ = decode_image_rvl(&src);
}
}