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#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
14pub(crate) struct TilesetId(pub(crate) u32);
15
16impl TilesetId {
17 pub(crate) fn from_raw(value: u32) -> Self {
19 Self(value)
20 }
21
22 }
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 const LINKS_EXTERNAL_FILE = 0x0001;
38 const FILE_INCLUDES_TILES = 0x0002;
40 const EMPTY_TILE_IS_ID_ZERO = 0x0004;
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct ExternalTilesetReference {
52 external_file_id: ExternalFileId,
53 tileset_id: u32,
54}
55
56impl ExternalTilesetReference {
57 pub fn external_file_id(&self) -> ExternalFileId {
59 self.external_file_id
60 }
61
62 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#[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 pub fn width(&self) -> u16 {
91 self.width
92 }
93
94 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#[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 pub fn id(&self) -> u32 {
126 self.id
127 }
128
129 pub fn empty_tile_is_id_zero(&self) -> bool {
133 self.empty_tile_is_id_zero
134 }
135
136 pub fn tile_count(&self) -> u32 {
138 self.tile_count
139 }
140
141 pub fn tile_size(&self) -> TileSize {
143 self.tile_size
144 }
145
146 pub fn base_index(&self) -> i16 {
149 self.base_index
150 }
151
152 pub fn name(&self) -> &str {
154 &self.name
155 }
156
157 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 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 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#[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 pub fn len(&self) -> u32 {
268 self.0.len() as u32
269 }
270
271 pub fn is_empty(&self) -> bool {
273 self.0.is_empty()
274 }
275
276 pub fn iter(&self) -> impl Iterator<Item = &Tileset<P>> {
278 self.0.values()
279 }
280
281 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 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#[derive(Debug, Clone)]
329pub enum TilesetImageError {
330 MissingTilesetId(u32),
332 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}