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
8pub 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
36fn invalid_data(message: String) -> Error {
38 Error::Invalid(message)
39}
40
41fn 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
52fn 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
82fn 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 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
149fn 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 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 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 #[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 #[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 #[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}