use std::io::Read;
pub const MAGIC: [u8; 4] = *b"DPCK";
pub const VERSION: u8 = 1;
pub const HEADER_LEN: usize = 46;
pub const UNIT_LEN: usize = 8;
pub const MAX_PIXELS: u64 = 1 << 27;
const CODEC_MED: u8 = 0;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("buffer has {got} values, expected width*height = {expected}")]
BadDimensions { got: usize, expected: u64 },
#[error("unsupported dimensions {width}x{height}")]
UnsupportedDimensions { width: u32, height: u32 },
#[error("scale must be finite and non-zero, offset finite")]
BadScale,
#[error("unit label must be <= {UNIT_LEN} ASCII bytes")]
BadUnit,
#[error("not a depthpack blob")]
NotDepthpack,
#[error("unsupported version {version} / codec {codec}")]
Unsupported { version: u8, codec: u8 },
#[error("corrupt blob: {0}")]
Corrupt(&'static str),
}
#[derive(Debug, Clone)]
pub struct EncodeOptions {
pub scale: f64,
pub offset: f64,
pub unit: String,
pub zstd_level: i32,
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
scale: 1.0,
offset: 0.0,
unit: String::new(),
zstd_level: 1,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Header {
pub width: u32,
pub height: u32,
pub n_valid: u32,
pub scale: f64,
pub offset: f64,
pub unit: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DepthImage {
pub width: u32,
pub height: u32,
pub values: Vec<u16>,
pub scale: f64,
pub offset: f64,
pub unit: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScaledImage {
pub width: u32,
pub height: u32,
pub values: Vec<f32>,
pub unit: String,
}
#[inline(always)]
fn predict(l: u16, u: u16, ul: u16, prev: u16) -> u16 {
match (l != 0, u != 0) {
(true, true) => {
if ul != 0 {
let (l, u, ul) = (l as i32, u as i32, ul as i32);
let mx = l.max(u);
let mn = l.min(u);
(if ul >= mx {
mn
} else if ul <= mn {
mx
} else {
l + u - ul
}) as u16
} else {
((l as u32 + u as u32) / 2) as u16
}
}
(true, false) => l,
(false, true) => u,
(false, false) => prev,
}
}
#[inline(always)]
fn zigzag(d: i16) -> u16 {
(((d as i32) << 1) ^ ((d as i32) >> 31)) as u16
}
#[inline(always)]
fn unzigzag(z: u16) -> i16 {
(((z >> 1) as i32) ^ -((z & 1) as i32)) as i16
}
fn validate_dims(width: u32, height: u32) -> Result<usize, Error> {
let n = width as u64 * height as u64;
if width == 0 || height == 0 || n > MAX_PIXELS {
return Err(Error::UnsupportedDimensions { width, height });
}
Ok(n as usize)
}
fn encode_unit(unit: &str) -> Result<[u8; UNIT_LEN], Error> {
let bytes = unit.as_bytes();
if bytes.len() > UNIT_LEN || !unit.is_ascii() {
return Err(Error::BadUnit);
}
let mut field = [0u8; UNIT_LEN];
field[..bytes.len()].copy_from_slice(bytes);
Ok(field)
}
fn decode_unit(field: &[u8]) -> Result<String, Error> {
let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
if !field[..end].is_ascii() || field[end..].iter().any(|&b| b != 0) {
return Err(Error::Corrupt("unit label not ascii / not null-padded"));
}
Ok(String::from_utf8_lossy(&field[..end]).into_owned())
}
#[cfg(feature = "zstd-c")]
fn zstd_compress(buf: &[u8], level: i32) -> Result<Vec<u8>, Error> {
zstd::bulk::compress(buf, level).map_err(|_| Error::Corrupt("zstd compress"))
}
#[cfg(not(feature = "zstd-c"))]
fn zstd_compress(buf: &[u8], _level: i32) -> Result<Vec<u8>, Error> {
Ok(ruzstd::encoding::compress_to_vec(
buf,
ruzstd::encoding::CompressionLevel::Fastest,
))
}
pub fn encode(
counts: &[u16],
width: u32,
height: u32,
opts: &EncodeOptions,
) -> Result<Vec<u8>, Error> {
let n = validate_dims(width, height)?;
if counts.len() != n {
return Err(Error::BadDimensions {
got: counts.len(),
expected: n as u64,
});
}
if !opts.scale.is_finite() || opts.scale == 0.0 || !opts.offset.is_finite() {
return Err(Error::BadScale);
}
let unit = encode_unit(&opts.unit)?;
let mut mask = vec![0u8; n.div_ceil(8)];
let mut n_valid: u32 = 0;
for (i, &v) in counts.iter().enumerate() {
if v != 0 {
mask[i / 8] |= 1 << (i % 8);
n_valid += 1;
}
}
let (w, h) = (width as usize, height as usize);
let mut res: Vec<u16> = Vec::with_capacity(n_valid as usize);
let mut prev: u16 = 0;
for y in 0..h {
let row = y * w;
for x in 0..w {
let i = row + x;
let v = counts[i];
if v == 0 {
continue;
}
let l = if x > 0 { counts[i - 1] } else { 0 };
let u = if y > 0 { counts[i - w] } else { 0 };
let ul = if x > 0 && y > 0 { counts[i - w - 1] } else { 0 };
res.push(zigzag(v.wrapping_sub(predict(l, u, ul, prev)) as i16));
prev = v;
}
}
let m = res.len();
let mut planes = Vec::with_capacity(m * 2);
planes.extend(res.iter().map(|&v| (v >> 8) as u8));
planes.extend(res.iter().map(|&v| (v & 0xFF) as u8));
let zm = zstd_compress(&mask, opts.zstd_level)?;
let zr = zstd_compress(&planes, opts.zstd_level)?;
let mut out = Vec::with_capacity(HEADER_LEN + zm.len() + zr.len());
out.extend_from_slice(&MAGIC);
out.push(VERSION);
out.push(CODEC_MED);
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&height.to_le_bytes());
out.extend_from_slice(&n_valid.to_le_bytes());
out.extend_from_slice(&opts.scale.to_le_bytes());
out.extend_from_slice(&opts.offset.to_le_bytes());
out.extend_from_slice(&unit);
out.extend_from_slice(&(zm.len() as u32).to_le_bytes());
out.extend_from_slice(&zm);
out.extend_from_slice(&zr);
Ok(out)
}
pub fn decode_header(blob: &[u8]) -> Result<Header, Error> {
if blob.len() < HEADER_LEN || blob[0..4] != MAGIC {
return Err(Error::NotDepthpack);
}
let version = blob[4];
let codec = blob[5];
if version != VERSION || codec != CODEC_MED {
return Err(Error::Unsupported { version, codec });
}
let width = u32::from_le_bytes(blob[6..10].try_into().unwrap());
let height = u32::from_le_bytes(blob[10..14].try_into().unwrap());
let n_valid = u32::from_le_bytes(blob[14..18].try_into().unwrap());
let scale = f64::from_le_bytes(blob[18..26].try_into().unwrap());
let offset = f64::from_le_bytes(blob[26..34].try_into().unwrap());
let unit = decode_unit(&blob[34..42])?;
if !scale.is_finite() || scale == 0.0 || !offset.is_finite() {
return Err(Error::Corrupt("non-finite or zero scale/offset"));
}
let n = validate_dims(width, height)?;
if n_valid as usize > n {
return Err(Error::Corrupt("n_valid exceeds pixel count"));
}
Ok(Header {
width,
height,
n_valid,
scale,
offset,
unit,
})
}
fn zstd_to_exact(src: &[u8], expected: usize, what: &'static str) -> Result<Vec<u8>, Error> {
let mut dec = ruzstd::decoding::StreamingDecoder::new(src).map_err(|_| Error::Corrupt(what))?;
let mut out = vec![0u8; expected];
dec.read_exact(&mut out).map_err(|_| Error::Corrupt(what))?;
let mut probe = [0u8; 1];
match dec.read(&mut probe) {
Ok(0) => Ok(out),
_ => Err(Error::Corrupt(what)),
}
}
pub fn decode(blob: &[u8]) -> Result<DepthImage, Error> {
let hdr = decode_header(blob)?;
let n = hdr.width as usize * hdr.height as usize;
let mut values = vec![0u16; n];
decode_into(blob, &mut values)?;
Ok(DepthImage {
width: hdr.width,
height: hdr.height,
values,
scale: hdr.scale,
offset: hdr.offset,
unit: hdr.unit,
})
}
pub fn decode_into(blob: &[u8], out: &mut [u16]) -> Result<Header, Error> {
let hdr = decode_header(blob)?;
let (w, h) = (hdr.width as usize, hdr.height as usize);
let n = w * h;
if out.len() != n {
return Err(Error::BadDimensions {
got: out.len(),
expected: n as u64,
});
}
let mask_zlen = u32::from_le_bytes(blob[42..46].try_into().unwrap()) as usize;
let body = &blob[HEADER_LEN..];
if mask_zlen > body.len() {
return Err(Error::Corrupt("mask section overruns blob"));
}
let mask = zstd_to_exact(&body[..mask_zlen], n.div_ceil(8), "mask")?;
let mut popcount: u64 = mask.iter().map(|b| b.count_ones() as u64).sum();
let tail_bits = mask.len() * 8 - n;
if tail_bits > 0 {
let tail = mask[mask.len() - 1] >> (8 - tail_bits);
if tail != 0 {
return Err(Error::Corrupt("mask has bits past the last pixel"));
}
popcount -= tail.count_ones() as u64;
}
if popcount != hdr.n_valid as u64 {
return Err(Error::Corrupt("mask popcount disagrees with n_valid"));
}
let m = hdr.n_valid as usize;
let planes = zstd_to_exact(&body[mask_zlen..], m * 2, "residuals")?;
out.fill(0);
let mut k = 0usize;
let mut prev: u16 = 0;
for y in 0..h {
let row = y * w;
for x in 0..w {
let i = row + x;
if mask[i / 8] & (1 << (i % 8)) == 0 {
continue;
}
let l = if x > 0 { out[i - 1] } else { 0 };
let u = if y > 0 { out[i - w] } else { 0 };
let ul = if x > 0 && y > 0 { out[i - w - 1] } else { 0 };
let p = predict(l, u, ul, prev);
let z = ((planes[k] as u16) << 8) | planes[m + k] as u16;
let v = p.wrapping_add(unzigzag(z) as u16);
out[i] = v;
prev = v;
k += 1;
}
}
Ok(hdr)
}
pub fn decode_scaled(blob: &[u8]) -> Result<ScaledImage, Error> {
let hdr = decode_header(blob)?;
let n = hdr.width as usize * hdr.height as usize;
let mut values = vec![0f32; n];
decode_scaled_into(blob, &mut values)?;
Ok(ScaledImage {
width: hdr.width,
height: hdr.height,
values,
unit: hdr.unit,
})
}
pub fn decode_scaled_into(blob: &[u8], out: &mut [f32]) -> Result<Header, Error> {
let hdr = decode_header(blob)?;
let n = hdr.width as usize * hdr.height as usize;
if out.len() != n {
return Err(Error::BadDimensions {
got: out.len(),
expected: n as u64,
});
}
let mut lattice = vec![0u16; n];
decode_into(blob, &mut lattice)?;
let (scale, offset) = (hdr.scale, hdr.offset);
for (dst, &c) in out.iter_mut().zip(lattice.iter()) {
*dst = if c == 0 {
f32::NAN
} else {
(c as f64 * scale + offset) as f32
};
}
Ok(hdr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zigzag_roundtrips_full_i16_range() {
for d in i16::MIN..=i16::MAX {
assert_eq!(unzigzag(zigzag(d)), d, "d={d}");
}
}
#[test]
fn wrapping_residual_reconstructs_any_pair() {
for p in [0u16, 1, 2, 255, 256, 32_767, 32_768, 65_534, 65_535] {
for v in [0u16, 1, 2, 255, 256, 32_767, 32_768, 65_534, 65_535] {
let d = v.wrapping_sub(p) as i16;
assert_eq!(p.wrapping_add(unzigzag(zigzag(d)) as u16), v, "p={p} v={v}");
}
}
}
#[test]
fn unit_field_roundtrips() {
for u in ["", "m", "ftUS", "12345678"] {
assert_eq!(decode_unit(&encode_unit(u).unwrap()).unwrap(), u);
}
assert!(matches!(encode_unit("123456789"), Err(Error::BadUnit))); assert!(matches!(encode_unit("mé"), Err(Error::BadUnit))); }
#[test]
fn header_rejects_garbage() {
assert!(matches!(decode_header(b"nope"), Err(Error::NotDepthpack)));
assert!(matches!(
decode_header(&[0u8; HEADER_LEN]),
Err(Error::NotDepthpack)
));
}
}