1use std::{
4 fs::File,
5 io::{
6 Cursor,
7 Read,
8 Seek,
9 },
10 path::Path,
11 str::from_utf8,
12};
13
14use byteorder::{
15 ReadBytesExt,
16 LE,
17};
18use thiserror::Error;
19
20use crate::{
21 chunk::{
22 read_main_chunk,
23 Chunk,
24 ChunkId,
25 },
26 data::{
27 VoxBuffer,
28 VoxData,
29 },
30 types::{
31 Palette,
32 Size,
33 Version,
34 Voxel,
35 },
36};
37
38#[derive(Debug, Error)]
40pub enum Error {
41 #[error("Expected file header to start with b'VOX ', but got: {got:?}.")]
43 InvalidMagic { got: [u8; 4] },
44
45 #[error("Unsupported file version: {version}")]
47 UnsupportedFileVersion { version: Version },
48
49 #[error("Expected MAIN chunk, but read chunk with ID: {0:?}", got.id())]
51 ExpectedMainChunk { got: Chunk },
52
53 #[error("Found {} SIZE chunks, {} XYZI chunks.", .size_chunks.len(), .xyzi_chunks.len())]
56 InvalidNumberOfSizeAndXyziChunks {
57 size_chunks: Vec<Chunk>,
58 xyzi_chunks: Vec<Chunk>,
59 },
60
61 #[error("Found multiple RGBA chunks (at {} and {}).", .chunks[0].offset(), chunks[1].offset())]
63 MultipleRgbaChunks { chunks: [Chunk; 2] },
64
65 #[error("Invalid material type: {material_type}")]
67 InvalidMaterial { material_type: u8 },
68
69 #[error("IO error")]
71 Io(#[from] std::io::Error),
72
73 #[error("Failed to decode UTF-8 string")]
75 Utf8(#[from] std::string::FromUtf8Error),
76}
77
78pub fn read_vox_into<R: Read + Seek, B: VoxBuffer>(
106 mut reader: R,
107 buffer: &mut B,
108) -> Result<(), Error> {
109 let (main_chunk, version) = read_main_chunk(&mut reader)?;
110
111 buffer.set_version(version);
112
113 log::trace!("main chunk: {:#?}", main_chunk);
115
116 let mut size_chunks = vec![];
118 let mut xyzi_chunks = vec![];
119 let mut rgba_chunk = None;
120 let mut transform_chunks = vec![];
121 let mut group_chunks = vec![];
122 let mut shape_chunks = vec![];
123 let mut layer_chunks = vec![];
124
125 for r in main_chunk.children(&mut reader) {
126 let chunk = r?;
127
128 match chunk.id() {
129 ChunkId::Size => size_chunks.push(chunk),
139 ChunkId::Xyzi => xyzi_chunks.push(chunk),
140 ChunkId::Rgba => {
141 if rgba_chunk.is_some() {
142 return Err(Error::MultipleRgbaChunks {
143 chunks: [rgba_chunk.take().unwrap(), chunk],
144 });
145 }
146 rgba_chunk = Some(chunk);
147 }
148 ChunkId::NTrn => transform_chunks.push(chunk),
154 ChunkId::NGrp => group_chunks.push(chunk),
155 ChunkId::NShp => shape_chunks.push(chunk),
156 ChunkId::Layr => layer_chunks.push(chunk),
157 ChunkId::Unsupported(raw) => {
158 let str_opt = from_utf8(&raw).ok();
159 log::debug!("Skipping unsupported chunk: {:?} ({:?})", raw, str_opt);
160 }
161 id => log::trace!("Skipping unimplemented chunk: {:?}", id),
162 }
163 }
164
165 if let Some(rgba_chunk) = rgba_chunk {
190 log::trace!("read RGBA chunk");
191 let palette = Palette::read(rgba_chunk.content(&mut reader)?)?;
192 buffer.set_palette(palette);
193 }
194 else {
195 log::trace!("no RGBA chunk found");
196 }
197
198 if xyzi_chunks.len() != size_chunks.len() {
205 return Err(Error::InvalidNumberOfSizeAndXyziChunks {
206 size_chunks,
207 xyzi_chunks,
208 });
209 }
210 let num_models = size_chunks.len();
211 log::trace!("num_models = {}", num_models);
212 buffer.set_num_models(num_models);
213
214 for (size_chunk, xyzi_chunk) in size_chunks.into_iter().zip(xyzi_chunks) {
215 let model_size = Size::read(size_chunk.content(&mut reader)?)?;
216 log::trace!("model_size = {:?}", model_size);
217 buffer.set_model_size(model_size);
218
219 let mut reader = xyzi_chunk.content(&mut reader)?;
220
221 let num_voxels = reader.read_u32::<LE>()?;
222 log::trace!("num_voxels = {}", num_voxels);
223
224 for _ in 0..num_voxels {
225 let voxel = Voxel::read(&mut reader)?;
226 log::trace!("voxel = {:?}", voxel);
227 buffer.set_voxel(voxel);
228 }
229 }
230
231 Ok(())
232}
233
234pub fn from_reader<R: Read + Seek>(reader: R) -> Result<VoxData, Error> {
236 let mut buffer = VoxData::default();
237 read_vox_into(reader, &mut buffer)?;
238 Ok(buffer)
239}
240
241pub fn from_slice(slice: &[u8]) -> Result<VoxData, Error> {
243 from_reader(Cursor::new(slice))
244}
245
246pub fn from_file<P: AsRef<Path>>(path: P) -> Result<VoxData, Error> {
248 from_reader(File::open(path)?)
249}
250
251#[cfg(test)]
252mod tests {
253 use std::collections::HashMap;
254
255 use super::from_slice;
256 use crate::types::{
257 Color,
258 ColorIndex,
259 Model,
260 Point,
261 Vector,
262 Voxel,
263 };
264
265 fn glider() -> Vec<Voxel> {
266 vec![
267 Voxel::new([0, 0, 1], 79),
268 Voxel::new([1, 0, 0], 79),
269 Voxel::new([2, 0, 0], 79),
270 Voxel::new([2, 0, 1], 69),
271 Voxel::new([2, 0, 2], 69),
272 ]
273 }
274
275 fn glider2() -> Vec<Voxel> {
276 vec![
277 Voxel::new([0, 2, 0], 79),
278 Voxel::new([1, 1, 0], 79),
279 Voxel::new([2, 1, 0], 79),
280 Voxel::new([1, 0, 0], 79),
281 Voxel::new([0, 0, 0], 79),
282 ]
283 }
284
285 fn assert_voxels(model: &Model, expected: &[Voxel]) {
286 let voxels = model
287 .voxels
288 .iter()
289 .map(|voxel| (voxel.point, voxel.color_index))
290 .collect::<HashMap<Point, ColorIndex>>();
291
292 for expected_voxel in expected {
293 let voxel = voxels.get(&Vector::from(expected_voxel.point)).copied();
294 assert_eq!(
295 voxel,
296 Some(expected_voxel.color_index),
297 "Expected right at {:?}",
298 expected_voxel.point
299 );
300 }
301 }
302
303 #[test]
304 fn it_reads_files_without_models() {
305 let vox = from_slice(include_bytes!(concat!(
306 env!("CARGO_MANIFEST_DIR"),
307 "/../test_files/test_no_models.vox"
308 )))
309 .unwrap();
310 assert!(vox.models.is_empty());
311 }
312
313 #[test]
314 fn it_reads_a_single_model() {
315 let vox = from_slice(include_bytes!(concat!(
316 env!("CARGO_MANIFEST_DIR"),
317 "/../test_files/test_single_model_default_palette.vox"
318 )))
319 .unwrap();
320
321 assert_eq!(vox.models.len(), 1);
322 assert!(vox.palette.is_default());
323
324 let model = &vox.models[0];
325 assert_eq!(model.size, Vector::new(3, 1, 3));
326 assert_voxels(model, &glider());
327 }
328
329 #[test]
330 fn it_reads_multiple_models() {
331 let vox = from_slice(include_bytes!(concat!(
332 env!("CARGO_MANIFEST_DIR"),
333 "/../test_files/test_multiple_models.vox"
334 )))
335 .unwrap();
336
337 assert_eq!(vox.models.len(), 2);
338
339 let model2 = &vox.models[0];
340 assert_eq!(model2.size, Vector::new(3, 3, 1));
341 assert_voxels(model2, &glider2());
342
343 let model1 = &vox.models[1];
344 assert_eq!(model1.size, Vector::new(3, 1, 3));
345 assert_voxels(model1, &glider());
346 }
347
348 #[test]
349 fn it_reads_a_custom_palette() {
350 let vox = from_slice(include_bytes!(concat!(
351 env!("CARGO_MANIFEST_DIR"),
352 "/../test_files/test_custom_palette.vox"
353 )))
354 .unwrap();
355
356 assert!(!vox.palette.is_default());
357 assert_eq!(vox.palette[79.into()], Color::light_blue());
358 assert_eq!(vox.palette[69.into()], Color::new(108, 0, 204, 255));
359 }
360
361 #[test]
362 fn color_indices_work_as_expected() {
363 let vox = from_slice(include_bytes!(concat!(
364 env!("CARGO_MANIFEST_DIR"),
365 "/../test_files/test_single_model_default_palette.vox"
366 )))
367 .unwrap();
368
369 let color_index = vox
370 .models
371 .get(0)
372 .unwrap()
373 .voxels
374 .first()
375 .unwrap()
376 .color_index;
377 assert_eq!(vox.palette[color_index], Color::light_blue());
378 }
379}