1use std::{
2 borrow::Cow,
3 error::Error,
4 fmt::Display,
5 io::{self, Cursor, Seek, SeekFrom},
6 str::{self, Utf8Error},
7};
8
9use byteorder::{LittleEndian, ReadBytesExt};
10
11use crate::{PaletteKind, SpriteId, Version};
12
13#[derive(Debug, Clone)]
14pub struct Decoder<'a> {
15 version: Version,
16 groups_count: u32,
17 images_count: u32,
18 palette_kind: PaletteKind,
19 comments: Comments<'a>,
20 sprites: Vec<Sprite<'a>>,
21}
22
23#[derive(Debug)]
24pub enum DecodeError {
25 InvalidData(io::Error),
26 InvalidSignature,
27 UnsuporttedVersion(Version),
28 InvalidPaletteKind,
29 PreviousPaletteNotFound,
30 LinkedSpriteNotFound {
31 sprite_id: SpriteId,
32 linked_index: u16,
33 },
34}
35
36impl Display for DecodeError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 DecodeError::InvalidData(err) => err.fmt(f),
40 DecodeError::InvalidSignature => write!(f, "invalid signature"),
41 DecodeError::UnsuporttedVersion(v) => write!(f, "unsupported version {:?}", v),
42 DecodeError::InvalidPaletteKind => write!(f, "invalid palette kind"),
43 DecodeError::PreviousPaletteNotFound => write!(f, "previous palette not found"),
44 DecodeError::LinkedSpriteNotFound {
45 sprite_id,
46 linked_index,
47 } => write!(
48 f,
49 "invalid link {} for sprite {}-{}",
50 linked_index, sprite_id.group, sprite_id.image
51 ),
52 }
53 }
54}
55
56impl Error for DecodeError {
57 fn source(&self) -> Option<&(dyn Error + 'static)> {
58 match self {
59 DecodeError::InvalidData(ref err) => Some(err),
60 _ => None,
61 }
62 }
63}
64
65impl From<io::Error> for DecodeError {
66 fn from(error: io::Error) -> Self {
67 DecodeError::InvalidData(error)
68 }
69}
70
71impl<'a> Decoder<'a> {
72 pub fn decode(data: &'a [u8]) -> Result<Self, DecodeError> {
73 if &data[0..12] != b"ElecbyteSpr\0" {
74 return Err(DecodeError::InvalidSignature);
75 }
76
77 let mut bytes = Cursor::new(&data);
78 bytes.set_position(12);
79
80 let version_low3 = bytes.read_u8()?;
81 let version_low2 = bytes.read_u8()?;
82 let version_low1 = bytes.read_u8()?;
83 let version_high = bytes.read_u8()?;
84 let version = Version(version_high, version_low1, version_low2, version_low3);
85 if version_high != 1 {
86 return Err(DecodeError::UnsuporttedVersion(version));
87 }
88
89 let groups_count = bytes.read_u32::<LittleEndian>()?;
90 let images_count = bytes.read_u32::<LittleEndian>()?;
91 let first_subfile_offset = bytes.read_u32::<LittleEndian>()?;
92 let subfile_header_size = bytes.read_u32::<LittleEndian>()?;
93 let palette_kind = match bytes.read_u8()? {
94 0 => PaletteKind::Individual,
95 1 => PaletteKind::Shared,
96 _ => return Err(DecodeError::InvalidPaletteKind),
97 };
98
99 let comments = Comments(&data[36..511]);
100
101 let mut images: Vec<Sprite> = Vec::with_capacity(images_count as usize);
102 let mut next_subfile_offset = Some(first_subfile_offset);
103 let mut previous_palette_offset = None;
104 let total_size = data.len() as u32;
105 while let Some(offset) = next_subfile_offset.take() {
106 bytes.set_position(offset.into());
107
108 next_subfile_offset = bytes.read_u32::<LittleEndian>().ok().and_then(|n| match n {
109 n if n > 0 && n + subfile_header_size <= total_size => Some(n),
110 _ => None,
111 });
112
113 let size = bytes.read_u32::<LittleEndian>()? as usize;
114 let x = bytes.read_i16::<LittleEndian>()?;
115 let y = bytes.read_i16::<LittleEndian>()?;
116 let group = bytes.read_u16::<LittleEndian>()?;
117 let image = bytes.read_u16::<LittleEndian>()?;
118 let linked_index = bytes.read_u16::<LittleEndian>()?;
119 let use_previous_palette = bytes.read_u8()? != 0;
120
121 bytes.seek(SeekFrom::Current(13))?;
122
123 let sprite_id = SpriteId { group, image };
124 let coordinates = Coordinates { x, y };
125
126 let data_offset = bytes.position() as usize;
127 let palette_size = 256 * 3;
128
129 let (data_size, palette_offset) = match previous_palette_offset {
130 None if use_previous_palette => return Err(DecodeError::PreviousPaletteNotFound),
131 Some(offset) if use_previous_palette => (size, offset),
132 _ => {
133 let offset = (data_offset + size) - palette_size;
134 previous_palette_offset = Some(offset);
135 (size - palette_size, offset)
136 }
137 };
138
139 let sprite = if size == 0 {
140 let linked_sprite =
141 images
142 .get(linked_index as usize)
143 .ok_or(DecodeError::LinkedSpriteNotFound {
144 sprite_id,
145 linked_index,
146 })?;
147
148 Sprite {
149 id: sprite_id,
150 coordinates,
151 ..linked_sprite.clone()
152 }
153 } else {
154 Sprite {
155 id: sprite_id,
156 data: Cow::Borrowed(&data[data_offset..data_offset + data_size]),
157 palette: Cow::Borrowed(&data[palette_offset..palette_offset + palette_size]),
158 coordinates,
159 }
160 };
161
162 images.push(sprite);
163 }
164
165 Ok(Decoder {
166 version,
167 groups_count,
168 images_count,
169 palette_kind,
170 comments,
171 sprites: images,
172 })
173 }
174
175 pub fn comments(&self) -> &Comments {
176 &self.comments
177 }
178
179 pub fn palette_kind(&self) -> PaletteKind {
180 self.palette_kind
181 }
182
183 pub fn version(&self) -> Version {
184 self.version
185 }
186
187 pub fn groups_count(&self) -> u32 {
188 self.groups_count
189 }
190
191 pub fn images_count(&self) -> u32 {
192 self.images_count
193 }
194
195 pub fn sprites(&self) -> impl Iterator<Item = &Sprite> {
196 self.sprites.iter()
197 }
198}
199
200impl<'a> IntoIterator for Decoder<'a> {
201 type Item = Sprite<'a>;
202 type IntoIter = std::vec::IntoIter<Self::Item>;
203
204 fn into_iter(self) -> Self::IntoIter {
205 self.sprites.into_iter()
206 }
207}
208
209#[derive(Debug, Clone, Copy, Eq, PartialEq)]
210pub struct Coordinates {
211 pub x: i16,
212 pub y: i16,
213}
214
215impl From<(i16, i16)> for Coordinates {
216 fn from((x, y): (i16, i16)) -> Self {
217 Self { x, y }
218 }
219}
220
221impl From<Coordinates> for (i16, i16) {
222 fn from(Coordinates { x, y }: Coordinates) -> Self {
223 (x, y)
224 }
225}
226
227#[derive(Debug, Clone)]
228pub struct Sprite<'a> {
229 id: SpriteId,
230 data: Cow<'a, [u8]>,
231 palette: Cow<'a, [u8]>,
232 coordinates: Coordinates,
233}
234
235impl<'a> Sprite<'a> {
236 pub fn id(&self) -> SpriteId {
237 self.id
238 }
239
240 pub fn coordinates(&self) -> Coordinates {
241 self.coordinates
242 }
243
244 pub fn raw_data(&self) -> &[u8] {
245 &self.data
246 }
247
248 pub fn palette(&self) -> &[u8] {
249 &self.palette
250 }
251
252 pub fn to_owned(&self) -> Sprite<'static> {
253 Sprite {
254 id: self.id,
255 data: Cow::Owned(self.raw_data().to_owned()),
256 palette: Cow::Owned(self.palette().to_owned()),
257 coordinates: self.coordinates,
258 }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct Comments<'a>(&'a [u8]);
264
265impl<'a> Comments<'a> {
266 pub fn as_str(&self) -> Result<&str, Utf8Error> {
267 Ok(str::from_utf8(self.0)?.trim_end_matches('\u{0}'))
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use crate::{decoder::Decoder, PaletteKind, Version};
274
275 #[test]
276 fn decode_headers() {
277 let sff = include_bytes!("../tests/samples/sample/sample.sff");
278 let sff = Decoder::decode(sff).unwrap();
279
280 assert_eq!(Version(1, 0, 1, 0), sff.version());
281 assert_eq!(4, sff.groups_count());
282 assert_eq!(8, sff.images_count());
283 assert_eq!(PaletteKind::Shared, sff.palette_kind());
284 assert_eq!(Ok("Some comment"), sff.comments().as_str());
285 }
286
287 #[test]
288 fn decode_sprites() {
289 let sff = include_bytes!("../tests/samples/sample/sample.sff");
290 let sff = Decoder::decode(sff).unwrap();
291
292 let sprites = sff.sprites().collect::<Vec<_>>();
293
294 assert_eq!(8, sprites.len());
295
296 assert_eq!(sprites[0].id, (0, 0).into());
297 assert_eq!(sprites[0].coordinates, (0, 0).into());
298 assert_eq!(
299 [sprites[0].raw_data(), sprites[0].palette()].concat(),
300 include_bytes!("../tests/samples/sample/sample-0-0.pcx")
301 );
302
303 assert_eq!(sprites[1].id, (0, 1).into());
304 assert_eq!(sprites[1].coordinates, (-50, -50).into());
305 assert_eq!(
306 [sprites[1].raw_data(), sprites[1].palette()].concat(),
307 include_bytes!("../tests/samples/sample/sample-0-1.pcx")
308 );
309
310 assert_eq!(sprites[2].id, (1, 0).into());
311 assert_eq!(sprites[2].coordinates, (-50, -50).into());
312 assert_eq!(
313 [sprites[2].raw_data(), sprites[2].palette()].concat(),
314 include_bytes!("../tests/samples/sample/sample-1-0.pcx")
315 );
316
317 assert_eq!(sprites[3].id, (1, 1).into());
318 assert_eq!(sprites[3].coordinates, (0, 0).into());
319 assert_eq!(
320 [sprites[3].raw_data(), sprites[3].palette()].concat(),
321 include_bytes!("../tests/samples/sample/sample-1-1.pcx")
322 );
323
324 assert_eq!(sprites[4].id, (10, 10).into());
325 assert_eq!(sprites[4].coordinates, (0, 0).into());
326 assert_eq!(
327 [sprites[4].raw_data(), sprites[4].palette()].concat(),
328 include_bytes!("../tests/samples/sample/sample-10-10.pcx")
329 );
330
331 assert_eq!(sprites[5].id, (10, 20).into());
332 assert_eq!(sprites[5].coordinates, (-100, -100).into());
333 assert_eq!(
334 [sprites[5].raw_data(), sprites[5].palette()].concat(),
335 include_bytes!("../tests/samples/sample/sample-10-20.pcx")
336 );
337
338 assert_eq!(sprites[6].id, (50, 0).into());
339 assert_eq!(sprites[6].coordinates, (-200, -200).into());
340 assert_eq!(
341 [sprites[6].raw_data(), sprites[6].palette()].concat(),
342 include_bytes!("../tests/samples/sample/sample-50-0.pcx")
343 );
344
345 assert_eq!(sprites[7].id, (50, 5).into());
346 assert_eq!(sprites[7].coordinates, (-200, -200).into());
347 assert_eq!(
348 [sprites[7].raw_data(), sprites[7].palette()].concat(),
349 include_bytes!("../tests/samples/sample/sample-50-5.pcx")
350 );
351 }
352
353 #[test]
354 fn decode_linked_sprites() {
355 let sff = include_bytes!("../tests/samples/sample/linked.sff");
356 let sff = Decoder::decode(sff).unwrap();
357
358 let sprites = sff.sprites().collect::<Vec<_>>();
359
360 assert_eq!(8, sprites.len());
361
362 assert_eq!(sprites[0].id, (0, 0).into());
363 assert_eq!(sprites[0].coordinates, (0, 0).into());
364 assert_eq!(
365 [sprites[0].raw_data(), sprites[0].palette()].concat(),
366 include_bytes!("../tests/samples/sample/linked-0.pcx")
367 );
368
369 assert_eq!(sprites[1].id, (0, 1).into());
370 assert_eq!(sprites[1].coordinates, (-50, -50).into());
371 assert_eq!(
372 [sprites[1].raw_data(), sprites[1].palette()].concat(),
373 include_bytes!("../tests/samples/sample/linked-0.pcx")
374 );
375
376 assert_eq!(sprites[2].id, (1, 0).into());
377 assert_eq!(sprites[2].coordinates, (-50, -50).into());
378 assert_eq!(
379 [sprites[2].raw_data(), sprites[2].palette()].concat(),
380 include_bytes!("../tests/samples/sample/linked-0.pcx")
381 );
382
383 assert_eq!(sprites[3].id, (1, 1).into());
384 assert_eq!(sprites[3].coordinates, (0, 0).into());
385 assert_eq!(
386 [sprites[3].raw_data(), sprites[3].palette()].concat(),
387 include_bytes!("../tests/samples/sample/linked-0.pcx")
388 );
389
390 assert_eq!(sprites[4].id, (10, 10).into());
391 assert_eq!(sprites[4].coordinates, (0, 0).into());
392 assert_eq!(
393 [sprites[4].raw_data(), sprites[4].palette()].concat(),
394 include_bytes!("../tests/samples/sample/linked-1.pcx")
395 );
396
397 assert_eq!(sprites[5].id, (10, 20).into());
398 assert_eq!(sprites[5].coordinates, (-100, -100).into());
399 assert_eq!(
400 [sprites[5].raw_data(), sprites[5].palette()].concat(),
401 include_bytes!("../tests/samples/sample/linked-1.pcx")
402 );
403
404 assert_eq!(sprites[6].id, (50, 0).into());
405 assert_eq!(sprites[6].coordinates, (-200, -200).into());
406 assert_eq!(
407 [sprites[6].raw_data(), sprites[6].palette()].concat(),
408 include_bytes!("../tests/samples/sample/linked-2.pcx")
409 );
410
411 assert_eq!(sprites[7].id, (50, 5).into());
412 assert_eq!(sprites[7].coordinates, (-200, -200).into());
413 assert_eq!(
414 [sprites[7].raw_data(), sprites[7].palette()].concat(),
415 include_bytes!("../tests/samples/sample/linked-2.pcx")
416 );
417 }
418
419 #[test]
420 fn sprite_can_be_owned() {
421 let sff = include_bytes!("../tests/samples/sample/sample.sff");
422 let sff = Decoder::decode(sff).unwrap();
423
424 let sprite = sff.sprites().next().unwrap();
425 let sprite = sprite.to_owned();
426
427 drop(sff);
428
429 assert_eq!(
430 [sprite.raw_data(), sprite.palette()].concat(),
431 include_bytes!("../tests/samples/sample/sample-0-0.pcx")
432 );
433 }
434}