aseprite_loader/loader/
mod.rs

1//! This module contains the actual loader API. This API is based on the
2//! `binary`-module of this crate and does provide a convenience API for
3//! accessing layers, frames, tags and fully blended images.
4
5use flate2::Decompress;
6use std::{
7    collections::hash_map::DefaultHasher,
8    collections::HashMap,
9    hash::{Hash, Hasher},
10    ops::RangeInclusive,
11};
12
13mod blend;
14
15use crate::{
16    binary::{
17        blend_mode::BlendMode,
18        chunks::{
19            cel::CelContent,
20            layer::{LayerFlags, LayerType},
21            slice::SliceChunk,
22            tags::AnimationDirection,
23        },
24        color_depth::ColorDepth,
25        file::{parse_file, File},
26        image::Image,
27        palette::Palette,
28    },
29    loader::blend::{blend_mode_to_blend_fn, Color},
30};
31
32/// This can be used to load an Aseprite file.
33#[derive(Debug)]
34pub struct AsepriteFile<'a> {
35    pub file: File<'a>,
36    /// All layers in the file in order
37    pub layers: Vec<Layer>,
38    /// All frames in the file in order
39    pub frames: Vec<Frame>,
40    /// All tags in the file
41    pub tags: Vec<Tag>,
42    /// All images in the file
43    pub images: Vec<Image<'a>>,
44}
45
46/// A cel in a frame
47///
48/// This is a reference to an image cel
49#[derive(Debug, Copy, Clone)]
50pub struct FrameCel {
51    pub origin: (i16, i16),
52    pub size: (u16, u16),
53    pub layer_index: usize,
54    pub image_index: usize,
55}
56
57/// A frame in the file
58///
59/// This is a collection of cels for each layer
60#[derive(Debug, Clone)]
61pub struct Frame {
62    pub duration: u16,
63    pub origin: (i16, i16),
64    pub cels: Vec<FrameCel>,
65}
66
67/// A tag in the file
68///
69/// This is a range of frames over the frames in the file, ordered by frame index
70#[derive(Debug, Clone)]
71pub struct Tag {
72    pub name: String,
73    pub range: RangeInclusive<u16>,
74    pub direction: AnimationDirection,
75    pub repeat: Option<u16>,
76}
77
78/// A layer in the file
79#[derive(Debug, Clone)]
80pub struct Layer {
81    pub name: String,
82    pub opacity: u8,
83    pub blend_mode: BlendMode,
84    pub visible: bool,
85}
86
87impl AsepriteFile<'_> {
88    /// Load a aseprite file from a byte slice
89    pub fn load(data: &[u8]) -> Result<AsepriteFile<'_>, LoadSpriteError> {
90        let file = parse_file(data).map_err(|e| LoadSpriteError::Parse {
91            message: e.to_string(),
92        })?;
93        let layers: Vec<_> = file
94            .layers
95            .iter()
96            .filter_map(|layer| {
97                if layer.layer_type == LayerType::Normal {
98                    Some(Layer {
99                        name: layer.name.to_string(),
100                        opacity: layer.opacity,
101                        blend_mode: layer.blend_mode,
102                        visible: layer.flags.contains(LayerFlags::VISIBLE),
103                    })
104                } else {
105                    None
106                }
107            })
108            .collect();
109
110        let mut image_vec: Vec<Image<'_>> = Vec::new();
111        let mut image_map: HashMap<(usize, usize), usize> = HashMap::new();
112
113        for (frame_index, frame) in file.frames.iter().enumerate() {
114            for cel in frame.cels.iter().filter_map(|x| x.as_ref()) {
115                if let CelContent::Image(image) = &cel.content {
116                    let image_index = image_vec.len();
117                    image_vec.push(image.clone());
118                    let _ = image_map.insert((frame_index, cel.layer_index.into()), image_index);
119                }
120            }
121        }
122
123        let mut frames: Vec<Frame> = Vec::new();
124        let mut tags: Vec<Tag> = Vec::new();
125
126        for tag in file.tags.iter() {
127            tags.push(Tag {
128                name: tag.name.to_string(),
129                range: tag.frames.clone(),
130                direction: tag.animation_direction,
131                repeat: if tag.animation_repeat > 0 {
132                    Some(tag.animation_repeat)
133                } else {
134                    None
135                },
136            });
137        }
138
139        for (index, frame) in file.frames.iter().enumerate() {
140            let mut cels: Vec<FrameCel> = Vec::new();
141            for cel in frame.cels.iter().filter_map(|x| x.as_ref()) {
142                let image_index = match cel.content {
143                    CelContent::Image(_) => image_map[&(index, cel.layer_index.into())],
144                    CelContent::LinkedCel { frame_position } => *image_map
145                        .get(&(frame_position.into(), cel.layer_index.into()))
146                        .ok_or_else(|| LoadSpriteError::Parse {
147                            message: format!(
148                                "invalid linked cel at frame {} layer {}",
149                                index, cel.layer_index
150                            ),
151                        })?,
152                    _ => {
153                        return Err(LoadSpriteError::Parse {
154                            message: "invalid cel".to_owned(),
155                        })
156                    }
157                };
158                let width = image_vec[image_index].width;
159                let height = image_vec[image_index].height;
160                cels.push(FrameCel {
161                    origin: (cel.x, cel.y),
162                    size: (width, height),
163                    layer_index: cel.layer_index.into(),
164                    image_index,
165                });
166            }
167
168            frames.push(Frame {
169                duration: frame.duration,
170                origin: (0, 0),
171                cels,
172            });
173        }
174
175        Ok(AsepriteFile {
176            file,
177            tags,
178            layers,
179            frames,
180            images: image_vec,
181        })
182    }
183    /// Get size of the sprite (width, height)
184    pub fn size(&self) -> (u16, u16) {
185        (self.file.header.width, self.file.header.height)
186    }
187    /// Get tag names
188    pub fn tags(&self) -> &[Tag] {
189        &self.tags
190    }
191    /// Get layer names
192    pub fn layers(&self) -> &[Layer] {
193        &self.layers
194    }
195    /// Get the image indices for a given tag and layer
196    pub fn frames(&self) -> &[Frame] {
197        &self.frames
198    }
199    /// Get image count
200    pub fn image_count(&self) -> usize {
201        self.images.len()
202    }
203
204    /// Get image loader for a given frame index
205    /// This will combine all layers into a single image
206    /// returns a hash describing the image, since cels can be reused in multiple frames
207    pub fn combined_frame_image(
208        &self,
209        frame_index: usize,
210        target: &mut [u8],
211    ) -> Result<u64, LoadImageError> {
212        let mut hasher = DefaultHasher::new();
213
214        let target_size =
215            usize::from(self.file.header.width) * usize::from(self.file.header.height) * 4;
216
217        if target.len() < target_size {
218            return Err(LoadImageError::TargetBufferTooSmall);
219        }
220
221        let frame = &self.frames[frame_index];
222
223        for cel in frame.cels.iter() {
224            let layer = &self.layers[cel.layer_index];
225            if !layer.visible {
226                continue;
227            }
228
229            let mut cel_target = vec![0; usize::from(cel.size.0) * usize::from(cel.size.1) * 4];
230            self.load_image(cel.image_index, &mut cel_target).unwrap();
231            let layer = &self.layers[cel.layer_index];
232
233            (cel.image_index, cel.layer_index, cel.origin, cel.size).hash(&mut hasher);
234
235            let blend_fn = blend_mode_to_blend_fn(layer.blend_mode);
236
237            for y in 0..cel.size.1 {
238                for x in 0..cel.size.0 {
239                    let Some(target_x) = x.checked_add_signed(cel.origin.0) else {
240                        continue;
241                    };
242                    if target_x >= self.file.header.width {
243                        continue;
244                    }
245                    let Some(target_y) = y.checked_add_signed(cel.origin.1) else {
246                        continue;
247                    };
248                    if target_y >= self.file.header.height {
249                        continue;
250                    }
251
252                    let target_index = usize::from(target_y) * usize::from(self.file.header.width)
253                        + usize::from(target_x);
254                    let cel_index = usize::from(y) * usize::from(cel.size.0) + usize::from(x);
255
256                    let cel_pixel: &[u8] = &cel_target[cel_index * 4..cel_index * 4 + 4];
257                    let target_pixel: &mut [u8] =
258                        &mut target[target_index * 4..target_index * 4 + 4];
259
260                    let back = Color::from(&*target_pixel);
261                    let front = Color::from(cel_pixel);
262                    let out = blend_fn(back, front, layer.opacity);
263
264                    target_pixel[0] = out.r;
265                    target_pixel[1] = out.g;
266                    target_pixel[2] = out.b;
267                    target_pixel[3] = out.a;
268                }
269            }
270        }
271
272        Ok(hasher.finish())
273    }
274
275    /// Get image loader for a given image index
276    pub fn load_image(&self, index: usize, target: &mut [u8]) -> Result<(), LoadImageError> {
277        let image = &self.images[index];
278        let target_size = usize::from(image.width) * usize::from(image.height) * 4;
279        if target.len() < target_size {
280            return Err(LoadImageError::TargetBufferTooSmall);
281        }
282        let target = &mut target[..target_size];
283        match (self.file.header.color_depth, image.compressed) {
284            (ColorDepth::Rgba, false) => target.copy_from_slice(image.data),
285            (ColorDepth::Rgba, true) => decompress(image.data, target)?,
286            (ColorDepth::Grayscale, false) => {
287                grayscale_to_rgba(image.data, target)?;
288            }
289            (ColorDepth::Grayscale, true) => {
290                let mut buf = vec![0u8; usize::from(image.width) * usize::from(image.height) * 2];
291                decompress(image.data, &mut buf)?;
292                grayscale_to_rgba(&buf, target)?;
293            }
294            (ColorDepth::Indexed, false) => {
295                indexed_to_rgba(
296                    image.data,
297                    self.file
298                        .palette
299                        .as_ref()
300                        .ok_or(LoadImageError::MissingPalette)?,
301                    target,
302                )?;
303            }
304            (ColorDepth::Indexed, true) => {
305                let mut buf = vec![0u8; usize::from(image.width) * usize::from(image.height)];
306                decompress(image.data, &mut buf)?;
307                indexed_to_rgba(
308                    &buf,
309                    self.file
310                        .palette
311                        .as_ref()
312                        .ok_or(LoadImageError::MissingPalette)?,
313                    target,
314                )?;
315            }
316            (ColorDepth::Unknown(_), _) => return Err(LoadImageError::UnsupportedColorDepth),
317        }
318        Ok(())
319    }
320    pub fn slices(&self) -> &[SliceChunk<'_>] {
321        &self.file.slices
322    }
323}
324
325use thiserror::Error;
326
327#[derive(Error, Debug)]
328pub enum LoadSpriteError {
329    #[error("parsing failed {message}")]
330    Parse { message: String },
331    #[error("missing tag: {0}")]
332    MissingTag(String),
333    #[error("missing layer: {0}")]
334    MissingLayer(String),
335    #[error("frame index out of range: {0}")]
336    FrameIndexOutOfRange(usize),
337}
338
339#[allow(missing_copy_implementations)]
340#[derive(Error, Debug)]
341pub enum LoadImageError {
342    #[error("target buffer too small")]
343    TargetBufferTooSmall,
344    #[error("missing palette")]
345    MissingPalette,
346    #[error("unsupported color depth")]
347    UnsupportedColorDepth,
348    #[error("decompression failed")]
349    DecompressError,
350    #[error("invalid image data")]
351    InvalidImageData,
352}
353
354pub fn decompress(data: &[u8], target: &mut [u8]) -> Result<(), LoadImageError> {
355    let mut decompressor = Decompress::new(true);
356    match decompressor.decompress(data, target, flate2::FlushDecompress::Finish) {
357        Ok(flate2::Status::Ok | flate2::Status::BufError) => Err(LoadImageError::DecompressError),
358        Ok(flate2::Status::StreamEnd) => Ok(()),
359        Err(_) => Err(LoadImageError::DecompressError),
360    }
361}
362
363fn grayscale_to_rgba(source: &[u8], target: &mut [u8]) -> Result<(), LoadImageError> {
364    if target.len() != source.len() * 2 {
365        return Err(LoadImageError::InvalidImageData);
366    }
367    for (i, chunk) in source.chunks(2).enumerate() {
368        target[i * 4] = chunk[0];
369        target[i * 4 + 1] = chunk[0];
370        target[i * 4 + 2] = chunk[0];
371        target[i * 4 + 3] = chunk[1];
372    }
373    Ok(())
374}
375
376fn indexed_to_rgba(
377    source: &[u8],
378    palette: &Palette,
379    target: &mut [u8],
380) -> Result<(), LoadImageError> {
381    if target.len() != source.len() * 4 {
382        return Err(LoadImageError::InvalidImageData);
383    }
384    for (i, px) in source.iter().enumerate() {
385        let color = palette.colors[usize::from(*px)];
386        target[i * 4] = color.red;
387        target[i * 4 + 1] = color.green;
388        target[i * 4 + 2] = color.blue;
389        target[i * 4 + 3] = color.alpha;
390    }
391    Ok(())
392}
393
394#[test]
395fn test_cel() {
396    use image::RgbaImage;
397    use tempfile::TempDir;
398
399    let path = "./tests/combine.aseprite";
400    let file = std::fs::read(path).unwrap();
401    let file = AsepriteFile::load(&file).unwrap();
402
403    for frame in file.frames().iter() {
404        for (i, cel) in frame.cels.iter().enumerate() {
405            let (width, height) = cel.size;
406
407            let mut target = vec![0; usize::from(width * height) * 4];
408            file.load_image(cel.image_index, &mut target).unwrap();
409
410            let image = RgbaImage::from_raw(u32::from(width), u32::from(height), target).unwrap();
411
412            let tmp = TempDir::with_prefix("aseprite-loader").unwrap();
413            let path = tmp.path().join(format!("cel_{}.png", i));
414            image.save(path).unwrap();
415        }
416    }
417}
418
419#[test]
420fn test_combine() {
421    use image::RgbaImage;
422    use tempfile::TempDir;
423
424    let path = "./tests/combine.aseprite";
425    let file = std::fs::read(path).unwrap();
426    let file = AsepriteFile::load(&file).unwrap();
427
428    let (width, height) = file.size();
429    for (index, _) in file.frames().iter().enumerate() {
430        let mut target = vec![0; usize::from(width * height) * 4];
431        let _ = file.combined_frame_image(index, &mut target).unwrap();
432        let image = RgbaImage::from_raw(u32::from(width), u32::from(height), target).unwrap();
433
434        let tmp = TempDir::with_prefix("aseprite-loader").unwrap();
435        println!("{:?}", tmp);
436        let path = tmp.path().join(format!("combined_{}.png", index));
437        image.save(path).unwrap();
438    }
439}
440
441/// https://github.com/bikeshedder/aseprite-loader/issues/4
442#[test]
443fn test_issue_4_1() {
444    let path = "./tests/issue_4_1.aseprite";
445    let file = std::fs::read(path).unwrap();
446    let file = AsepriteFile::load(&file).unwrap();
447    let (width, height) = file.size();
448    let mut buf = vec![0; usize::from(width * height) * 4];
449    for idx in 0..file.frames().len() {
450        let _ = file.combined_frame_image(idx, &mut buf).unwrap();
451    }
452}
453
454#[test]
455fn test_issue_4_2() {
456    let path = "./tests/issue_4_2.aseprite";
457    let file = std::fs::read(path).unwrap();
458    let file = AsepriteFile::load(&file).unwrap();
459    let (width, height) = file.size();
460    let mut buf = vec![0; usize::from(width * height) * 4];
461    for idx in 0..file.frames().len() {
462        let _ = file.combined_frame_image(idx, &mut buf).unwrap();
463    }
464}