mabel_aseprite/
tileset.rs

1use std::{collections::HashMap, error::Error, fmt, io::Read, sync::Arc};
2
3use crate::{
4    pixel::{Pixels, RawPixels},
5    AsepriteParseError, ColorPalette, PixelFormat, Result,
6};
7use bitflags::bitflags;
8use image::RgbaImage;
9
10use crate::{external_file::ExternalFileId, reader::AseReader};
11
12/// An id for a [Tileset].
13#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
14pub(crate) struct TilesetId(pub(crate) u32);
15
16impl TilesetId {
17    /// Create a new `TilesetId` from a raw `u32` value.
18    pub(crate) fn from_raw(value: u32) -> Self {
19        Self(value)
20    }
21
22    // Get the underlying `u32` value.
23    // pub(crate) fn value(&self) -> u32 {
24    //     self.0
25    // }
26}
27
28impl fmt::Display for TilesetId {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "TilesetId({})", self.0)
31    }
32}
33
34bitflags! {
35    struct TilesetFlags: u32 {
36        // Include link to external file.
37        const LINKS_EXTERNAL_FILE = 0x0001;
38        // Include tiles inside this file.
39        const FILE_INCLUDES_TILES = 0x0002;
40        // From the spec:
41        // Tilemaps using this tileset use tile ID=0 as empty tile
42        // (this is the new format). In rare cases this bit is off,
43        // the empty tile will be equal to 0xffffffff (used in
44        // internal versions of Aseprite).
45        const EMPTY_TILE_IS_ID_ZERO = 0x0004;
46    }
47}
48
49/// A [Tileset] reference to an [crate::ExternalFile].
50#[derive(Debug, Clone)]
51pub struct ExternalTilesetReference {
52    external_file_id: ExternalFileId,
53    tileset_id: u32,
54}
55
56impl ExternalTilesetReference {
57    /// The id of the [crate::ExternalFile].
58    pub fn external_file_id(&self) -> ExternalFileId {
59        self.external_file_id
60    }
61
62    /// The id of the [Tileset] in the [crate::ExternalFile].
63    pub fn tileset_id(&self) -> u32 {
64        self.tileset_id
65    }
66
67    fn parse<T: Read>(reader: &mut AseReader<T>) -> Result<Self> {
68        Ok(ExternalTilesetReference {
69            external_file_id: reader.dword().map(ExternalFileId::new)?,
70            tileset_id: reader.dword()?,
71        })
72    }
73}
74
75/// The size of a tile in pixels.
76#[derive(Debug, Clone, Copy)]
77pub struct TileSize {
78    width: u16,
79    height: u16,
80}
81
82impl From<TileSize> for (u32, u32) {
83    fn from(sz: TileSize) -> Self {
84        (sz.width as u32, sz.height as u32)
85    }
86}
87
88impl TileSize {
89    /// Tile width in pixels.
90    pub fn width(&self) -> u16 {
91        self.width
92    }
93
94    /// Tile height in pixels.
95    pub fn height(&self) -> u16 {
96        self.height
97    }
98
99    pub(crate) fn pixels_per_tile(&self) -> u32 {
100        self.width as u32 * self.height as u32
101    }
102}
103
104/// A set of tiles of the same size.
105///
106/// In the GUI, this is the collection of tiles that you build up in the side
107/// bar. Each tile has the same size and is identified by an Id.
108///
109/// See [official docs for tilemaps and tilesets](https://www.aseprite.org/docs/tilemap/)
110/// for details.
111#[derive(Debug)]
112pub struct Tileset<P = Pixels> {
113    pub(crate) id: u32,
114    pub(crate) empty_tile_is_id_zero: bool,
115    pub(crate) tile_count: u32,
116    pub(crate) tile_size: TileSize,
117    pub(crate) base_index: i16,
118    pub(crate) name: String,
119    pub(crate) external_file: Option<ExternalTilesetReference>,
120    pub(crate) pixels: Option<P>,
121}
122
123impl<P> Tileset<P> {
124    /// Tileset id.
125    pub fn id(&self) -> u32 {
126        self.id
127    }
128
129    /// From the Aseprite file spec:
130    /// When true, tilemaps using this tileset use tile ID=0 as empty tile.
131    /// In rare cases this is false, the empty tile will be equal to 0xffffffff (used in internal versions of Aseprite).
132    pub fn empty_tile_is_id_zero(&self) -> bool {
133        self.empty_tile_is_id_zero
134    }
135
136    /// Number of tiles.
137    pub fn tile_count(&self) -> u32 {
138        self.tile_count
139    }
140
141    /// Tile width and height.
142    pub fn tile_size(&self) -> TileSize {
143        self.tile_size
144    }
145
146    /// Number to show in the UI for the tile with index=0. Default is 1.
147    /// Only used for Aseprite UI purposes. Not used for data representation.
148    pub fn base_index(&self) -> i16 {
149        self.base_index
150    }
151
152    /// Tileset name. May not be unique among tilesets.
153    pub fn name(&self) -> &str {
154        &self.name
155    }
156
157    /// When non-empty, describes a link to an external file.
158    pub fn external_file(&self) -> Option<&ExternalTilesetReference> {
159        self.external_file.as_ref()
160    }
161}
162
163impl Tileset<RawPixels> {
164    pub(crate) fn parse_chunk(
165        data: &[u8],
166        pixel_format: PixelFormat,
167    ) -> Result<Tileset<RawPixels>> {
168        let mut reader = AseReader::new(data);
169        let id = reader.dword()?;
170        let flags = reader.dword().map(TilesetFlags::from_bits_truncate)?;
171        let empty_tile_is_id_zero = flags.contains(TilesetFlags::EMPTY_TILE_IS_ID_ZERO);
172        let tile_count = reader.dword()?;
173        let tile_width = reader.word()?;
174        let tile_height = reader.word()?;
175        let tile_size = TileSize {
176            width: tile_width,
177            height: tile_height,
178        };
179        let base_index = reader.short()?;
180        reader.skip_reserved(14)?;
181        let name = reader.string()?;
182
183        let external_file = {
184            if !flags.contains(TilesetFlags::LINKS_EXTERNAL_FILE) {
185                None
186            } else {
187                Some(ExternalTilesetReference::parse(&mut reader)?)
188            }
189        };
190        let pixels = {
191            if !flags.contains(TilesetFlags::FILE_INCLUDES_TILES) {
192                None
193            } else {
194                let _compressed_length = reader.dword()?;
195                let expected_pixel_count =
196                    (tile_count * (tile_height as u32) * (tile_width as u32)) as usize;
197                RawPixels::from_compressed(reader, pixel_format, expected_pixel_count).map(Some)?
198            }
199        };
200        Ok(Tileset {
201            id,
202            empty_tile_is_id_zero,
203            tile_count,
204            tile_size,
205            base_index,
206            name,
207            external_file,
208            pixels,
209        })
210    }
211}
212
213impl Tileset<Pixels> {
214    /// Get the image for the given tile.
215    pub fn tile_image(&self, tile_index: u32) -> RgbaImage {
216        assert!(tile_index < self.tile_count());
217        let width = self.tile_size.width() as u32;
218        let height = self.tile_size.height() as u32;
219        let pixels = self.pixels.as_ref().expect("No pixel data in tileset");
220        let pixels_per_tile = (width * height) as usize;
221        let start_ofs = tile_index as usize * pixels_per_tile;
222        let raw: Vec<u8> = pixels
223            .clone_as_image_rgba()
224            .iter()
225            .copied()
226            .skip(start_ofs)
227            .take(pixels_per_tile)
228            .flat_map(|pixel| pixel.0)
229            .collect();
230        RgbaImage::from_raw(width, height, raw).expect("Mismatched image size")
231    }
232
233    /// Collect all tiles into one long vertical image.
234    ///
235    /// The image has width equal to the tile width and height equal to
236    /// `tile_size().width() * tile_count()`.
237    pub fn image(&self) -> RgbaImage {
238        let width = self.tile_size.width() as u32;
239        let tile_height = self.tile_size.height() as u32;
240        let image_height = tile_height * self.tile_count;
241        let pixels = self.pixels.as_ref().expect("No pixel data in tileset");
242
243        let raw: Vec<u8> = pixels
244            .clone_as_image_rgba()
245            .iter()
246            .copied()
247            .flat_map(|pixel| pixel.0)
248            .collect();
249        RgbaImage::from_raw(width, image_height, raw).expect("Mismatched image size")
250    }
251}
252
253/// A map from tileset ids (`u32`) to [Tileset]s.
254#[derive(Debug)]
255pub struct TilesetsById<P = Pixels>(HashMap<TilesetId, Tileset<P>>);
256
257impl<P> TilesetsById<P> {
258    pub(crate) fn new() -> Self {
259        Self(HashMap::new())
260    }
261
262    pub(crate) fn add(&mut self, tileset: Tileset<P>) {
263        self.0.insert(TilesetId::from_raw(tileset.id), tileset);
264    }
265
266    /// Returns the number of entries in the tileset.
267    pub fn len(&self) -> u32 {
268        self.0.len() as u32
269    }
270
271    /// Returns `true` if the tileset is empty.
272    pub fn is_empty(&self) -> bool {
273        self.0.is_empty()
274    }
275
276    /// An iterator over all [Tileset] entries in arbitrary order.
277    pub fn iter(&self) -> impl Iterator<Item = &Tileset<P>> {
278        self.0.values()
279    }
280
281    /// Get a reference to a [Tileset] from an id, if the entry exists.
282    pub fn get(&self, id: u32) -> Option<&Tileset<P>> {
283        self.0.get(&TilesetId::from_raw(id))
284    }
285}
286
287impl TilesetsById<RawPixels> {
288    pub(crate) fn validate(
289        self,
290        pixel_format: &PixelFormat,
291        palette: Option<Arc<ColorPalette>>,
292    ) -> Result<TilesetsById<Pixels>> {
293        let mut result = HashMap::with_capacity(self.0.capacity());
294        for (id, tileset) in self.0.into_iter() {
295            // Validates that all Tilesets contain their own pixel data.
296            // External file references currently not supported.
297            let _ = tileset.pixels.as_ref().ok_or_else(|| {
298                AsepriteParseError::UnsupportedFeature(
299                    "Expected Tileset data to contain pixels. External file Tilesets not supported"
300                        .into(),
301                )
302            })?;
303
304            let pixels = tileset
305                .pixels
306                .unwrap()
307                .validate(palette.clone(), pixel_format, false)?;
308
309            result.insert(
310                id,
311                Tileset {
312                    pixels: Some(pixels),
313                    id: tileset.id,
314                    empty_tile_is_id_zero: tileset.empty_tile_is_id_zero,
315                    tile_count: tileset.tile_count,
316                    tile_size: tileset.tile_size,
317                    base_index: tileset.base_index,
318                    name: tileset.name,
319                    external_file: tileset.external_file,
320                },
321            );
322        }
323        Ok(TilesetsById(result))
324    }
325}
326
327/// An error occured while generating a tileset image.
328#[derive(Debug, Clone)]
329pub enum TilesetImageError {
330    /// No tileset was found for the given id.
331    MissingTilesetId(u32),
332    /// No pixel data contained in the tileset with the given id.
333    NoPixelsInTileset(u32),
334}
335
336impl fmt::Display for TilesetImageError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            TilesetImageError::MissingTilesetId(tileset_id) => {
340                write!(f, "No tileset found with id: {}", tileset_id)
341            }
342            TilesetImageError::NoPixelsInTileset(tileset_id) => {
343                write!(f, "No pixel data for tileset with id: {}", tileset_id)
344            }
345        }
346    }
347}
348
349impl Error for TilesetImageError {
350    fn source(&self) -> Option<&(dyn Error + 'static)> {
351        None
352    }
353}