Skip to main content

aseprite/
types.rs

1use std::collections::BTreeMap;
2use std::ops::RangeInclusive;
3
4use crate::error::AsepriteError;
5
6// --- Enums ---
7
8/// The color depth mode of an Aseprite file.
9#[non_exhaustive]
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11pub enum ColorMode {
12    Rgba,
13    Grayscale,
14    Indexed,
15}
16
17impl ColorMode {
18    /// Returns the number of bytes per pixel for this mode.
19    pub fn bytes_per_pixel(self) -> usize {
20        match self {
21            Self::Rgba => 4,
22            Self::Grayscale => 2,
23            Self::Indexed => 1,
24        }
25    }
26
27    pub(crate) fn from_depth(depth: u16) -> Result<Self, AsepriteError> {
28        match depth {
29            32 => Ok(Self::Rgba),
30            16 => Ok(Self::Grayscale),
31            8 => Ok(Self::Indexed),
32            d => Err(AsepriteError::UnsupportedColorDepth(d)),
33        }
34    }
35
36    pub(crate) fn to_depth(self) -> u16 {
37        match self {
38            Self::Rgba => 32,
39            Self::Grayscale => 16,
40            Self::Indexed => 8,
41        }
42    }
43}
44
45/// The type of a layer.
46#[non_exhaustive]
47#[derive(Copy, Clone, Debug, PartialEq, Eq)]
48pub enum LayerKind {
49    Normal,
50    Group,
51    /// A tilemap layer referencing a tileset by index.
52    Tilemap {
53        tileset_index: u32,
54    },
55}
56
57/// Layer blend mode, matching Aseprite's blend mode list.
58#[non_exhaustive]
59#[derive(Copy, Clone, Debug, PartialEq, Eq)]
60pub enum BlendMode {
61    Normal,
62    Multiply,
63    Screen,
64    Overlay,
65    Darken,
66    Lighten,
67    ColorDodge,
68    ColorBurn,
69    HardLight,
70    SoftLight,
71    Difference,
72    Exclusion,
73    Hue,
74    Saturation,
75    Color,
76    Luminosity,
77    Addition,
78    Subtract,
79    Divide,
80}
81
82impl BlendMode {
83    pub(crate) fn from_u16(v: u16) -> Self {
84        match v {
85            0 => Self::Normal,
86            1 => Self::Multiply,
87            2 => Self::Screen,
88            3 => Self::Overlay,
89            4 => Self::Darken,
90            5 => Self::Lighten,
91            6 => Self::ColorDodge,
92            7 => Self::ColorBurn,
93            8 => Self::HardLight,
94            9 => Self::SoftLight,
95            10 => Self::Difference,
96            11 => Self::Exclusion,
97            12 => Self::Hue,
98            13 => Self::Saturation,
99            14 => Self::Color,
100            15 => Self::Luminosity,
101            16 => Self::Addition,
102            17 => Self::Subtract,
103            18 => Self::Divide,
104            _ => Self::Normal,
105        }
106    }
107
108    pub(crate) fn to_u16(self) -> u16 {
109        match self {
110            Self::Normal => 0,
111            Self::Multiply => 1,
112            Self::Screen => 2,
113            Self::Overlay => 3,
114            Self::Darken => 4,
115            Self::Lighten => 5,
116            Self::ColorDodge => 6,
117            Self::ColorBurn => 7,
118            Self::HardLight => 8,
119            Self::SoftLight => 9,
120            Self::Difference => 10,
121            Self::Exclusion => 11,
122            Self::Hue => 12,
123            Self::Saturation => 13,
124            Self::Color => 14,
125            Self::Luminosity => 15,
126            Self::Addition => 16,
127            Self::Subtract => 17,
128            Self::Divide => 18,
129        }
130    }
131}
132
133/// Animation loop direction for tags.
134#[non_exhaustive]
135#[derive(Copy, Clone, Debug, PartialEq, Eq)]
136pub enum LoopDirection {
137    Forward,
138    Reverse,
139    PingPong,
140    PingPongReverse,
141}
142
143impl LoopDirection {
144    pub(crate) fn from_u8(v: u8) -> Self {
145        match v {
146            0 => Self::Forward,
147            1 => Self::Reverse,
148            2 => Self::PingPong,
149            3 => Self::PingPongReverse,
150            _ => Self::Forward,
151        }
152    }
153
154    pub(crate) fn to_u8(self) -> u8 {
155        match self {
156            Self::Forward => 0,
157            Self::Reverse => 1,
158            Self::PingPong => 2,
159            Self::PingPongReverse => 3,
160        }
161    }
162}
163
164/// Color profile embedded in the file.
165#[non_exhaustive]
166#[derive(Clone, Debug, PartialEq)]
167pub enum ColorProfile {
168    None,
169    SRgb {
170        flags: u16,
171        gamma: u32,
172    },
173    Icc {
174        flags: u16,
175        gamma: u32,
176        data: Vec<u8>,
177    },
178}
179
180// --- Handles ---
181
182/// Handle to a non-group layer. Obtained from [`AsepriteFile::add_layer`] or [`AsepriteFile::layer_ref`].
183#[derive(Copy, Clone, Debug, PartialEq, Eq)]
184pub struct LayerRef(pub(crate) usize);
185
186/// Handle to a group layer. Obtained from [`AsepriteFile::add_group`] or [`AsepriteFile::group_ref`].
187#[derive(Copy, Clone, Debug, PartialEq, Eq)]
188pub struct GroupRef(pub(crate) usize);
189
190impl LayerRef {
191    /// Returns the underlying layer index.
192    pub fn index(&self) -> usize {
193        self.0
194    }
195}
196
197impl GroupRef {
198    /// Returns the underlying layer index.
199    pub fn index(&self) -> usize {
200        self.0
201    }
202}
203
204// --- Data structs ---
205
206/// Raw pixel data buffer. Format depends on the file's [`ColorMode`]: RGBA = 4 bytes/pixel, Grayscale = 2 bytes/pixel, Indexed = 1 byte/pixel.
207#[derive(Clone, Debug, PartialEq)]
208pub struct Pixels {
209    pub data: Vec<u8>,
210    pub width: u16,
211    pub height: u16,
212}
213
214impl Pixels {
215    /// Creates a new pixel buffer, validating that `data.len()` matches `width * height * color_mode.bytes_per_pixel()`.
216    pub fn new(
217        data: Vec<u8>,
218        width: u16,
219        height: u16,
220        color_mode: ColorMode,
221    ) -> Result<Self, AsepriteError> {
222        let expected = width as usize * height as usize * color_mode.bytes_per_pixel();
223        if data.len() != expected {
224            return Err(AsepriteError::PixelSizeMismatch {
225                expected,
226                actual: data.len(),
227            });
228        }
229        Ok(Self {
230            data,
231            width,
232            height,
233        })
234    }
235}
236
237/// An RGBA color with an optional name (used in palettes).
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct Color {
240    pub r: u8,
241    pub g: u8,
242    pub b: u8,
243    pub a: u8,
244    pub name: Option<String>,
245}
246
247/// Grid overlay settings.
248#[derive(Clone, Debug, PartialEq)]
249pub struct GridInfo {
250    pub x: i16,
251    pub y: i16,
252    pub width: u16,
253    pub height: u16,
254}
255
256impl Default for GridInfo {
257    fn default() -> Self {
258        Self {
259            x: 0,
260            y: 0,
261            width: 16,
262            height: 16,
263        }
264    }
265}
266
267#[derive(Clone)]
268pub(crate) struct UnknownChunk {
269    pub frame_index: usize,
270    pub chunk_type: u16,
271    pub data: Vec<u8>,
272}
273
274#[derive(Clone, Debug)]
275pub(crate) struct ChunkOrderEntry {
276    pub frame_index: usize,
277    pub chunk_type: u16,
278    /// For cel chunks (0x2005), stores the layer index to identify which cel
279    pub layer_index: Option<usize>,
280}
281
282// --- Layer ---
283
284/// A layer in the file (normal, group, or tilemap).
285#[derive(Clone, Debug, PartialEq)]
286pub struct Layer {
287    pub name: String,
288    pub kind: LayerKind,
289    pub parent: Option<usize>,
290    pub opacity: u8,
291    pub blend_mode: BlendMode,
292    pub visible: bool,
293    pub editable: bool,
294    pub lock_movement: bool,
295    pub background: bool,
296    pub prefer_linked_cels: bool,
297    pub collapsed: bool,
298    pub reference_layer: bool,
299    pub user_data: Option<UserData>,
300}
301
302/// Options for creating a new layer.
303pub struct LayerOptions {
304    pub opacity: u8,
305    pub blend_mode: BlendMode,
306    pub visible: bool,
307    pub editable: bool,
308    pub lock_movement: bool,
309    pub background: bool,
310    pub collapsed: bool,
311    pub prefer_linked_cels: bool,
312    pub reference_layer: bool,
313}
314
315impl Default for LayerOptions {
316    fn default() -> Self {
317        Self {
318            opacity: 255,
319            blend_mode: BlendMode::Normal,
320            visible: true,
321            editable: true,
322            lock_movement: false,
323            background: false,
324            collapsed: false,
325            prefer_linked_cels: false,
326            reference_layer: false,
327        }
328    }
329}
330
331// --- Frame ---
332
333/// A single animation frame.
334#[derive(Clone, Debug, PartialEq, Eq)]
335pub struct Frame {
336    pub duration_ms: u16,
337}
338
339// --- Tag ---
340
341/// An animation tag spanning a range of frames.
342#[derive(Clone, Debug, PartialEq)]
343pub struct Tag {
344    pub name: String,
345    pub from_frame: usize,
346    pub to_frame: usize,
347    pub direction: LoopDirection,
348    pub repeat: u16,
349    pub user_data: Option<UserData>,
350}
351
352// --- Cel ---
353
354/// A cel (the content of one layer in one frame).
355#[derive(Clone, Debug, PartialEq)]
356pub struct Cel {
357    pub kind: CelKind,
358    pub opacity: u8,
359    pub z_index: i16,
360    pub user_data: Option<UserData>,
361    pub extra: Option<CelExtra>,
362}
363
364/// The type and data of a cel.
365#[non_exhaustive]
366#[derive(Clone, Debug, PartialEq)]
367pub enum CelKind {
368    /// Uncompressed pixel data.
369    Raw { pixels: Pixels, x: i16, y: i16 },
370    /// Zlib-compressed pixel data. `original_compressed` preserves the original bytes for round-trip fidelity; `None` for programmatically created cels.
371    Compressed {
372        pixels: Pixels,
373        x: i16,
374        y: i16,
375        original_compressed: Option<Vec<u8>>,
376    },
377    /// References another frame's cel on the same layer. Use [`AsepriteFile::resolve_cel`] to follow the link.
378    Linked { source_frame: usize, x: i16, y: i16 },
379    /// Tilemap data referencing tiles by ID.
380    Tilemap {
381        width: u16,
382        height: u16,
383        bits_per_tile: u16,
384        tile_id_bitmask: u32,
385        x_flip_bitmask: u32,
386        y_flip_bitmask: u32,
387        d_flip_bitmask: u32,
388        tiles: Vec<u32>,
389        x: i16,
390        y: i16,
391        original_compressed: Option<Vec<u8>>,
392    },
393}
394
395/// Options for creating a cel with non-default opacity, position, or z-index.
396pub struct CelOptions {
397    pub pixels: Pixels,
398    pub x: i16,
399    pub y: i16,
400    pub opacity: u8,
401    pub z_index: i16,
402}
403
404impl Default for CelOptions {
405    fn default() -> Self {
406        Self {
407            pixels: Pixels {
408                data: vec![],
409                width: 0,
410                height: 0,
411            },
412            x: 0,
413            y: 0,
414            opacity: 255,
415            z_index: 0,
416        }
417    }
418}
419
420/// Options for creating a linked cel with non-default opacity, position, z-index, or metadata.
421pub struct LinkedCelOptions {
422    pub x: i16,
423    pub y: i16,
424    pub opacity: u8,
425    pub z_index: i16,
426    pub user_data: Option<UserData>,
427    pub extra: Option<CelExtra>,
428}
429
430impl Default for LinkedCelOptions {
431    fn default() -> Self {
432        Self {
433            x: 0,
434            y: 0,
435            opacity: 255,
436            z_index: 0,
437            user_data: None,
438            extra: None,
439        }
440    }
441}
442
443// --- User Data ---
444
445/// User-defined metadata attached to layers, cels, tags, slices, or the sprite itself.
446#[derive(Clone, Debug, Default, PartialEq)]
447pub struct UserData {
448    pub text: Option<String>,
449    pub color: Option<Color>,
450    pub properties: Vec<PropertiesMap>,
451}
452
453/// A named map of typed properties within user data.
454#[derive(Clone, Debug, PartialEq)]
455pub struct PropertiesMap {
456    pub key: u32,
457    pub entries: Vec<(String, PropertyValue)>,
458}
459
460/// A typed value within a properties map.
461#[non_exhaustive]
462#[derive(Clone, Debug, PartialEq)]
463pub enum PropertyValue {
464    Bool(bool),
465    Int8(i8),
466    UInt8(u8),
467    Int16(i16),
468    UInt16(u16),
469    Int32(i32),
470    UInt32(u32),
471    Int64(i64),
472    UInt64(u64),
473    Fixed(u32),
474    Float(f32),
475    Double(f64),
476    String(String),
477    Point(i32, i32),
478    Size(i32, i32),
479    Rect(i32, i32, i32, i32),
480    Vector(Vec<PropertyValue>),
481    Properties(Vec<(String, PropertyValue)>),
482    Uuid([u8; 16]),
483}
484
485// --- Slice ---
486
487/// A named rectangular region, optionally with nine-patch and pivot data.
488#[derive(Clone, Debug, PartialEq)]
489pub struct Slice {
490    pub name: String,
491    pub keys: Vec<SliceKey>,
492    pub has_nine_patch: bool,
493    pub has_pivot: bool,
494    pub user_data: Option<UserData>,
495}
496
497/// A slice's bounds at a specific frame.
498#[derive(Clone, Debug, PartialEq)]
499pub struct SliceKey {
500    pub frame: u32,
501    pub x: i32,
502    pub y: i32,
503    pub width: u32,
504    pub height: u32,
505    pub nine_patch: Option<NinePatch>,
506    pub pivot: Option<(i32, i32)>,
507}
508
509/// Nine-patch (9-slice) center region within a slice.
510#[derive(Clone, Debug, PartialEq)]
511pub struct NinePatch {
512    pub center_x: i32,
513    pub center_y: i32,
514    pub center_width: u32,
515    pub center_height: u32,
516}
517
518// --- Cel Extra ---
519
520/// Extra precision bounds for a cel.
521#[derive(Clone, Debug, PartialEq)]
522pub struct CelExtra {
523    pub precise_x: u32,
524    pub precise_y: u32,
525    pub width: u32,
526    pub height: u32,
527}
528
529// --- Tileset ---
530
531/// Bitflags for tileset properties.
532#[derive(Copy, Clone, Debug, PartialEq, Eq)]
533pub struct TilesetFlags(pub u32);
534
535impl TilesetFlags {
536    /// Returns whether the tileset references an external file.
537    pub fn has_external_link(self) -> bool {
538        self.0 & 1 != 0
539    }
540    /// Returns whether the tileset has embedded tile pixel data.
541    pub fn has_embedded_tiles(self) -> bool {
542        self.0 & 2 != 0
543    }
544    /// Returns whether tile ID 0 represents an empty tile.
545    pub fn empty_tile_is_zero(self) -> bool {
546        self.0 & 4 != 0
547    }
548}
549
550/// A tileset definition.
551#[derive(Clone, Debug, PartialEq)]
552pub struct Tileset {
553    pub id: u32,
554    pub flags: TilesetFlags,
555    pub name: String,
556    pub tile_count: u32,
557    pub tile_width: u16,
558    pub tile_height: u16,
559    pub base_index: i16,
560    pub data: TilesetData,
561    pub user_data: Option<UserData>,
562    pub tile_user_data: Vec<Option<UserData>>,
563}
564
565/// The pixel data source for a tileset.
566#[non_exhaustive]
567#[derive(Clone, Debug, PartialEq)]
568pub enum TilesetData {
569    Embedded {
570        pixels: Vec<u8>,
571        original_compressed: Option<Vec<u8>>,
572    },
573    External {
574        external_file_id: u32,
575        tileset_id_in_external: u32,
576    },
577    Empty,
578}
579
580// --- External File ---
581
582/// A reference to an external file.
583#[derive(Clone, Debug, PartialEq)]
584pub struct ExternalFile {
585    pub id: u32,
586    pub file_type: ExternalFileType,
587    pub name: String,
588}
589
590/// The type of an external file reference.
591#[non_exhaustive]
592#[derive(Copy, Clone, Debug, PartialEq, Eq)]
593pub enum ExternalFileType {
594    Palette,
595    Tileset,
596    ExtensionProps,
597    ExtensionTileMgmt,
598}
599
600impl ExternalFileType {
601    pub(crate) fn from_u8(v: u8) -> Self {
602        match v {
603            0 => Self::Palette,
604            1 => Self::Tileset,
605            2 => Self::ExtensionProps,
606            3 => Self::ExtensionTileMgmt,
607            _ => Self::Palette,
608        }
609    }
610
611    pub(crate) fn to_u8(self) -> u8 {
612        match self {
613            Self::Palette => 0,
614            Self::Tileset => 1,
615            Self::ExtensionProps => 2,
616            Self::ExtensionTileMgmt => 3,
617        }
618    }
619}
620
621// --- Legacy types (read-only) ---
622
623/// A legacy mask chunk (deprecated in modern Aseprite, preserved for round-trip fidelity).
624#[derive(Clone, Debug, PartialEq)]
625pub struct LegacyMask {
626    pub x: i16,
627    pub y: i16,
628    pub width: u16,
629    pub height: u16,
630    pub name: String,
631    pub bitmap: Vec<u8>,
632}
633
634// --- The main file struct ---
635
636/// An Aseprite `.ase`/`.aseprite` file.
637///
638/// # Creating a file from scratch
639///
640/// ```
641/// use aseprite::*;
642///
643/// let mut file = AsepriteFile::new(32, 32, ColorMode::Rgba);
644/// let layer = file.add_layer("Background");
645/// let frame = file.add_frame(100);
646/// let pixels = Pixels::new(vec![0u8; 32 * 32 * 4], 32, 32, ColorMode::Rgba).unwrap();
647/// file.set_cel(layer, frame, pixels, 0, 0).unwrap();
648/// ```
649///
650/// # Reading and writing
651///
652/// ```no_run
653/// use aseprite::AsepriteFile;
654///
655/// let data = std::fs::read("sprite.aseprite").unwrap();
656/// let file = AsepriteFile::from_reader(&data[..]).unwrap();
657/// let mut output = Vec::new();
658/// file.write_to(&mut output).unwrap();
659/// ```
660#[derive(Clone)]
661pub struct AsepriteFile {
662    width: u16,
663    height: u16,
664    color_mode: ColorMode,
665    flags: u32,
666    deprecated_speed: u16,
667    num_colors: u16,
668    transparent_index: u8,
669    pixel_ratio: (u8, u8),
670    grid: GridInfo,
671    color_profile: Option<ColorProfile>,
672    palette: Vec<Color>,
673    layers: Vec<Layer>,
674    frames: Vec<Frame>,
675    tags: Vec<Tag>,
676    slices: Vec<Slice>,
677    sprite_user_data: Option<UserData>,
678    cels: BTreeMap<(usize, usize), Cel>,
679    tilesets: Vec<Tileset>,
680    external_files: Vec<ExternalFile>,
681    legacy_masks: Vec<LegacyMask>,
682    pub(crate) unknown_chunks: Vec<UnknownChunk>,
683    pub(crate) chunk_order: Vec<ChunkOrderEntry>,
684}
685
686impl std::fmt::Debug for AsepriteFile {
687    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
688        f.debug_struct("AsepriteFile")
689            .field("width", &self.width)
690            .field("height", &self.height)
691            .field("color_mode", &self.color_mode)
692            .field("layers", &self.layers.len())
693            .field("frames", &self.frames.len())
694            .field("tags", &self.tags.len())
695            .field("cels", &self.cels.len())
696            .finish()
697    }
698}
699
700impl AsepriteFile {
701    /// Creates an empty file with the given canvas dimensions and color mode.
702    pub fn new(width: u16, height: u16, color_mode: ColorMode) -> Self {
703        Self {
704            width,
705            height,
706            color_mode,
707            flags: 1,
708            deprecated_speed: 0,
709            num_colors: 0,
710            transparent_index: 0,
711            pixel_ratio: (1, 1),
712            grid: GridInfo::default(),
713            color_profile: None,
714            palette: Vec::new(),
715            layers: Vec::new(),
716            frames: Vec::new(),
717            tags: Vec::new(),
718            slices: Vec::new(),
719            sprite_user_data: None,
720            cels: BTreeMap::new(),
721            tilesets: Vec::new(),
722            external_files: Vec::new(),
723            legacy_masks: Vec::new(),
724            unknown_chunks: Vec::new(),
725            chunk_order: Vec::new(),
726        }
727    }
728
729    // --- Accessors ---
730    /// Returns the canvas width in pixels.
731    pub fn width(&self) -> u16 {
732        self.width
733    }
734    /// Returns the canvas height in pixels.
735    pub fn height(&self) -> u16 {
736        self.height
737    }
738    /// Returns the color mode.
739    pub fn color_mode(&self) -> ColorMode {
740        self.color_mode
741    }
742    /// Returns the raw header flags.
743    pub fn flags(&self) -> u32 {
744        self.flags
745    }
746    pub(crate) fn deprecated_speed(&self) -> u16 {
747        self.deprecated_speed
748    }
749    pub(crate) fn num_colors(&self) -> u16 {
750        self.num_colors
751    }
752    /// Returns the pixel aspect ratio as (width, height).
753    pub fn pixel_ratio(&self) -> (u8, u8) {
754        self.pixel_ratio
755    }
756    /// Returns the grid overlay settings.
757    pub fn grid(&self) -> &GridInfo {
758        &self.grid
759    }
760    /// Returns the color profile, if set.
761    pub fn color_profile(&self) -> &Option<ColorProfile> {
762        &self.color_profile
763    }
764    /// Returns the color palette (empty if no palette chunk was present).
765    pub fn palette(&self) -> &[Color] {
766        &self.palette
767    }
768    /// Returns all layers in document order.
769    pub fn layers(&self) -> &[Layer] {
770        &self.layers
771    }
772    /// Returns all frames.
773    pub fn frames(&self) -> &[Frame] {
774        &self.frames
775    }
776    /// Returns all animation tags.
777    pub fn tags(&self) -> &[Tag] {
778        &self.tags
779    }
780    /// Returns all slices.
781    pub fn slices(&self) -> &[Slice] {
782        &self.slices
783    }
784    /// Returns the sprite-level user data, if set.
785    pub fn sprite_user_data(&self) -> &Option<UserData> {
786        &self.sprite_user_data
787    }
788    /// Returns the transparent palette index (only meaningful for indexed color mode).
789    pub fn transparent_index(&self) -> u8 {
790        self.transparent_index
791    }
792    /// Returns legacy mask chunks (deprecated in modern Aseprite, preserved for round-trip fidelity).
793    pub fn legacy_masks(&self) -> &[LegacyMask] {
794        &self.legacy_masks
795    }
796    /// Returns all tilesets.
797    pub fn tilesets(&self) -> &[Tileset] {
798        &self.tilesets
799    }
800    /// Returns all external file references.
801    pub fn external_files(&self) -> &[ExternalFile] {
802        &self.external_files
803    }
804
805    /// Returns the raw cel at the given layer and frame, or `None`. May return [`CelKind::Linked`]; use [`resolve_cel`](Self::resolve_cel) to follow links.
806    pub fn cel(&self, layer: LayerRef, frame: usize) -> Option<&Cel> {
807        self.cels.get(&(layer.0, frame))
808    }
809
810    /// Returns the cel at the given layer and frame, following linked cels.
811    ///
812    /// Unlike [`cel()`](Self::cel), this never returns [`CelKind::Linked`].
813    /// Returns `None` if the cel doesn't exist or the link target is missing.
814    pub fn resolve_cel(&self, layer: LayerRef, frame: usize) -> Option<&Cel> {
815        let cel = self.cels.get(&(layer.0, frame))?;
816        match &cel.kind {
817            CelKind::Linked { source_frame, .. } => self.cels.get(&(layer.0, *source_frame)),
818            _ => Some(cel),
819        }
820    }
821
822    // --- Handle construction from parsed data ---
823    /// Returns a [`LayerRef`] handle for the given index, or `None` if the index is out of bounds or points to a group.
824    pub fn layer_ref(&self, index: usize) -> Option<LayerRef> {
825        self.layers.get(index).and_then(|l| match l.kind {
826            LayerKind::Normal | LayerKind::Tilemap { .. } => Some(LayerRef(index)),
827            LayerKind::Group => None,
828        })
829    }
830
831    /// Returns a [`GroupRef`] handle for the given index, or `None` if the index is out of bounds or is not a group.
832    pub fn group_ref(&self, index: usize) -> Option<GroupRef> {
833        self.layers.get(index).and_then(|l| {
834            if l.kind == LayerKind::Group {
835                Some(GroupRef(index))
836            } else {
837                None
838            }
839        })
840    }
841
842    // --- Layers ---
843    fn push_layer(
844        &mut self,
845        name: &str,
846        kind: LayerKind,
847        parent: Option<usize>,
848        opts: &LayerOptions,
849    ) -> usize {
850        let index = self.layers.len();
851        self.layers.push(Layer {
852            name: name.to_string(),
853            kind,
854            parent,
855            opacity: opts.opacity,
856            blend_mode: opts.blend_mode,
857            visible: opts.visible,
858            editable: opts.editable,
859            lock_movement: opts.lock_movement,
860            background: opts.background,
861            prefer_linked_cels: opts.prefer_linked_cels,
862            collapsed: opts.collapsed,
863            reference_layer: opts.reference_layer,
864            user_data: None,
865        });
866        index
867    }
868
869    /// Adds a normal layer at the top level.
870    pub fn add_layer(&mut self, name: &str) -> LayerRef {
871        LayerRef(self.push_layer(name, LayerKind::Normal, None, &LayerOptions::default()))
872    }
873    /// Adds a normal layer at the top level with custom options.
874    pub fn add_layer_with(&mut self, name: &str, opts: LayerOptions) -> LayerRef {
875        LayerRef(self.push_layer(name, LayerKind::Normal, None, &opts))
876    }
877    /// Adds a group layer at the top level.
878    pub fn add_group(&mut self, name: &str) -> GroupRef {
879        GroupRef(self.push_layer(name, LayerKind::Group, None, &LayerOptions::default()))
880    }
881    /// Adds a group layer at the top level with custom options.
882    pub fn add_group_with(&mut self, name: &str, opts: LayerOptions) -> GroupRef {
883        GroupRef(self.push_layer(name, LayerKind::Group, None, &opts))
884    }
885    /// Adds a normal layer inside a group.
886    pub fn add_layer_in(&mut self, name: &str, parent: GroupRef) -> LayerRef {
887        LayerRef(self.push_layer(
888            name,
889            LayerKind::Normal,
890            Some(parent.0),
891            &LayerOptions::default(),
892        ))
893    }
894    /// Adds a normal layer inside a group with custom options.
895    pub fn add_layer_in_with(
896        &mut self,
897        name: &str,
898        parent: GroupRef,
899        opts: LayerOptions,
900    ) -> LayerRef {
901        LayerRef(self.push_layer(name, LayerKind::Normal, Some(parent.0), &opts))
902    }
903    /// Adds a nested group inside a parent group.
904    pub fn add_group_in(&mut self, name: &str, parent: GroupRef) -> GroupRef {
905        GroupRef(self.push_layer(
906            name,
907            LayerKind::Group,
908            Some(parent.0),
909            &LayerOptions::default(),
910        ))
911    }
912    /// Adds a nested group inside a parent group with custom options.
913    pub fn add_group_in_with(
914        &mut self,
915        name: &str,
916        parent: GroupRef,
917        opts: LayerOptions,
918    ) -> GroupRef {
919        GroupRef(self.push_layer(name, LayerKind::Group, Some(parent.0), &opts))
920    }
921
922    /// Adds a tilemap layer referencing the given tileset index.
923    pub fn add_tilemap_layer(&mut self, name: &str, tileset_index: u32) -> LayerRef {
924        let index = self.layers.len();
925        self.layers.push(Layer {
926            name: name.to_string(),
927            kind: LayerKind::Tilemap { tileset_index },
928            parent: None,
929            opacity: 255,
930            blend_mode: BlendMode::Normal,
931            visible: true,
932            editable: true,
933            lock_movement: false,
934            background: false,
935            prefer_linked_cels: false,
936            collapsed: false,
937            reference_layer: false,
938            user_data: None,
939        });
940        LayerRef(index)
941    }
942
943    /// Sets tilemap data for a tilemap layer/frame.
944    #[allow(clippy::too_many_arguments)]
945    pub fn set_tilemap_cel(
946        &mut self,
947        layer: LayerRef,
948        frame: usize,
949        tiles: Vec<u32>,
950        width: u16,
951        height: u16,
952        x: i16,
953        y: i16,
954    ) -> Result<(), AsepriteError> {
955        if frame >= self.frames.len() {
956            return Err(AsepriteError::FrameOutOfBounds(frame));
957        }
958        self.cels.insert(
959            (layer.0, frame),
960            Cel {
961                kind: CelKind::Tilemap {
962                    width,
963                    height,
964                    bits_per_tile: 32,
965                    tile_id_bitmask: 0x1fff_ffff,
966                    x_flip_bitmask: 0x2000_0000,
967                    y_flip_bitmask: 0x4000_0000,
968                    d_flip_bitmask: 0x8000_0000,
969                    tiles,
970                    x,
971                    y,
972                    original_compressed: None,
973                },
974                opacity: 255,
975                z_index: 0,
976                user_data: None,
977                extra: None,
978            },
979        );
980        Ok(())
981    }
982
983    // --- Frames ---
984    /// Adds a frame with the given duration in milliseconds. Returns the frame index.
985    pub fn add_frame(&mut self, duration_ms: u16) -> usize {
986        let index = self.frames.len();
987        self.frames.push(Frame { duration_ms });
988        index
989    }
990
991    // --- Cels ---
992    /// Sets compressed pixel data for a layer/frame. Returns an error if the frame index is out of bounds.
993    pub fn set_cel(
994        &mut self,
995        layer: LayerRef,
996        frame: usize,
997        pixels: Pixels,
998        x: i16,
999        y: i16,
1000    ) -> Result<(), AsepriteError> {
1001        if frame >= self.frames.len() {
1002            return Err(AsepriteError::FrameOutOfBounds(frame));
1003        }
1004        self.cels.insert(
1005            (layer.0, frame),
1006            Cel {
1007                kind: CelKind::Compressed {
1008                    pixels,
1009                    x,
1010                    y,
1011                    original_compressed: None,
1012                },
1013                opacity: 255,
1014                z_index: 0,
1015                user_data: None,
1016                extra: None,
1017            },
1018        );
1019        Ok(())
1020    }
1021
1022    /// Sets pixel data for a layer/frame with custom opacity, position, and z-index.
1023    pub fn set_cel_with(
1024        &mut self,
1025        layer: LayerRef,
1026        frame: usize,
1027        opts: CelOptions,
1028    ) -> Result<(), AsepriteError> {
1029        if frame >= self.frames.len() {
1030            return Err(AsepriteError::FrameOutOfBounds(frame));
1031        }
1032        self.cels.insert(
1033            (layer.0, frame),
1034            Cel {
1035                kind: CelKind::Compressed {
1036                    pixels: opts.pixels,
1037                    x: opts.x,
1038                    y: opts.y,
1039                    original_compressed: None,
1040                },
1041                opacity: opts.opacity,
1042                z_index: opts.z_index,
1043                user_data: None,
1044                extra: None,
1045            },
1046        );
1047        Ok(())
1048    }
1049
1050    /// Sets uncompressed pixel data for a layer/frame.
1051    pub fn set_raw_cel(
1052        &mut self,
1053        layer: LayerRef,
1054        frame: usize,
1055        pixels: Pixels,
1056        x: i16,
1057        y: i16,
1058    ) -> Result<(), AsepriteError> {
1059        if frame >= self.frames.len() {
1060            return Err(AsepriteError::FrameOutOfBounds(frame));
1061        }
1062        self.cels.insert(
1063            (layer.0, frame),
1064            Cel {
1065                kind: CelKind::Raw { pixels, x, y },
1066                opacity: 255,
1067                z_index: 0,
1068                user_data: None,
1069                extra: None,
1070            },
1071        );
1072        Ok(())
1073    }
1074
1075    /// Sets a linked cel pointing to another frame's cel. Returns an error if either frame index is out of bounds.
1076    pub fn set_linked_cel(
1077        &mut self,
1078        layer: LayerRef,
1079        frame: usize,
1080        source_frame: usize,
1081    ) -> Result<(), AsepriteError> {
1082        if frame >= self.frames.len() {
1083            return Err(AsepriteError::FrameOutOfBounds(frame));
1084        }
1085        if source_frame >= self.frames.len() {
1086            return Err(AsepriteError::FrameOutOfBounds(source_frame));
1087        }
1088        self.cels.insert(
1089            (layer.0, frame),
1090            Cel {
1091                kind: CelKind::Linked {
1092                    source_frame,
1093                    x: 0,
1094                    y: 0,
1095                },
1096                opacity: 255,
1097                z_index: 0,
1098                user_data: None,
1099                extra: None,
1100            },
1101        );
1102        Ok(())
1103    }
1104
1105    /// Sets a linked cel pointing to another frame's cel with LinkedCelOptions. Returns an error if either frame index is out of bounds.
1106    pub fn set_linked_cel_with(
1107        &mut self,
1108        layer: LayerRef,
1109        frame: usize,
1110        source_frame: usize,
1111        opts: LinkedCelOptions,
1112    ) -> Result<(), AsepriteError> {
1113        if frame >= self.frames.len() {
1114            return Err(AsepriteError::FrameOutOfBounds(frame));
1115        }
1116        if source_frame >= self.frames.len() {
1117            return Err(AsepriteError::FrameOutOfBounds(source_frame));
1118        }
1119
1120        self.cels.insert(
1121            (layer.0, frame),
1122            Cel {
1123                kind: CelKind::Linked {
1124                    source_frame,
1125                    x: opts.x,
1126                    y: opts.y,
1127                },
1128                opacity: opts.opacity,
1129                z_index: opts.z_index,
1130                user_data: opts.user_data,
1131                extra: opts.extra,
1132            },
1133        );
1134
1135        Ok(())
1136    }
1137
1138    // --- Tags ---
1139    /// Adds an animation tag spanning the given frame range.
1140    pub fn add_tag(
1141        &mut self,
1142        name: &str,
1143        frames: RangeInclusive<usize>,
1144        direction: LoopDirection,
1145    ) -> Result<usize, AsepriteError> {
1146        self.add_tag_with(name, frames, direction, 0)
1147    }
1148
1149    /// Adds an animation tag with a custom repeat count.
1150    pub fn add_tag_with(
1151        &mut self,
1152        name: &str,
1153        frames: RangeInclusive<usize>,
1154        direction: LoopDirection,
1155        repeat: u16,
1156    ) -> Result<usize, AsepriteError> {
1157        let from = *frames.start();
1158        let to = *frames.end();
1159        if !self.frames.is_empty() && to >= self.frames.len() {
1160            return Err(AsepriteError::InvalidFrameRange);
1161        }
1162        let index = self.tags.len();
1163        self.tags.push(Tag {
1164            name: name.to_string(),
1165            from_frame: from,
1166            to_frame: to,
1167            direction,
1168            repeat,
1169            user_data: None,
1170        });
1171        Ok(index)
1172    }
1173
1174    // --- Palette ---
1175    /// Sets the color palette. Returns an error if more than 256 entries.
1176    pub fn set_palette(&mut self, colors: &[Color]) -> Result<(), AsepriteError> {
1177        if colors.len() > 256 {
1178            return Err(AsepriteError::FormatLimitExceeded {
1179                field: "palette",
1180                value: colors.len(),
1181                max: 256,
1182            });
1183        }
1184        self.palette = colors.to_vec();
1185        Ok(())
1186    }
1187
1188    /// Sets the transparent palette index.
1189    pub fn set_transparent_index(&mut self, index: u8) {
1190        self.transparent_index = index;
1191    }
1192    /// Sets the color profile.
1193    pub fn set_color_profile(&mut self, profile: ColorProfile) {
1194        self.color_profile = Some(profile);
1195    }
1196
1197    /// Adds a slice.
1198    pub fn add_slice(&mut self, slice: Slice) {
1199        self.slices.push(slice);
1200    }
1201    /// Sets the sprite-level user data.
1202    pub fn set_sprite_user_data(&mut self, ud: UserData) {
1203        self.sprite_user_data = Some(ud);
1204    }
1205    /// Adds a tileset definition.
1206    pub fn add_tileset(&mut self, tileset: Tileset) {
1207        self.tilesets.push(tileset);
1208    }
1209    /// Adds an external file reference.
1210    pub fn add_external_file(&mut self, ef: ExternalFile) {
1211        self.external_files.push(ef);
1212    }
1213
1214    /// Sets user data on a layer.
1215    pub fn set_layer_user_data(&mut self, layer: LayerRef, ud: UserData) {
1216        if let Some(l) = self.layers.get_mut(layer.0) {
1217            l.user_data = Some(ud);
1218        }
1219    }
1220    /// Sets user data on a group layer.
1221    pub fn set_group_user_data(&mut self, group: GroupRef, ud: UserData) {
1222        if let Some(l) = self.layers.get_mut(group.0) {
1223            l.user_data = Some(ud);
1224        }
1225    }
1226    /// Sets user data on a cel.
1227    pub fn set_cel_user_data(&mut self, layer: LayerRef, frame: usize, ud: UserData) {
1228        if let Some(cel) = self.cels.get_mut(&(layer.0, frame)) {
1229            cel.user_data = Some(ud);
1230        }
1231    }
1232    /// Sets extra precision bounds on a cel.
1233    pub fn set_cel_extra(&mut self, layer: LayerRef, frame: usize, extra: CelExtra) {
1234        if let Some(cel) = self.cels.get_mut(&(layer.0, frame)) {
1235            cel.extra = Some(extra);
1236        }
1237    }
1238    /// Sets user data on a tag.
1239    pub fn set_tag_user_data(&mut self, tag_index: usize, ud: UserData) {
1240        if let Some(tag) = self.tags.get_mut(tag_index) {
1241            tag.user_data = Some(ud);
1242        }
1243    }
1244
1245    // --- Internal setters for reader ---
1246    pub(crate) fn set_flags(&mut self, flags: u32) {
1247        self.flags = flags;
1248    }
1249    pub(crate) fn set_deprecated_speed(&mut self, speed: u16) {
1250        self.deprecated_speed = speed;
1251    }
1252    pub(crate) fn set_num_colors(&mut self, n: u16) {
1253        self.num_colors = n;
1254    }
1255    pub(crate) fn set_pixel_ratio(&mut self, ratio: (u8, u8)) {
1256        self.pixel_ratio = ratio;
1257    }
1258    pub(crate) fn set_grid(&mut self, grid: GridInfo) {
1259        self.grid = grid;
1260    }
1261    pub(crate) fn push_legacy_mask(&mut self, mask: LegacyMask) {
1262        self.legacy_masks.push(mask);
1263    }
1264    pub(crate) fn push_unknown_chunk(
1265        &mut self,
1266        frame_index: usize,
1267        chunk_type: u16,
1268        data: Vec<u8>,
1269    ) {
1270        self.unknown_chunks.push(UnknownChunk {
1271            frame_index,
1272            chunk_type,
1273            data,
1274        });
1275    }
1276    pub(crate) fn push_tileset(&mut self, tileset: Tileset) {
1277        self.tilesets.push(tileset);
1278    }
1279    pub(crate) fn push_external_file(&mut self, ef: ExternalFile) {
1280        self.external_files.push(ef);
1281    }
1282    pub(crate) fn tilesets_mut(&mut self) -> &mut Vec<Tileset> {
1283        &mut self.tilesets
1284    }
1285    pub(crate) fn push_layer_raw(&mut self, layer: Layer) {
1286        self.layers.push(layer);
1287    }
1288    pub(crate) fn insert_cel(&mut self, layer_index: usize, frame_index: usize, cel: Cel) {
1289        self.cels.insert((layer_index, frame_index), cel);
1290    }
1291    pub(crate) fn push_tag(&mut self, tag: Tag) {
1292        self.tags.push(tag);
1293    }
1294    pub(crate) fn push_slice(&mut self, slice: Slice) {
1295        self.slices.push(slice);
1296    }
1297    pub(crate) fn set_sprite_user_data_raw(&mut self, ud: UserData) {
1298        self.sprite_user_data = Some(ud);
1299    }
1300    pub(crate) fn layers_mut(&mut self) -> &mut Vec<Layer> {
1301        &mut self.layers
1302    }
1303    pub(crate) fn tags_mut(&mut self) -> &mut Vec<Tag> {
1304        &mut self.tags
1305    }
1306    pub(crate) fn slices_mut(&mut self) -> &mut Vec<Slice> {
1307        &mut self.slices
1308    }
1309    pub(crate) fn cel_mut(&mut self, layer_index: usize, frame_index: usize) -> Option<&mut Cel> {
1310        self.cels.get_mut(&(layer_index, frame_index))
1311    }
1312    pub(crate) fn set_palette_entry(&mut self, index: usize, color: Color) {
1313        if index >= self.palette.len() {
1314            self.palette.resize(
1315                index + 1,
1316                Color {
1317                    r: 0,
1318                    g: 0,
1319                    b: 0,
1320                    a: 255,
1321                    name: None,
1322                },
1323            );
1324        }
1325        self.palette[index] = color;
1326    }
1327    pub(crate) fn cels_iter(&self) -> impl Iterator<Item = (&(usize, usize), &Cel)> {
1328        self.cels.iter()
1329    }
1330    pub(crate) fn unknown_chunks_for_frame(
1331        &self,
1332        frame_index: usize,
1333    ) -> impl Iterator<Item = &UnknownChunk> {
1334        self.unknown_chunks
1335            .iter()
1336            .filter(move |uc| uc.frame_index == frame_index)
1337    }
1338
1339    pub(crate) fn push_chunk_order(
1340        &mut self,
1341        frame_index: usize,
1342        chunk_type: u16,
1343        layer_index: Option<usize>,
1344    ) {
1345        self.chunk_order.push(ChunkOrderEntry {
1346            frame_index,
1347            chunk_type,
1348            layer_index,
1349        });
1350    }
1351
1352    pub(crate) fn chunk_order_for_frame(
1353        &self,
1354        frame_index: usize,
1355    ) -> impl Iterator<Item = &ChunkOrderEntry> {
1356        self.chunk_order
1357            .iter()
1358            .filter(move |e| e.frame_index == frame_index)
1359    }
1360}