Skip to main content

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    /// Parsed low-level file representation.
36    pub file: File<'a>,
37    /// All layers in the file in order
38    pub layers: Vec<Layer>,
39    /// All frames in the file in order
40    pub frames: Vec<Frame>,
41    /// All tags in the file
42    pub tags: Vec<Tag>,
43    /// All images in the file
44    pub images: Vec<Image<'a>>,
45}
46
47/// A cel in a frame
48///
49/// This is a reference to an image cel
50#[derive(Debug, Copy, Clone)]
51pub struct FrameCel {
52    /// Cel origin in sprite coordinates.
53    pub origin: (i16, i16),
54    /// Cel size in pixels `(width, height)`.
55    pub size: (u16, u16),
56    /// Index into [`AsepriteFile::layers`].
57    pub layer_index: usize,
58    /// Index into [`AsepriteFile::images`].
59    pub image_index: usize,
60}
61
62/// A frame in the file
63///
64/// This is a collection of cels for each layer
65#[derive(Debug, Clone)]
66pub struct Frame {
67    /// Frame duration in milliseconds.
68    pub duration: u16,
69    /// Frame origin in sprite coordinates.
70    pub origin: (i16, i16),
71    /// Cels in this frame.
72    pub cels: Vec<FrameCel>,
73}
74
75/// A tag in the file
76///
77/// This is a range of frames over the frames in the file, ordered by frame index
78#[derive(Debug, Clone)]
79pub struct Tag {
80    /// Tag name as defined in the Aseprite file.
81    pub name: String,
82    /// Inclusive frame range covered by this tag.
83    pub range: RangeInclusive<u16>,
84    /// Playback direction for this tag.
85    pub direction: AnimationDirection,
86    /// Optional repeat count (`None` means infinite/unspecified).
87    pub repeat: Option<u16>,
88}
89
90/// A layer in the file
91#[derive(Debug, Clone)]
92pub struct Layer {
93    /// Layer name.
94    pub name: String,
95    /// Layer opacity in the range `0..=255`.
96    pub opacity: u8,
97    /// Layer blend mode.
98    pub blend_mode: BlendMode,
99    /// Whether the layer is visible in the source file.
100    pub visible: bool,
101    /// Layer kind (normal/group/etc).
102    pub layer_type: LayerType,
103}
104
105/// Pre-computed layer visibility selection for efficient per-frame filtering.
106///
107/// Created via [`AsepriteFile::select_layers`].
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum LayerSelection {
110    /// Render visible layers as defined in the aseprite file.
111    Visible,
112    /// Render all layers regardless of their visibility in the aseprite file.
113    All,
114    /// Render layers using a mask.
115    Mask(Vec<bool>),
116}
117
118impl LayerSelection {
119    fn is_selected(&self, layer_index: usize, layer: &Layer) -> bool {
120        match self {
121            Self::Visible => layer.visible,
122            Self::All => true,
123            Self::Mask(mask) => mask.get(layer_index).copied().unwrap_or(false),
124        }
125    }
126}
127
128impl AsepriteFile<'_> {
129    /// Load a aseprite file from a byte slice
130    pub fn load(data: &[u8]) -> Result<AsepriteFile<'_>, LoadSpriteError> {
131        let file = parse_file(data).map_err(|e| LoadSpriteError::Parse {
132            message: e.to_string(),
133        })?;
134        let layers: Vec<_> = file
135            .layers
136            .iter()
137            .filter_map(|layer| {
138                if layer.layer_type == LayerType::Normal || layer.layer_type == LayerType::Group {
139                    Some(Layer {
140                        name: layer.name.to_string(),
141                        opacity: layer.opacity,
142                        blend_mode: layer.blend_mode,
143                        visible: layer.flags.contains(LayerFlags::VISIBLE),
144                        layer_type: layer.layer_type,
145                    })
146                } else {
147                    None
148                }
149            })
150            .collect();
151
152        let mut image_vec: Vec<Image<'_>> = Vec::new();
153        let mut image_map: HashMap<(usize, usize), usize> = HashMap::new();
154
155        for (frame_index, frame) in file.frames.iter().enumerate() {
156            for cel in frame.cels.iter().filter_map(|x| x.as_ref()) {
157                if let CelContent::Image(image) = &cel.content {
158                    let image_index = image_vec.len();
159                    image_vec.push(image.clone());
160                    let _ = image_map.insert((frame_index, cel.layer_index.into()), image_index);
161                }
162            }
163        }
164
165        let mut frames: Vec<Frame> = Vec::new();
166        let mut tags: Vec<Tag> = Vec::new();
167
168        for tag in file.tags.iter() {
169            tags.push(Tag {
170                name: tag.name.to_string(),
171                range: tag.frames.clone(),
172                direction: tag.animation_direction,
173                repeat: if tag.animation_repeat > 0 {
174                    Some(tag.animation_repeat)
175                } else {
176                    None
177                },
178            });
179        }
180
181        for (index, frame) in file.frames.iter().enumerate() {
182            let mut cels: Vec<FrameCel> = Vec::new();
183            for cel in frame.cels.iter().filter_map(|x| x.as_ref()) {
184                let image_index = match cel.content {
185                    CelContent::Image(_) => image_map[&(index, cel.layer_index.into())],
186                    CelContent::LinkedCel { frame_position } => *image_map
187                        .get(&(frame_position.into(), cel.layer_index.into()))
188                        .ok_or_else(|| LoadSpriteError::Parse {
189                            message: format!(
190                                "invalid linked cel at frame {} layer {}",
191                                index, cel.layer_index
192                            ),
193                        })?,
194                    _ => {
195                        return Err(LoadSpriteError::Parse {
196                            message: "invalid cel".to_owned(),
197                        })
198                    }
199                };
200                let width = image_vec[image_index].width;
201                let height = image_vec[image_index].height;
202                cels.push(FrameCel {
203                    origin: (cel.x, cel.y),
204                    size: (width, height),
205                    layer_index: cel.layer_index.into(),
206                    image_index,
207                });
208            }
209
210            frames.push(Frame {
211                duration: frame.duration,
212                origin: (0, 0),
213                cels,
214            });
215        }
216
217        Ok(AsepriteFile {
218            file,
219            tags,
220            layers,
221            frames,
222            images: image_vec,
223        })
224    }
225    /// Get size of the sprite (width, height)
226    pub fn size(&self) -> (u16, u16) {
227        (self.file.header.width, self.file.header.height)
228    }
229    /// Get tag names
230    pub fn tags(&self) -> &[Tag] {
231        &self.tags
232    }
233    /// Get layer names
234    pub fn layers(&self) -> &[Layer] {
235        &self.layers
236    }
237    /// Get the image indices for a given tag and layer
238    pub fn frames(&self) -> &[Frame] {
239        &self.frames
240    }
241    /// Get image count
242    pub fn image_count(&self) -> usize {
243        self.images.len()
244    }
245
246    /// Convert layer names into a [`LayerSelection`] for use with
247    /// [`render_frame`](Self::render_frame).
248    ///
249    /// The premise is used to select the layers which should be included.
250    pub fn select_layers(&self, predicate: impl Fn(&Layer) -> bool) -> LayerSelection {
251        LayerSelection::Mask(self.layers.iter().map(predicate).collect())
252    }
253
254    /// Convert layer names into a [`LayerSelection`] for use with
255    /// [`render_frame`](Self::render_frame).
256    ///
257    /// The returned selection marks a layer as visible if its name appears
258    /// in `names`. Call this once and reuse the result across frames.
259    pub fn select_layers_by_name(&self, names: &[&str]) -> LayerSelection {
260        self.select_layers(|l| names.contains(&l.name.as_str()))
261    }
262
263    /// Render a frame for a given frame index. This combines all visible
264    /// layers into a buffer.
265    ///
266    /// Returns a hash describing the image, since cels can be reused in multiple frames.
267    #[deprecated(note = "Use `render_frame` instead.")]
268    pub fn combined_frame_image(
269        &self,
270        frame_index: usize,
271        target: &mut [u8],
272    ) -> Result<u64, LoadImageError> {
273        self.render_frame(frame_index, target, &LayerSelection::Visible)?;
274        let mut hasher = DefaultHasher::new();
275        let frame = &self.frames[frame_index];
276        for cel in frame.cels.iter() {
277            (cel.image_index, cel.layer_index, cel.origin, cel.size).hash(&mut hasher);
278        }
279        Ok(hasher.finish())
280    }
281
282    /// Render a frame for a given frame index. This combines layers depending
283    /// on the layer selection into a buffer.
284    ///
285    /// The `target` buffer must be at least `width * height * 4` bytes.
286    /// Pixels are written as RGBA8.
287    pub fn render_frame(
288        &self,
289        frame_index: usize,
290        target: &mut [u8],
291        layers: &LayerSelection,
292    ) -> Result<(), LoadImageError> {
293        let target_size =
294            usize::from(self.file.header.width) * usize::from(self.file.header.height) * 4;
295
296        if target.len() < target_size {
297            return Err(LoadImageError::TargetBufferTooSmall);
298        }
299
300        let frame = &self.frames[frame_index];
301
302        for cel in frame.cels.iter() {
303            let Some(layer) = self.layers.get(cel.layer_index) else {
304                continue;
305            };
306            if layer.layer_type == LayerType::Group || !layers.is_selected(cel.layer_index, layer) {
307                continue;
308            }
309
310            let mut cel_target = vec![0; usize::from(cel.size.0) * usize::from(cel.size.1) * 4];
311            self.load_image(cel.image_index, &mut cel_target).unwrap();
312            let layer = &self.layers[cel.layer_index];
313
314            let blend_fn = blend_mode_to_blend_fn(layer.blend_mode);
315
316            for y in 0..cel.size.1 {
317                for x in 0..cel.size.0 {
318                    let Some(target_x) = x.checked_add_signed(cel.origin.0) else {
319                        continue;
320                    };
321                    if target_x >= self.file.header.width {
322                        continue;
323                    }
324                    let Some(target_y) = y.checked_add_signed(cel.origin.1) else {
325                        continue;
326                    };
327                    if target_y >= self.file.header.height {
328                        continue;
329                    }
330
331                    let target_index = usize::from(target_y) * usize::from(self.file.header.width)
332                        + usize::from(target_x);
333                    let cel_index = usize::from(y) * usize::from(cel.size.0) + usize::from(x);
334
335                    let cel_pixel: &[u8] = &cel_target[cel_index * 4..cel_index * 4 + 4];
336                    let target_pixel: &mut [u8] =
337                        &mut target[target_index * 4..target_index * 4 + 4];
338
339                    let back = Color::from(&*target_pixel);
340                    let front = Color::from(cel_pixel);
341                    let out = blend_fn(back, front, layer.opacity);
342
343                    target_pixel[0] = out.r;
344                    target_pixel[1] = out.g;
345                    target_pixel[2] = out.b;
346                    target_pixel[3] = out.a;
347                }
348            }
349        }
350        Ok(())
351    }
352
353    /// Get image loader for a given image index
354    pub fn load_image(&self, index: usize, target: &mut [u8]) -> Result<(), LoadImageError> {
355        let image = &self.images[index];
356        let target_size = usize::from(image.width) * usize::from(image.height) * 4;
357        if target.len() < target_size {
358            return Err(LoadImageError::TargetBufferTooSmall);
359        }
360        let target = &mut target[..target_size];
361        match (self.file.header.color_depth, image.compressed) {
362            (ColorDepth::Rgba, false) => target.copy_from_slice(image.data),
363            (ColorDepth::Rgba, true) => decompress(image.data, target)?,
364            (ColorDepth::Grayscale, false) => {
365                grayscale_to_rgba(image.data, target)?;
366            }
367            (ColorDepth::Grayscale, true) => {
368                let mut buf = vec![0u8; usize::from(image.width) * usize::from(image.height) * 2];
369                decompress(image.data, &mut buf)?;
370                grayscale_to_rgba(&buf, target)?;
371            }
372            (ColorDepth::Indexed, false) => {
373                indexed_to_rgba(
374                    image.data,
375                    self.file
376                        .palette
377                        .as_ref()
378                        .ok_or(LoadImageError::MissingPalette)?,
379                    target,
380                )?;
381            }
382            (ColorDepth::Indexed, true) => {
383                let mut buf = vec![0u8; usize::from(image.width) * usize::from(image.height)];
384                decompress(image.data, &mut buf)?;
385                indexed_to_rgba(
386                    &buf,
387                    self.file
388                        .palette
389                        .as_ref()
390                        .ok_or(LoadImageError::MissingPalette)?,
391                    target,
392                )?;
393            }
394            (ColorDepth::Unknown(_), _) => return Err(LoadImageError::UnsupportedColorDepth),
395        }
396        Ok(())
397    }
398    /// Return all slice chunks from the source file.
399    pub fn slices(&self) -> &[SliceChunk<'_>] {
400        &self.file.slices
401    }
402}
403
404use thiserror::Error;
405
406/// Errors that can occur while building the high-level [`AsepriteFile`].
407#[derive(Error, Debug)]
408pub enum LoadSpriteError {
409    /// Parsing the binary `.aseprite` content failed.
410    #[error("parsing failed {message}")]
411    Parse {
412        /// Detailed parse failure description.
413        message: String,
414    },
415    /// A named tag lookup failed.
416    #[error("missing tag: {0}")]
417    MissingTag(String),
418    /// A named layer lookup failed.
419    #[error("missing layer: {0}")]
420    MissingLayer(String),
421    /// A frame index was outside available frame bounds.
422    #[error("frame index out of range: {0}")]
423    FrameIndexOutOfRange(usize),
424}
425
426/// Errors that can occur while decoding image pixel data.
427#[allow(missing_copy_implementations)]
428#[derive(Error, Debug)]
429pub enum LoadImageError {
430    /// The destination buffer is not large enough.
431    #[error("target buffer too small")]
432    TargetBufferTooSmall,
433    /// Indexed image decoding requires a palette, but none was present.
434    #[error("missing palette")]
435    MissingPalette,
436    /// The file uses an unsupported color depth.
437    #[error("unsupported color depth")]
438    UnsupportedColorDepth,
439    /// Zlib decompression failed.
440    #[error("decompression failed")]
441    DecompressError,
442    /// Pixel data length did not match the expected format.
443    #[error("invalid image data")]
444    InvalidImageData,
445}
446
447/// Decompress zlib-compressed image bytes into `target`.
448///
449/// The expected output size depends on color depth and dimensions.
450pub fn decompress(data: &[u8], target: &mut [u8]) -> Result<(), LoadImageError> {
451    let mut decompressor = Decompress::new(true);
452    match decompressor.decompress(data, target, flate2::FlushDecompress::Finish) {
453        Ok(flate2::Status::Ok | flate2::Status::BufError) => Err(LoadImageError::DecompressError),
454        Ok(flate2::Status::StreamEnd) => Ok(()),
455        Err(_) => Err(LoadImageError::DecompressError),
456    }
457}
458
459fn grayscale_to_rgba(source: &[u8], target: &mut [u8]) -> Result<(), LoadImageError> {
460    if target.len() != source.len() * 2 {
461        return Err(LoadImageError::InvalidImageData);
462    }
463    for (i, chunk) in source.chunks(2).enumerate() {
464        target[i * 4] = chunk[0];
465        target[i * 4 + 1] = chunk[0];
466        target[i * 4 + 2] = chunk[0];
467        target[i * 4 + 3] = chunk[1];
468    }
469    Ok(())
470}
471
472fn indexed_to_rgba(
473    source: &[u8],
474    palette: &Palette,
475    target: &mut [u8],
476) -> Result<(), LoadImageError> {
477    if target.len() != source.len() * 4 {
478        return Err(LoadImageError::InvalidImageData);
479    }
480    for (i, px) in source.iter().enumerate() {
481        let color = palette.colors[usize::from(*px)];
482        target[i * 4] = color.red;
483        target[i * 4 + 1] = color.green;
484        target[i * 4 + 2] = color.blue;
485        target[i * 4 + 3] = color.alpha;
486    }
487    Ok(())
488}
489
490#[test]
491fn test_cel() {
492    use image::RgbaImage;
493    use tempfile::TempDir;
494
495    let path = "./tests/combine.aseprite";
496    let file = std::fs::read(path).unwrap();
497    let file = AsepriteFile::load(&file).unwrap();
498
499    for frame in file.frames().iter() {
500        for (i, cel) in frame.cels.iter().enumerate() {
501            let (width, height) = cel.size;
502
503            let mut target = vec![0; usize::from(width * height) * 4];
504            file.load_image(cel.image_index, &mut target).unwrap();
505
506            let image = RgbaImage::from_raw(u32::from(width), u32::from(height), target).unwrap();
507
508            let tmp = TempDir::with_prefix("aseprite-loader").unwrap();
509            let path = tmp.path().join(format!("cel_{}.png", i));
510            image.save(path).unwrap();
511        }
512    }
513}
514
515#[test]
516fn test_combine() {
517    use image::RgbaImage;
518    use tempfile::TempDir;
519
520    let path = "./tests/combine.aseprite";
521    let file = std::fs::read(path).unwrap();
522    let file = AsepriteFile::load(&file).unwrap();
523
524    let (width, height) = file.size();
525    for (index, _) in file.frames().iter().enumerate() {
526        let mut target = vec![0; usize::from(width * height) * 4];
527        file.render_frame(index, &mut target, &LayerSelection::Visible)
528            .unwrap();
529        let image = RgbaImage::from_raw(u32::from(width), u32::from(height), target).unwrap();
530
531        let tmp = TempDir::with_prefix("aseprite-loader").unwrap();
532        println!("{:?}", tmp);
533        let path = tmp.path().join(format!("combined_{}.png", index));
534        image.save(path).unwrap();
535    }
536}
537
538#[test]
539fn test_visible_layers_empty_produces_blank() {
540    let data = std::fs::read("./tests/layers.aseprite").unwrap();
541    let file = AsepriteFile::load(&data).unwrap();
542    let (width, height) = file.size();
543    let mut buf = vec![0u8; usize::from(width) * usize::from(height) * 4];
544    let sel = file.select_layers_by_name(&[]);
545    file.render_frame(0, &mut buf, &sel).unwrap();
546    assert!(
547        buf.iter().all(|&b| b == 0),
548        "empty layer list should produce a blank image"
549    );
550}
551
552#[test]
553fn test_visible_layers_single_differs_from_all() {
554    let data = std::fs::read("./tests/layers.aseprite").unwrap();
555    let file = AsepriteFile::load(&data).unwrap();
556
557    let visible_normal: Vec<&str> = file
558        .layers()
559        .iter()
560        .filter(|l| l.visible && l.layer_type != LayerType::Group)
561        .map(|l| l.name.as_str())
562        .collect();
563
564    assert!(
565        visible_normal.len() >= 2,
566        "layers.aseprite needs at least 2 visible layers for this test"
567    );
568
569    let (width, height) = file.size();
570    let buf_size = usize::from(width) * usize::from(height) * 4;
571
572    let mut buf_all = vec![0u8; buf_size];
573    file.render_frame(0, &mut buf_all, &LayerSelection::Visible)
574        .unwrap();
575
576    let sel = file.select_layers_by_name(&visible_normal[..1]);
577    let mut buf_one = vec![0u8; buf_size];
578    file.render_frame(0, &mut buf_one, &sel).unwrap();
579
580    assert_ne!(
581        buf_all, buf_one,
582        "single layer composite should differ from all-layers composite"
583    );
584}
585
586/// https://github.com/bikeshedder/aseprite-loader/issues/4
587#[test]
588fn test_issue_4_1() {
589    let path = "./tests/issue_4_1.aseprite";
590    let file = std::fs::read(path).unwrap();
591    let file = AsepriteFile::load(&file).unwrap();
592    let (width, height) = file.size();
593    let mut buf = vec![0; usize::from(width * height) * 4];
594    for idx in 0..file.frames().len() {
595        file.render_frame(idx, &mut buf, &LayerSelection::Visible)
596            .unwrap();
597    }
598}
599
600#[test]
601fn test_issue_4_2() {
602    let path = "./tests/issue_4_2.aseprite";
603    let file = std::fs::read(path).unwrap();
604    let file = AsepriteFile::load(&file).unwrap();
605    let (width, height) = file.size();
606    let mut buf = vec![0; usize::from(width * height) * 4];
607    for idx in 0..file.frames().len() {
608        file.render_frame(idx, &mut buf, &LayerSelection::Visible)
609            .unwrap();
610    }
611}