Skip to main content

voxj_codec/
decode_voxj_object.rs

1use crate::{
2    Error, Result, decode_hilbert, decode_varint, hilbert_bits, packed_width, unpack_bits,
3};
4use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
5use std::iter;
6use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
7
8/// Decodes one [`VoxjSerdeObject`] back into a [`VoxjCodecObject`], the inverse
9/// of [`encode_voxj_object`](crate::encode_voxj_object). `cell_counts[p]` is
10/// the cell count of the palette referenced by `object.palette_refs[p]`, needed
11/// to recover the bit width of `packed-base64` samples;
12/// [`voxj_palette_cell_counts`](crate::voxj_palette_cell_counts) computes it
13/// from the document's palettes.
14///
15/// Bitmap and Hilbert positions decode in ascending cell / Hilbert-index order;
16/// the sample channels share that same order, so each returned `positions[k]`
17/// pairs with `samples[k]`.
18pub fn decode_voxj_object(
19    object: &VoxjSerdeObject,
20    cell_counts: &[usize],
21) -> Result<VoxjCodecObject> {
22    let positions = decode_positions(&object.voxel_positions, object.bounds)?;
23    let channels = decode_samples(&object.voxel_samples, cell_counts, positions.len())?;
24    let samples = (0..positions.len())
25        .map(|k| channels.iter().map(|channel| channel[k]).collect())
26        .collect();
27    Ok(VoxjCodecObject {
28        name: object.name.clone(),
29        palette_refs: object.palette_refs.clone(),
30        bounds: object.bounds,
31        positions,
32        samples,
33    })
34}
35
36/// Wraps a message describing malformed input as invalid data.
37fn invalid_data(message: String) -> Error {
38    Error::Invalid(message)
39}
40
41/// Inverse of the raster `cell_index`: `x = k / (Y*Z)`, `y = (k / Z) % Y`,
42/// `z = k % Z`.
43fn cell_to_position(cell: u64, bounds: [u32; 3]) -> [u32; 3] {
44    let plane = bounds[1] as u64 * bounds[2] as u64;
45    [
46        (cell / plane) as u32,
47        ((cell % plane) / bounds[2] as u64) as u32,
48        (cell % bounds[2] as u64) as u32,
49    ]
50}
51
52/// Decodes the position block into `[x, y, z]` positions.
53fn decode_positions(block: &VoxjSerdePositionBlock, bounds: [u32; 3]) -> Result<Vec<[u32; 3]>> {
54    Ok(match block {
55        VoxjSerdePositionBlock::RawJson(positions) => positions.clone(),
56
57        VoxjSerdePositionBlock::BitmapBase64(base64) => {
58            let cells = bounds[0] as usize * bounds[1] as usize * bounds[2] as usize;
59            let occupancy = unpack_bits(&BASE64.decode(base64).map_err(Error::Base64)?, 1, cells);
60            occupancy
61                .iter()
62                .enumerate()
63                .filter(|&(_, &bit)| bit == 1)
64                .map(|(cell, _)| cell_to_position(cell as u64, bounds))
65                .collect()
66        }
67
68        VoxjSerdePositionBlock::HilbertIndexDeltaVarintBase64(base64) => {
69            let bits = hilbert_bits(bounds);
70            let mut index = 0u64;
71            decode_varint(&BASE64.decode(base64).map_err(Error::Base64)?)
72                .iter()
73                .map(|&delta| {
74                    index += delta;
75                    decode_hilbert(index, bits)
76                })
77                .collect()
78        }
79    })
80}
81
82/// Decodes the sample block into one channel (`Vec<u32>` of length `n`) per
83/// referenced palette, in the position block's voxel order.
84fn decode_samples(
85    block: &VoxjSerdeSampleBlock,
86    cell_counts: &[usize],
87    n: usize,
88) -> Result<Vec<Vec<u32>>> {
89    let channels: Vec<Vec<u32>> = match block {
90        VoxjSerdeSampleBlock::RawJson(rows) => {
91            if rows.len() != n {
92                return Err(invalid_data(format!(
93                    "raw-json sample block has {} rows, expected {n}",
94                    rows.len()
95                )));
96            }
97            if let Some(row) = rows.iter().find(|row| row.len() != cell_counts.len()) {
98                return Err(invalid_data(format!(
99                    "raw-json sample row has {} values, expected {}",
100                    row.len(),
101                    cell_counts.len()
102                )));
103            }
104            (0..cell_counts.len())
105                .map(|p| rows.iter().map(|row| row[p]).collect())
106                .collect()
107        }
108
109        VoxjSerdeSampleBlock::RleJson(channels) => {
110            channels.iter().map(|channel| rle_decode(channel)).collect()
111        }
112
113        VoxjSerdeSampleBlock::PackedBase64(channels) => channels
114            .iter()
115            .enumerate()
116            .map(|(p, base64)| {
117                let width = packed_width(cell_counts.get(p).copied().unwrap_or(1));
118                let bytes = BASE64.decode(base64).map_err(Error::Base64)?;
119                let required = (n * width as usize).div_ceil(8);
120                if bytes.len() < required {
121                    return Err(invalid_data(format!(
122                        "packed sample channel {p} has {} bytes, need {required} for {n} values of width {width}",
123                        bytes.len()
124                    )));
125                }
126                Ok(unpack_bits(&bytes, width, n))
127            })
128            .collect::<Result<Vec<_>>>()?,
129    };
130
131    // Every encoding must yield one channel per referenced palette, each holding
132    // a value for every voxel; otherwise the object's samples are malformed.
133    if channels.len() != cell_counts.len() {
134        return Err(invalid_data(format!(
135            "sample block has {} channels, expected {} (one per referenced palette)",
136            channels.len(),
137            cell_counts.len()
138        )));
139    }
140    if let Some(channel) = channels.iter().find(|channel| channel.len() != n) {
141        return Err(invalid_data(format!(
142            "sample channel has {} values, expected {n}",
143            channel.len()
144        )));
145    }
146    Ok(channels)
147}
148
149/// Expands flat run-length encoding `[value, count, value, count, ...]`.
150fn rle_decode(rle: &[u32]) -> Vec<u32> {
151    let mut out = Vec::new();
152    for pair in rle.chunks_exact(2) {
153        out.extend(iter::repeat_n(pair[0], pair[1] as usize));
154    }
155    out
156}
157
158#[cfg(test)]
159mod tests {
160    use crate::{PositionEncoding, SampleEncoding, decode_voxj_object, encode_voxj_object};
161    use std::collections::BTreeSet;
162    use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
163
164    const POSITIONS: [PositionEncoding; 3] = [
165        PositionEncoding::RawJson,
166        PositionEncoding::BitmapBase64,
167        PositionEncoding::Hilbert,
168    ];
169    const SAMPLES: [SampleEncoding; 3] = [
170        SampleEncoding::RawJson,
171        SampleEncoding::RleJson,
172        SampleEncoding::PackedBase64,
173    ];
174
175    /// Cell counts of the two palettes `sample_object` references.
176    const CELL_COUNTS: [usize; 2] = [256, 8];
177
178    fn sample_object() -> VoxjCodecObject {
179        VoxjCodecObject {
180            name: "o".to_owned(),
181            palette_refs: vec![0, 1],
182            bounds: [4, 4, 5],
183            positions: vec![[0, 0, 0], [2, 1, 0], [1, 3, 4], [3, 3, 3]],
184            samples: vec![vec![1, 0], vec![5, 2], vec![200, 7], vec![0, 1]],
185        }
186    }
187
188    /// The set of `(position, samples)` pairs, order-independent, so it also
189    /// proves positions and samples stay aligned through any reordering.
190    fn voxel_set(object: &VoxjCodecObject) -> BTreeSet<([u32; 3], Vec<u32>)> {
191        object
192            .positions
193            .iter()
194            .copied()
195            .zip(object.samples.iter().cloned())
196            .collect()
197    }
198
199    #[test]
200    fn round_trips_every_encoding_pair() {
201        for position in POSITIONS {
202            for sample in SAMPLES {
203                let object = sample_object();
204                let (expected, bounds) = (voxel_set(&object), object.bounds);
205                let encoded = encode_voxj_object(&object, &CELL_COUNTS, position, sample).unwrap();
206                let decoded = decode_voxj_object(&encoded, &CELL_COUNTS).unwrap();
207                assert_eq!(
208                    voxel_set(&decoded),
209                    expected,
210                    "pair {position:?}/{sample:?}"
211                );
212                assert_eq!(decoded.bounds, bounds, "pair {position:?}/{sample:?}");
213            }
214        }
215    }
216
217    #[test]
218    fn round_trips_empty_object() {
219        let object = VoxjCodecObject {
220            name: "o".to_owned(),
221            palette_refs: Vec::new(),
222            bounds: [0, 0, 0],
223            positions: Vec::new(),
224            samples: Vec::new(),
225        };
226        let encoded = encode_voxj_object(
227            &object,
228            &[],
229            PositionEncoding::RawJson,
230            SampleEncoding::RawJson,
231        )
232        .unwrap();
233        let decoded = decode_voxj_object(&encoded, &[]).unwrap();
234        assert!(decoded.positions.is_empty());
235        assert!(decoded.samples.is_empty());
236    }
237
238    #[test]
239    fn round_trips_zero_palette_object() {
240        for sample in SAMPLES {
241            let object = VoxjCodecObject {
242                name: "o".to_owned(),
243                palette_refs: Vec::new(),
244                bounds: [2, 1, 1],
245                positions: vec![[0, 0, 0], [1, 0, 0]],
246                samples: vec![Vec::new(), Vec::new()],
247            };
248            let encoded =
249                encode_voxj_object(&object, &[], PositionEncoding::BitmapBase64, sample).unwrap();
250            let decoded = decode_voxj_object(&encoded, &[]).unwrap();
251            assert_eq!(decoded.positions.len(), 2, "sample {sample:?}");
252            assert!(
253                decoded.samples.iter().all(Vec::is_empty),
254                "sample {sample:?}"
255            );
256        }
257    }
258
259    /// Two raw-json positions with a sample row that is too short for the
260    /// referenced palette count is malformed, not silently truncated.
261    #[test]
262    fn rejects_ragged_raw_json_samples() {
263        let object = VoxjSerdeObject {
264            name: "o".to_owned(),
265            palette_refs: vec![0],
266            bounds: [2, 1, 1],
267            voxel_positions: VoxjSerdePositionBlock::RawJson(vec![[0, 0, 0], [1, 0, 0]]),
268            voxel_samples: VoxjSerdeSampleBlock::RawJson(vec![vec![1], Vec::new()]),
269        };
270        assert!(decode_voxj_object(&object, &[4]).is_err());
271    }
272
273    /// A packed channel with fewer bytes than the voxel count and bit width
274    /// require is a truncated block, not zero-padded samples.
275    #[test]
276    fn rejects_truncated_packed_samples() {
277        let object = VoxjSerdeObject {
278            name: "o".to_owned(),
279            palette_refs: vec![0],
280            bounds: [2, 1, 1],
281            voxel_positions: VoxjSerdePositionBlock::RawJson(vec![[0, 0, 0], [1, 0, 0]]),
282            voxel_samples: VoxjSerdeSampleBlock::PackedBase64(vec![String::new()]),
283        };
284        assert!(decode_voxj_object(&object, &[4]).is_err());
285    }
286
287    /// A sample block carrying more channels than the object references palettes
288    /// is rejected rather than packing the extra channel at a guessed width.
289    #[test]
290    fn rejects_channel_count_mismatch() {
291        let object = VoxjSerdeObject {
292            name: "o".to_owned(),
293            palette_refs: vec![0],
294            bounds: [1, 1, 1],
295            voxel_positions: VoxjSerdePositionBlock::RawJson(vec![[0, 0, 0]]),
296            voxel_samples: VoxjSerdeSampleBlock::RleJson(vec![vec![0, 1], vec![0, 1]]),
297        };
298        assert!(decode_voxj_object(&object, &[4]).is_err());
299    }
300}