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 { tileset_index: u32 },
53}
54
55/// Layer blend mode, matching Aseprite's blend mode list.
56#[non_exhaustive]
57#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58pub enum BlendMode {
59    Normal,
60    Multiply,
61    Screen,
62    Overlay,
63    Darken,
64    Lighten,
65    ColorDodge,
66    ColorBurn,
67    HardLight,
68    SoftLight,
69    Difference,
70    Exclusion,
71    Hue,
72    Saturation,
73    Color,
74    Luminosity,
75    Addition,
76    Subtract,
77    Divide,
78}
79
80impl BlendMode {
81    pub(crate) fn from_u16(v: u16) -> Self {
82        match v {
83            0 => Self::Normal, 1 => Self::Multiply, 2 => Self::Screen,
84            3 => Self::Overlay, 4 => Self::Darken, 5 => Self::Lighten,
85            6 => Self::ColorDodge, 7 => Self::ColorBurn, 8 => Self::HardLight,
86            9 => Self::SoftLight, 10 => Self::Difference, 11 => Self::Exclusion,
87            12 => Self::Hue, 13 => Self::Saturation, 14 => Self::Color,
88            15 => Self::Luminosity, 16 => Self::Addition, 17 => Self::Subtract,
89            18 => Self::Divide, _ => Self::Normal,
90        }
91    }
92
93    pub(crate) fn to_u16(self) -> u16 {
94        match self {
95            Self::Normal => 0, Self::Multiply => 1, Self::Screen => 2,
96            Self::Overlay => 3, Self::Darken => 4, Self::Lighten => 5,
97            Self::ColorDodge => 6, Self::ColorBurn => 7, Self::HardLight => 8,
98            Self::SoftLight => 9, Self::Difference => 10, Self::Exclusion => 11,
99            Self::Hue => 12, Self::Saturation => 13, Self::Color => 14,
100            Self::Luminosity => 15, Self::Addition => 16, Self::Subtract => 17,
101            Self::Divide => 18,
102        }
103    }
104}
105
106/// Animation loop direction for tags.
107#[non_exhaustive]
108#[derive(Copy, Clone, Debug, PartialEq, Eq)]
109pub enum LoopDirection {
110    Forward,
111    Reverse,
112    PingPong,
113    PingPongReverse,
114}
115
116impl LoopDirection {
117    pub(crate) fn from_u8(v: u8) -> Self {
118        match v {
119            0 => Self::Forward, 1 => Self::Reverse,
120            2 => Self::PingPong, 3 => Self::PingPongReverse,
121            _ => Self::Forward,
122        }
123    }
124
125    pub(crate) fn to_u8(self) -> u8 {
126        match self {
127            Self::Forward => 0, Self::Reverse => 1,
128            Self::PingPong => 2, Self::PingPongReverse => 3,
129        }
130    }
131}
132
133/// Color profile embedded in the file.
134#[non_exhaustive]
135#[derive(Clone, Debug, PartialEq)]
136pub enum ColorProfile {
137    None,
138    SRgb { flags: u16, gamma: u32 },
139    Icc { flags: u16, gamma: u32, data: Vec<u8> },
140}
141
142// --- Handles ---
143
144/// Handle to a non-group layer. Obtained from [`AsepriteFile::add_layer`] or [`AsepriteFile::layer_ref`].
145#[derive(Copy, Clone, Debug, PartialEq, Eq)]
146pub struct LayerRef(pub(crate) usize);
147
148/// Handle to a group layer. Obtained from [`AsepriteFile::add_group`] or [`AsepriteFile::group_ref`].
149#[derive(Copy, Clone, Debug, PartialEq, Eq)]
150pub struct GroupRef(pub(crate) usize);
151
152impl LayerRef {
153    /// Returns the underlying layer index.
154    pub fn index(&self) -> usize { self.0 }
155}
156
157impl GroupRef {
158    /// Returns the underlying layer index.
159    pub fn index(&self) -> usize { self.0 }
160}
161
162// --- Data structs ---
163
164/// Raw pixel data buffer. Format depends on the file's [`ColorMode`]: RGBA = 4 bytes/pixel, Grayscale = 2 bytes/pixel, Indexed = 1 byte/pixel.
165#[derive(Clone, Debug, PartialEq)]
166pub struct Pixels {
167    pub data: Vec<u8>,
168    pub width: u16,
169    pub height: u16,
170}
171
172impl Pixels {
173    /// Creates a new pixel buffer, validating that `data.len()` matches `width * height * color_mode.bytes_per_pixel()`.
174    pub fn new(data: Vec<u8>, width: u16, height: u16, color_mode: ColorMode) -> Result<Self, AsepriteError> {
175        let expected = width as usize * height as usize * color_mode.bytes_per_pixel();
176        if data.len() != expected {
177            return Err(AsepriteError::PixelSizeMismatch { expected, actual: data.len() });
178        }
179        Ok(Self { data, width, height })
180    }
181}
182
183/// An RGBA color with an optional name (used in palettes).
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct Color {
186    pub r: u8,
187    pub g: u8,
188    pub b: u8,
189    pub a: u8,
190    pub name: Option<String>,
191}
192
193/// Grid overlay settings.
194#[derive(Clone, Debug, PartialEq)]
195pub struct GridInfo {
196    pub x: i16,
197    pub y: i16,
198    pub width: u16,
199    pub height: u16,
200}
201
202impl Default for GridInfo {
203    fn default() -> Self {
204        Self { x: 0, y: 0, width: 16, height: 16 }
205    }
206}
207
208#[derive(Clone)]
209pub(crate) struct UnknownChunk {
210    pub frame_index: usize,
211    pub chunk_type: u16,
212    pub data: Vec<u8>,
213}
214
215#[derive(Clone, Debug)]
216pub(crate) struct ChunkOrderEntry {
217    pub frame_index: usize,
218    pub chunk_type: u16,
219    /// For cel chunks (0x2005), stores the layer index to identify which cel
220    pub layer_index: Option<usize>,
221}
222
223// --- Layer ---
224
225/// A layer in the file (normal, group, or tilemap).
226#[derive(Clone, Debug, PartialEq)]
227pub struct Layer {
228    pub name: String,
229    pub kind: LayerKind,
230    pub parent: Option<usize>,
231    pub opacity: u8,
232    pub blend_mode: BlendMode,
233    pub visible: bool,
234    pub editable: bool,
235    pub lock_movement: bool,
236    pub background: bool,
237    pub prefer_linked_cels: bool,
238    pub collapsed: bool,
239    pub reference_layer: bool,
240    pub user_data: Option<UserData>,
241}
242
243/// Options for creating a new layer.
244pub struct LayerOptions {
245    pub opacity: u8,
246    pub blend_mode: BlendMode,
247    pub visible: bool,
248    pub editable: bool,
249    pub lock_movement: bool,
250    pub background: bool,
251    pub collapsed: bool,
252    pub prefer_linked_cels: bool,
253    pub reference_layer: bool,
254}
255
256impl Default for LayerOptions {
257    fn default() -> Self {
258        Self {
259            opacity: 255, blend_mode: BlendMode::Normal,
260            visible: true, editable: true, lock_movement: false,
261            background: false, collapsed: false,
262            prefer_linked_cels: false, reference_layer: false,
263        }
264    }
265}
266
267// --- Frame ---
268
269/// A single animation frame.
270#[derive(Clone, Debug, PartialEq, Eq)]
271pub struct Frame {
272    pub duration_ms: u16,
273}
274
275// --- Tag ---
276
277/// An animation tag spanning a range of frames.
278#[derive(Clone, Debug, PartialEq)]
279pub struct Tag {
280    pub name: String,
281    pub from_frame: usize,
282    pub to_frame: usize,
283    pub direction: LoopDirection,
284    pub repeat: u16,
285    pub user_data: Option<UserData>,
286}
287
288// --- Cel ---
289
290/// A cel (the content of one layer in one frame).
291#[derive(Clone, Debug, PartialEq)]
292pub struct Cel {
293    pub kind: CelKind,
294    pub opacity: u8,
295    pub z_index: i16,
296    pub user_data: Option<UserData>,
297    pub extra: Option<CelExtra>,
298}
299
300/// The type and data of a cel.
301#[non_exhaustive]
302#[derive(Clone, Debug, PartialEq)]
303pub enum CelKind {
304    /// Uncompressed pixel data.
305    Raw { pixels: Pixels, x: i16, y: i16 },
306    /// Zlib-compressed pixel data. `original_compressed` preserves the original bytes for round-trip fidelity; `None` for programmatically created cels.
307    Compressed { pixels: Pixels, x: i16, y: i16, original_compressed: Option<Vec<u8>> },
308    /// References another frame's cel on the same layer. Use [`AsepriteFile::resolve_cel`] to follow the link.
309    Linked { source_frame: usize, x: i16, y: i16 },
310    /// Tilemap data referencing tiles by ID.
311    Tilemap {
312        width: u16,
313        height: u16,
314        bits_per_tile: u16,
315        tile_id_bitmask: u32,
316        x_flip_bitmask: u32,
317        y_flip_bitmask: u32,
318        d_flip_bitmask: u32,
319        tiles: Vec<u32>,
320        x: i16,
321        y: i16,
322        original_compressed: Option<Vec<u8>>,
323    },
324}
325
326/// Options for creating a cel with non-default opacity, position, or z-index.
327pub struct CelOptions {
328    pub pixels: Pixels,
329    pub x: i16,
330    pub y: i16,
331    pub opacity: u8,
332    pub z_index: i16,
333}
334
335impl Default for CelOptions {
336    fn default() -> Self {
337        Self {
338            pixels: Pixels { data: vec![], width: 0, height: 0 },
339            x: 0, y: 0, opacity: 255, z_index: 0,
340        }
341    }
342}
343
344// --- User Data ---
345
346/// User-defined metadata attached to layers, cels, tags, slices, or the sprite itself.
347#[derive(Clone, Debug, Default, PartialEq)]
348pub struct UserData {
349    pub text: Option<String>,
350    pub color: Option<Color>,
351    pub properties: Vec<PropertiesMap>,
352}
353
354/// A named map of typed properties within user data.
355#[derive(Clone, Debug, PartialEq)]
356pub struct PropertiesMap {
357    pub key: u32,
358    pub entries: Vec<(String, PropertyValue)>,
359}
360
361/// A typed value within a properties map.
362#[non_exhaustive]
363#[derive(Clone, Debug, PartialEq)]
364pub enum PropertyValue {
365    Bool(bool), Int8(i8), UInt8(u8), Int16(i16), UInt16(u16),
366    Int32(i32), UInt32(u32), Int64(i64), UInt64(u64),
367    Fixed(u32), Float(f32), Double(f64), String(String),
368    Point(i32, i32), Size(i32, i32), Rect(i32, i32, i32, i32),
369    Vector(Vec<PropertyValue>), Properties(Vec<(String, PropertyValue)>),
370    Uuid([u8; 16]),
371}
372
373// --- Slice ---
374
375/// A named rectangular region, optionally with nine-patch and pivot data.
376#[derive(Clone, Debug, PartialEq)]
377pub struct Slice {
378    pub name: String,
379    pub keys: Vec<SliceKey>,
380    pub has_nine_patch: bool,
381    pub has_pivot: bool,
382    pub user_data: Option<UserData>,
383}
384
385/// A slice's bounds at a specific frame.
386#[derive(Clone, Debug, PartialEq)]
387pub struct SliceKey {
388    pub frame: u32, pub x: i32, pub y: i32,
389    pub width: u32, pub height: u32,
390    pub nine_patch: Option<NinePatch>,
391    pub pivot: Option<(i32, i32)>,
392}
393
394/// Nine-patch (9-slice) center region within a slice.
395#[derive(Clone, Debug, PartialEq)]
396pub struct NinePatch {
397    pub center_x: i32, pub center_y: i32,
398    pub center_width: u32, pub center_height: u32,
399}
400
401// --- Cel Extra ---
402
403/// Extra precision bounds for a cel.
404#[derive(Clone, Debug, PartialEq)]
405pub struct CelExtra {
406    pub precise_x: u32, pub precise_y: u32,
407    pub width: u32, pub height: u32,
408}
409
410// --- Tileset ---
411
412/// Bitflags for tileset properties.
413#[derive(Copy, Clone, Debug, PartialEq, Eq)]
414pub struct TilesetFlags(pub u32);
415
416impl TilesetFlags {
417    /// Returns whether the tileset references an external file.
418    pub fn has_external_link(self) -> bool { self.0 & 1 != 0 }
419    /// Returns whether the tileset has embedded tile pixel data.
420    pub fn has_embedded_tiles(self) -> bool { self.0 & 2 != 0 }
421    /// Returns whether tile ID 0 represents an empty tile.
422    pub fn empty_tile_is_zero(self) -> bool { self.0 & 4 != 0 }
423}
424
425/// A tileset definition.
426#[derive(Clone, Debug, PartialEq)]
427pub struct Tileset {
428    pub id: u32,
429    pub flags: TilesetFlags,
430    pub name: String,
431    pub tile_count: u32,
432    pub tile_width: u16,
433    pub tile_height: u16,
434    pub base_index: i16,
435    pub data: TilesetData,
436    pub user_data: Option<UserData>,
437    pub tile_user_data: Vec<Option<UserData>>,
438}
439
440/// The pixel data source for a tileset.
441#[non_exhaustive]
442#[derive(Clone, Debug, PartialEq)]
443pub enum TilesetData {
444    Embedded { pixels: Vec<u8>, original_compressed: Option<Vec<u8>> },
445    External { external_file_id: u32, tileset_id_in_external: u32 },
446    Empty,
447}
448
449// --- External File ---
450
451/// A reference to an external file.
452#[derive(Clone, Debug, PartialEq)]
453pub struct ExternalFile {
454    pub id: u32,
455    pub file_type: ExternalFileType,
456    pub name: String,
457}
458
459/// The type of an external file reference.
460#[non_exhaustive]
461#[derive(Copy, Clone, Debug, PartialEq, Eq)]
462pub enum ExternalFileType {
463    Palette,
464    Tileset,
465    ExtensionProps,
466    ExtensionTileMgmt,
467}
468
469impl ExternalFileType {
470    pub(crate) fn from_u8(v: u8) -> Self {
471        match v {
472            0 => Self::Palette,
473            1 => Self::Tileset,
474            2 => Self::ExtensionProps,
475            3 => Self::ExtensionTileMgmt,
476            _ => Self::Palette,
477        }
478    }
479
480    pub(crate) fn to_u8(self) -> u8 {
481        match self {
482            Self::Palette => 0,
483            Self::Tileset => 1,
484            Self::ExtensionProps => 2,
485            Self::ExtensionTileMgmt => 3,
486        }
487    }
488}
489
490// --- Legacy types (read-only) ---
491
492/// A legacy mask chunk (deprecated in modern Aseprite, preserved for round-trip fidelity).
493#[derive(Clone, Debug, PartialEq)]
494pub struct LegacyMask {
495    pub x: i16,
496    pub y: i16,
497    pub width: u16,
498    pub height: u16,
499    pub name: String,
500    pub bitmap: Vec<u8>,
501}
502
503// --- The main file struct ---
504
505/// An Aseprite `.ase`/`.aseprite` file.
506///
507/// # Creating a file from scratch
508///
509/// ```
510/// use aseprite::*;
511///
512/// let mut file = AsepriteFile::new(32, 32, ColorMode::Rgba);
513/// let layer = file.add_layer("Background");
514/// let frame = file.add_frame(100);
515/// let pixels = Pixels::new(vec![0u8; 32 * 32 * 4], 32, 32, ColorMode::Rgba).unwrap();
516/// file.set_cel(layer, frame, pixels, 0, 0).unwrap();
517/// ```
518///
519/// # Reading and writing
520///
521/// ```no_run
522/// use aseprite::AsepriteFile;
523///
524/// let data = std::fs::read("sprite.aseprite").unwrap();
525/// let file = AsepriteFile::from_reader(&data[..]).unwrap();
526/// let mut output = Vec::new();
527/// file.write_to(&mut output).unwrap();
528/// ```
529#[derive(Clone)]
530pub struct AsepriteFile {
531    width: u16,
532    height: u16,
533    color_mode: ColorMode,
534    flags: u32,
535    deprecated_speed: u16,
536    num_colors: u16,
537    transparent_index: u8,
538    pixel_ratio: (u8, u8),
539    grid: GridInfo,
540    color_profile: Option<ColorProfile>,
541    palette: Vec<Color>,
542    layers: Vec<Layer>,
543    frames: Vec<Frame>,
544    tags: Vec<Tag>,
545    slices: Vec<Slice>,
546    sprite_user_data: Option<UserData>,
547    cels: BTreeMap<(usize, usize), Cel>,
548    tilesets: Vec<Tileset>,
549    external_files: Vec<ExternalFile>,
550    legacy_masks: Vec<LegacyMask>,
551    pub(crate) unknown_chunks: Vec<UnknownChunk>,
552    pub(crate) chunk_order: Vec<ChunkOrderEntry>,
553}
554
555impl std::fmt::Debug for AsepriteFile {
556    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557        f.debug_struct("AsepriteFile")
558            .field("width", &self.width)
559            .field("height", &self.height)
560            .field("color_mode", &self.color_mode)
561            .field("layers", &self.layers.len())
562            .field("frames", &self.frames.len())
563            .field("tags", &self.tags.len())
564            .field("cels", &self.cels.len())
565            .finish()
566    }
567}
568
569impl AsepriteFile {
570    /// Creates an empty file with the given canvas dimensions and color mode.
571    pub fn new(width: u16, height: u16, color_mode: ColorMode) -> Self {
572        Self {
573            width, height, color_mode,
574            flags: 1, deprecated_speed: 0, num_colors: 0,
575            transparent_index: 0,
576            pixel_ratio: (1, 1), grid: GridInfo::default(),
577            color_profile: None, palette: Vec::new(),
578            layers: Vec::new(), frames: Vec::new(),
579            tags: Vec::new(), slices: Vec::new(),
580            sprite_user_data: None,
581            cels: BTreeMap::new(),
582            tilesets: Vec::new(),
583            external_files: Vec::new(),
584            legacy_masks: Vec::new(),
585            unknown_chunks: Vec::new(),
586            chunk_order: Vec::new(),
587        }
588    }
589
590    // --- Accessors ---
591    /// Returns the canvas width in pixels.
592    pub fn width(&self) -> u16 { self.width }
593    /// Returns the canvas height in pixels.
594    pub fn height(&self) -> u16 { self.height }
595    /// Returns the color mode.
596    pub fn color_mode(&self) -> ColorMode { self.color_mode }
597    /// Returns the raw header flags.
598    pub fn flags(&self) -> u32 { self.flags }
599    pub(crate) fn deprecated_speed(&self) -> u16 { self.deprecated_speed }
600    pub(crate) fn num_colors(&self) -> u16 { self.num_colors }
601    /// Returns the pixel aspect ratio as (width, height).
602    pub fn pixel_ratio(&self) -> (u8, u8) { self.pixel_ratio }
603    /// Returns the grid overlay settings.
604    pub fn grid(&self) -> &GridInfo { &self.grid }
605    /// Returns the color profile, if set.
606    pub fn color_profile(&self) -> &Option<ColorProfile> { &self.color_profile }
607    /// Returns the color palette (empty if no palette chunk was present).
608    pub fn palette(&self) -> &[Color] { &self.palette }
609    /// Returns all layers in document order.
610    pub fn layers(&self) -> &[Layer] { &self.layers }
611    /// Returns all frames.
612    pub fn frames(&self) -> &[Frame] { &self.frames }
613    /// Returns all animation tags.
614    pub fn tags(&self) -> &[Tag] { &self.tags }
615    /// Returns all slices.
616    pub fn slices(&self) -> &[Slice] { &self.slices }
617    /// Returns the sprite-level user data, if set.
618    pub fn sprite_user_data(&self) -> &Option<UserData> { &self.sprite_user_data }
619    /// Returns the transparent palette index (only meaningful for indexed color mode).
620    pub fn transparent_index(&self) -> u8 { self.transparent_index }
621    /// Returns legacy mask chunks (deprecated in modern Aseprite, preserved for round-trip fidelity).
622    pub fn legacy_masks(&self) -> &[LegacyMask] { &self.legacy_masks }
623    /// Returns all tilesets.
624    pub fn tilesets(&self) -> &[Tileset] { &self.tilesets }
625    /// Returns all external file references.
626    pub fn external_files(&self) -> &[ExternalFile] { &self.external_files }
627
628    /// 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.
629    pub fn cel(&self, layer: LayerRef, frame: usize) -> Option<&Cel> {
630        self.cels.get(&(layer.0, frame))
631    }
632
633    /// Returns the cel at the given layer and frame, following linked cels.
634    ///
635    /// Unlike [`cel()`](Self::cel), this never returns [`CelKind::Linked`].
636    /// Returns `None` if the cel doesn't exist or the link target is missing.
637    pub fn resolve_cel(&self, layer: LayerRef, frame: usize) -> Option<&Cel> {
638        let cel = self.cels.get(&(layer.0, frame))?;
639        match &cel.kind {
640            CelKind::Linked { source_frame, .. } => self.cels.get(&(layer.0, *source_frame)),
641            _ => Some(cel),
642        }
643    }
644
645    // --- Handle construction from parsed data ---
646    /// Returns a [`LayerRef`] handle for the given index, or `None` if the index is out of bounds or points to a group.
647    pub fn layer_ref(&self, index: usize) -> Option<LayerRef> {
648        self.layers.get(index).and_then(|l| {
649            match l.kind {
650                LayerKind::Normal | LayerKind::Tilemap { .. } => Some(LayerRef(index)),
651                LayerKind::Group => None,
652            }
653        })
654    }
655
656    /// Returns a [`GroupRef`] handle for the given index, or `None` if the index is out of bounds or is not a group.
657    pub fn group_ref(&self, index: usize) -> Option<GroupRef> {
658        self.layers.get(index).and_then(|l| {
659            if l.kind == LayerKind::Group { Some(GroupRef(index)) } else { None }
660        })
661    }
662
663    // --- Layers ---
664    fn push_layer(&mut self, name: &str, kind: LayerKind, parent: Option<usize>, opts: &LayerOptions) -> usize {
665        let index = self.layers.len();
666        self.layers.push(Layer {
667            name: name.to_string(), kind, parent,
668            opacity: opts.opacity, blend_mode: opts.blend_mode,
669            visible: opts.visible, editable: opts.editable,
670            lock_movement: opts.lock_movement, background: opts.background,
671            prefer_linked_cels: opts.prefer_linked_cels,
672            collapsed: opts.collapsed, reference_layer: opts.reference_layer,
673            user_data: None,
674        });
675        index
676    }
677
678    /// Adds a normal layer at the top level.
679    pub fn add_layer(&mut self, name: &str) -> LayerRef {
680        LayerRef(self.push_layer(name, LayerKind::Normal, None, &LayerOptions::default()))
681    }
682    /// Adds a normal layer at the top level with custom options.
683    pub fn add_layer_with(&mut self, name: &str, opts: LayerOptions) -> LayerRef {
684        LayerRef(self.push_layer(name, LayerKind::Normal, None, &opts))
685    }
686    /// Adds a group layer at the top level.
687    pub fn add_group(&mut self, name: &str) -> GroupRef {
688        GroupRef(self.push_layer(name, LayerKind::Group, None, &LayerOptions::default()))
689    }
690    /// Adds a group layer at the top level with custom options.
691    pub fn add_group_with(&mut self, name: &str, opts: LayerOptions) -> GroupRef {
692        GroupRef(self.push_layer(name, LayerKind::Group, None, &opts))
693    }
694    /// Adds a normal layer inside a group.
695    pub fn add_layer_in(&mut self, name: &str, parent: GroupRef) -> LayerRef {
696        LayerRef(self.push_layer(name, LayerKind::Normal, Some(parent.0), &LayerOptions::default()))
697    }
698    /// Adds a normal layer inside a group with custom options.
699    pub fn add_layer_in_with(&mut self, name: &str, parent: GroupRef, opts: LayerOptions) -> LayerRef {
700        LayerRef(self.push_layer(name, LayerKind::Normal, Some(parent.0), &opts))
701    }
702    /// Adds a nested group inside a parent group.
703    pub fn add_group_in(&mut self, name: &str, parent: GroupRef) -> GroupRef {
704        GroupRef(self.push_layer(name, LayerKind::Group, Some(parent.0), &LayerOptions::default()))
705    }
706    /// Adds a nested group inside a parent group with custom options.
707    pub fn add_group_in_with(&mut self, name: &str, parent: GroupRef, opts: LayerOptions) -> GroupRef {
708        GroupRef(self.push_layer(name, LayerKind::Group, Some(parent.0), &opts))
709    }
710
711    /// Adds a tilemap layer referencing the given tileset index.
712    pub fn add_tilemap_layer(&mut self, name: &str, tileset_index: u32) -> LayerRef {
713        let index = self.layers.len();
714        self.layers.push(Layer {
715            name: name.to_string(),
716            kind: LayerKind::Tilemap { tileset_index },
717            parent: None,
718            opacity: 255,
719            blend_mode: BlendMode::Normal,
720            visible: true,
721            editable: true,
722            lock_movement: false,
723            background: false,
724            prefer_linked_cels: false,
725            collapsed: false,
726            reference_layer: false,
727            user_data: None,
728        });
729        LayerRef(index)
730    }
731
732    /// Sets tilemap data for a tilemap layer/frame.
733    #[allow(clippy::too_many_arguments)]
734    pub fn set_tilemap_cel(
735        &mut self, layer: LayerRef, frame: usize,
736        tiles: Vec<u32>, width: u16, height: u16, x: i16, y: i16,
737    ) -> Result<(), AsepriteError> {
738        if frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(frame)); }
739        self.cels.insert((layer.0, frame), Cel {
740            kind: CelKind::Tilemap {
741                width, height, bits_per_tile: 32,
742                tile_id_bitmask: 0x1fff_ffff, x_flip_bitmask: 0x2000_0000,
743                y_flip_bitmask: 0x4000_0000, d_flip_bitmask: 0x8000_0000,
744                tiles, x, y, original_compressed: None,
745            },
746            opacity: 255, z_index: 0, user_data: None, extra: None,
747        });
748        Ok(())
749    }
750
751    // --- Frames ---
752    /// Adds a frame with the given duration in milliseconds. Returns the frame index.
753    pub fn add_frame(&mut self, duration_ms: u16) -> usize {
754        let index = self.frames.len();
755        self.frames.push(Frame { duration_ms });
756        index
757    }
758
759    // --- Cels ---
760    /// Sets compressed pixel data for a layer/frame. Returns an error if the frame index is out of bounds.
761    pub fn set_cel(&mut self, layer: LayerRef, frame: usize, pixels: Pixels, x: i16, y: i16) -> Result<(), AsepriteError> {
762        if frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(frame)); }
763        self.cels.insert((layer.0, frame), Cel {
764            kind: CelKind::Compressed { pixels, x, y, original_compressed: None },
765            opacity: 255, z_index: 0, user_data: None, extra: None,
766        });
767        Ok(())
768    }
769
770    /// Sets pixel data for a layer/frame with custom opacity, position, and z-index.
771    pub fn set_cel_with(&mut self, layer: LayerRef, frame: usize, opts: CelOptions) -> Result<(), AsepriteError> {
772        if frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(frame)); }
773        self.cels.insert((layer.0, frame), Cel {
774            kind: CelKind::Compressed { pixels: opts.pixels, x: opts.x, y: opts.y, original_compressed: None },
775            opacity: opts.opacity, z_index: opts.z_index, user_data: None, extra: None,
776        });
777        Ok(())
778    }
779
780    /// Sets uncompressed pixel data for a layer/frame.
781    pub fn set_raw_cel(&mut self, layer: LayerRef, frame: usize, pixels: Pixels, x: i16, y: i16) -> Result<(), AsepriteError> {
782        if frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(frame)); }
783        self.cels.insert((layer.0, frame), Cel {
784            kind: CelKind::Raw { pixels, x, y }, opacity: 255, z_index: 0,
785            user_data: None, extra: None,
786        });
787        Ok(())
788    }
789
790    /// Sets a linked cel pointing to another frame's cel. Returns an error if either frame index is out of bounds.
791    pub fn set_linked_cel(&mut self, layer: LayerRef, frame: usize, source_frame: usize) -> Result<(), AsepriteError> {
792        if frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(frame)); }
793        if source_frame >= self.frames.len() { return Err(AsepriteError::FrameOutOfBounds(source_frame)); }
794        self.cels.insert((layer.0, frame), Cel {
795            kind: CelKind::Linked { source_frame, x: 0, y: 0 }, opacity: 255, z_index: 0,
796            user_data: None, extra: None,
797        });
798        Ok(())
799    }
800
801    // --- Tags ---
802    /// Adds an animation tag spanning the given frame range.
803    pub fn add_tag(&mut self, name: &str, frames: RangeInclusive<usize>, direction: LoopDirection) -> Result<usize, AsepriteError> {
804        self.add_tag_with(name, frames, direction, 0)
805    }
806
807    /// Adds an animation tag with a custom repeat count.
808    pub fn add_tag_with(&mut self, name: &str, frames: RangeInclusive<usize>, direction: LoopDirection, repeat: u16) -> Result<usize, AsepriteError> {
809        let from = *frames.start();
810        let to = *frames.end();
811        if !self.frames.is_empty() && to >= self.frames.len() {
812            return Err(AsepriteError::InvalidFrameRange);
813        }
814        let index = self.tags.len();
815        self.tags.push(Tag { name: name.to_string(), from_frame: from, to_frame: to, direction, repeat, user_data: None });
816        Ok(index)
817    }
818
819    // --- Palette ---
820    /// Sets the color palette. Returns an error if more than 256 entries.
821    pub fn set_palette(&mut self, colors: &[Color]) -> Result<(), AsepriteError> {
822        if colors.len() > 256 {
823            return Err(AsepriteError::FormatLimitExceeded { field: "palette", value: colors.len(), max: 256 });
824        }
825        self.palette = colors.to_vec();
826        Ok(())
827    }
828
829    /// Sets the transparent palette index.
830    pub fn set_transparent_index(&mut self, index: u8) { self.transparent_index = index; }
831    /// Sets the color profile.
832    pub fn set_color_profile(&mut self, profile: ColorProfile) { self.color_profile = Some(profile); }
833
834    /// Adds a slice.
835    pub fn add_slice(&mut self, slice: Slice) { self.slices.push(slice); }
836    /// Sets the sprite-level user data.
837    pub fn set_sprite_user_data(&mut self, ud: UserData) { self.sprite_user_data = Some(ud); }
838    /// Adds a tileset definition.
839    pub fn add_tileset(&mut self, tileset: Tileset) { self.tilesets.push(tileset); }
840    /// Adds an external file reference.
841    pub fn add_external_file(&mut self, ef: ExternalFile) { self.external_files.push(ef); }
842
843    /// Sets user data on a layer.
844    pub fn set_layer_user_data(&mut self, layer: LayerRef, ud: UserData) {
845        if let Some(l) = self.layers.get_mut(layer.0) { l.user_data = Some(ud); }
846    }
847    /// Sets user data on a group layer.
848    pub fn set_group_user_data(&mut self, group: GroupRef, ud: UserData) {
849        if let Some(l) = self.layers.get_mut(group.0) { l.user_data = Some(ud); }
850    }
851    /// Sets user data on a cel.
852    pub fn set_cel_user_data(&mut self, layer: LayerRef, frame: usize, ud: UserData) {
853        if let Some(cel) = self.cels.get_mut(&(layer.0, frame)) { cel.user_data = Some(ud); }
854    }
855    /// Sets extra precision bounds on a cel.
856    pub fn set_cel_extra(&mut self, layer: LayerRef, frame: usize, extra: CelExtra) {
857        if let Some(cel) = self.cels.get_mut(&(layer.0, frame)) { cel.extra = Some(extra); }
858    }
859    /// Sets user data on a tag.
860    pub fn set_tag_user_data(&mut self, tag_index: usize, ud: UserData) {
861        if let Some(tag) = self.tags.get_mut(tag_index) { tag.user_data = Some(ud); }
862    }
863
864    // --- Internal setters for reader ---
865    pub(crate) fn set_flags(&mut self, flags: u32) { self.flags = flags; }
866    pub(crate) fn set_deprecated_speed(&mut self, speed: u16) { self.deprecated_speed = speed; }
867    pub(crate) fn set_num_colors(&mut self, n: u16) { self.num_colors = n; }
868    pub(crate) fn set_pixel_ratio(&mut self, ratio: (u8, u8)) { self.pixel_ratio = ratio; }
869    pub(crate) fn set_grid(&mut self, grid: GridInfo) { self.grid = grid; }
870    pub(crate) fn push_legacy_mask(&mut self, mask: LegacyMask) { self.legacy_masks.push(mask); }
871    pub(crate) fn push_unknown_chunk(&mut self, frame_index: usize, chunk_type: u16, data: Vec<u8>) {
872        self.unknown_chunks.push(UnknownChunk { frame_index, chunk_type, data });
873    }
874    pub(crate) fn push_tileset(&mut self, tileset: Tileset) { self.tilesets.push(tileset); }
875    pub(crate) fn push_external_file(&mut self, ef: ExternalFile) { self.external_files.push(ef); }
876    pub(crate) fn tilesets_mut(&mut self) -> &mut Vec<Tileset> { &mut self.tilesets }
877    pub(crate) fn push_layer_raw(&mut self, layer: Layer) { self.layers.push(layer); }
878    pub(crate) fn insert_cel(&mut self, layer_index: usize, frame_index: usize, cel: Cel) {
879        self.cels.insert((layer_index, frame_index), cel);
880    }
881    pub(crate) fn push_tag(&mut self, tag: Tag) { self.tags.push(tag); }
882    pub(crate) fn push_slice(&mut self, slice: Slice) { self.slices.push(slice); }
883    pub(crate) fn set_sprite_user_data_raw(&mut self, ud: UserData) { self.sprite_user_data = Some(ud); }
884    pub(crate) fn layers_mut(&mut self) -> &mut Vec<Layer> { &mut self.layers }
885    pub(crate) fn tags_mut(&mut self) -> &mut Vec<Tag> { &mut self.tags }
886    pub(crate) fn slices_mut(&mut self) -> &mut Vec<Slice> { &mut self.slices }
887    pub(crate) fn cel_mut(&mut self, layer_index: usize, frame_index: usize) -> Option<&mut Cel> {
888        self.cels.get_mut(&(layer_index, frame_index))
889    }
890    pub(crate) fn set_palette_entry(&mut self, index: usize, color: Color) {
891        if index >= self.palette.len() {
892            self.palette.resize(index + 1, Color { r: 0, g: 0, b: 0, a: 255, name: None });
893        }
894        self.palette[index] = color;
895    }
896    pub(crate) fn cels_iter(&self) -> impl Iterator<Item = (&(usize, usize), &Cel)> { self.cels.iter() }
897    pub(crate) fn unknown_chunks_for_frame(&self, frame_index: usize) -> impl Iterator<Item = &UnknownChunk> {
898        self.unknown_chunks.iter().filter(move |uc| uc.frame_index == frame_index)
899    }
900
901    pub(crate) fn push_chunk_order(&mut self, frame_index: usize, chunk_type: u16, layer_index: Option<usize>) {
902        self.chunk_order.push(ChunkOrderEntry { frame_index, chunk_type, layer_index });
903    }
904
905    pub(crate) fn chunk_order_for_frame(&self, frame_index: usize) -> impl Iterator<Item = &ChunkOrderEntry> {
906        self.chunk_order.iter().filter(move |e| e.frame_index == frame_index)
907    }
908}