vox_format/
reader.rs

1//! Provides functions to read VOX files.
2
3use 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/// Error type returned when reading a VOX file fails.
39#[derive(Debug, Error)]
40pub enum Error {
41    /// The file signature is incorrect.
42    #[error("Expected file header to start with b'VOX ', but got: {got:?}.")]
43    InvalidMagic { got: [u8; 4] },
44
45    /// The file uses an unsupported version.
46    #[error("Unsupported file version: {version}")]
47    UnsupportedFileVersion { version: Version },
48
49    /// We expected to read the `MAIN` chunk, but instead read another chunk.
50    #[error("Expected MAIN chunk, but read chunk with ID: {0:?}", got.id())]
51    ExpectedMainChunk { got: Chunk },
52
53    /// `SIZE` and `XYZI` chunks must appear in pairs. This error is returned if
54    /// the number of `SIZE` chunks doesn't match the number of `XYZI` chunks.
55    #[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    /// Multiple `RGBA` chunks (color palette) were found.
62    #[error("Found multiple RGBA chunks (at {} and {}).", .chunks[0].offset(), chunks[1].offset())]
63    MultipleRgbaChunks { chunks: [Chunk; 2] },
64
65    /// Unknown material type.
66    #[error("Invalid material type: {material_type}")]
67    InvalidMaterial { material_type: u8 },
68
69    /// An error of the underlying IO
70    #[error("IO error")]
71    Io(#[from] std::io::Error),
72
73    /// An error while decoding strings to UTF-8.
74    #[error("Failed to decode UTF-8 string")]
75    Utf8(#[from] std::string::FromUtf8Error),
76}
77
78/// Reads a VOX file from the reader into the [`VoxBuffer`]. This function is
79/// useful, if you want to provide your own [`VoxBuffer`].
80///
81/// As an example, this `VoxBuffer` only counts the number of models in the
82/// file:
83///
84/// ```
85/// # use vox_format::{reader::read_vox_into, data::VoxBuffer, types::{Version, Size, Voxel, Palette}};
86/// # let mut vox_file = std::fs::File::open("../test_files/test_multiple_models.vox").unwrap();
87///
88/// #[derive(Default)]
89/// pub struct CountModels {
90///   num_models: usize
91/// }
92///
93/// impl VoxBuffer for CountModels {
94///   fn set_voxel(&mut self, _voxel: Voxel) {}
95///   fn set_palette(&mut self, _palette: Palette) {}
96///   fn set_num_models(&mut self, num_models: usize) {
97///     self.num_models += 1
98///   }
99/// }
100///
101/// let mut counter = CountModels::default();
102/// read_vox_into(vox_file, &mut counter).unwrap();
103/// println!("{}", counter.num_models);
104/// ```
105pub 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    //print_chunk(&main_chunk, &mut self.reader, 0)?;
114    log::trace!("main chunk: {:#?}", main_chunk);
115
116    //let mut pack_chunk = None;
117    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::Pack => {
130                log::debug!("read PACK chunk: {:?}", chunk);
131                if pack_chunk.is_some() {
132                    return Err(Error::MultiplePackChunks {
133                        chunks: [pack_chunk.take().unwrap(), chunk],
134                    });
135                }
136                pack_chunk = Some(chunk);
137            }*/
138            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::Note => {
149                let data = chunk.read_content_to_vec(&mut reader)?;
150                log::error!("{:#?}", data);
151                todo!();
152            },*/
153            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    /*
166    for chunk in &transform_chunks {
167        let transform = Transform::read(chunk.content(&mut reader)?)?;
168        log::debug!("{:#?}", transform);
169    }
170
171    for chunk in &group_chunks {
172        let group = Group::read(chunk.content(&mut reader)?)?;
173        log::debug!("{:#?}", group);
174    }
175
176    for chunk in &shape_chunks {
177        let shape = Shape::read(chunk.content(&mut reader)?)?;
178        log::debug!("{:#?}", shape);
179    }
180
181    for chunk in &layer_chunks {
182        let layer = Layer::read(chunk.content(&mut reader)?)?;
183        log::debug!("{:#?}", layer);
184    }
185    */
186
187    // Call `set_palette` first, so the trait impl has the palette data already when
188    // reading the voxels.
189    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    /*let num_models = pack_chunk
199        .map(|pack| Ok::<_, Error>(pack.content(&mut reader)?.read_u32::<LE>()? as usize))
200        .transpose()?
201        .unwrap_or(1);
202    log::trace!("num_models = {}", num_models);*/
203
204    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
234/// Reads a VOX file from a reader into [`crate::data::VoxData`].
235pub 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
241/// Reads a VOX file from a slice into [`crate::data::VoxData`].
242pub fn from_slice(slice: &[u8]) -> Result<VoxData, Error> {
243    from_reader(Cursor::new(slice))
244}
245
246/// Reads a VOX file from the specified path into [`crate::data::VoxData`].
247pub 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}