use std::{collections::HashMap, error::Error, fmt, io::Read, sync::Arc};
use crate::{
pixel::{Pixels, RawPixels},
AsepriteParseError, ColorPalette, PixelFormat, Result,
};
use bitflags::bitflags;
use image::RgbaImage;
use crate::{external_file::ExternalFileId, reader::AseReader};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub(crate) struct TilesetId(pub(crate) u32);
impl TilesetId {
pub(crate) fn from_raw(value: u32) -> Self {
Self(value)
}
}
impl fmt::Display for TilesetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TilesetId({})", self.0)
}
}
bitflags! {
struct TilesetFlags: u32 {
const LINKS_EXTERNAL_FILE = 0x0001;
const FILE_INCLUDES_TILES = 0x0002;
const EMPTY_TILE_IS_ID_ZERO = 0x0004;
}
}
#[derive(Debug, Clone)]
pub struct ExternalTilesetReference {
external_file_id: ExternalFileId,
tileset_id: u32,
}
impl ExternalTilesetReference {
pub fn external_file_id(&self) -> ExternalFileId {
self.external_file_id
}
pub fn tileset_id(&self) -> u32 {
self.tileset_id
}
fn parse<T: Read>(reader: &mut AseReader<T>) -> Result<Self> {
Ok(ExternalTilesetReference {
external_file_id: reader.dword().map(ExternalFileId::new)?,
tileset_id: reader.dword()?,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct TileSize {
width: u16,
height: u16,
}
impl From<TileSize> for (u32, u32) {
fn from(sz: TileSize) -> Self {
(sz.width as u32, sz.height as u32)
}
}
impl TileSize {
pub fn width(&self) -> u16 {
self.width
}
pub fn height(&self) -> u16 {
self.height
}
pub(crate) fn pixels_per_tile(&self) -> u32 {
self.width as u32 * self.height as u32
}
}
#[derive(Debug)]
pub struct Tileset<P = Pixels> {
pub(crate) id: u32,
pub(crate) empty_tile_is_id_zero: bool,
pub(crate) tile_count: u32,
pub(crate) tile_size: TileSize,
pub(crate) base_index: i16,
pub(crate) name: String,
pub(crate) external_file: Option<ExternalTilesetReference>,
pub(crate) pixels: Option<P>,
}
impl<P> Tileset<P> {
pub fn id(&self) -> u32 {
self.id
}
pub fn empty_tile_is_id_zero(&self) -> bool {
self.empty_tile_is_id_zero
}
pub fn tile_count(&self) -> u32 {
self.tile_count
}
pub fn tile_size(&self) -> TileSize {
self.tile_size
}
pub fn base_index(&self) -> i16 {
self.base_index
}
pub fn name(&self) -> &str {
&self.name
}
pub fn external_file(&self) -> Option<&ExternalTilesetReference> {
self.external_file.as_ref()
}
}
impl Tileset<RawPixels> {
pub(crate) fn parse_chunk(
data: &[u8],
pixel_format: PixelFormat,
) -> Result<Tileset<RawPixels>> {
let mut reader = AseReader::new(data);
let id = reader.dword()?;
let flags = reader.dword().map(TilesetFlags::from_bits_truncate)?;
let empty_tile_is_id_zero = flags.contains(TilesetFlags::EMPTY_TILE_IS_ID_ZERO);
let tile_count = reader.dword()?;
let tile_width = reader.word()?;
let tile_height = reader.word()?;
let tile_size = TileSize {
width: tile_width,
height: tile_height,
};
let base_index = reader.short()?;
reader.skip_reserved(14)?;
let name = reader.string()?;
let external_file = {
if !flags.contains(TilesetFlags::LINKS_EXTERNAL_FILE) {
None
} else {
Some(ExternalTilesetReference::parse(&mut reader)?)
}
};
let pixels = {
if !flags.contains(TilesetFlags::FILE_INCLUDES_TILES) {
None
} else {
let _compressed_length = reader.dword()?;
let expected_pixel_count =
(tile_count * (tile_height as u32) * (tile_width as u32)) as usize;
RawPixels::from_compressed(reader, pixel_format, expected_pixel_count).map(Some)?
}
};
Ok(Tileset {
id,
empty_tile_is_id_zero,
tile_count,
tile_size,
base_index,
name,
external_file,
pixels,
})
}
}
impl Tileset<Pixels> {
pub fn tile_image(&self, tile_index: u32) -> RgbaImage {
assert!(tile_index < self.tile_count());
let width = self.tile_size.width() as u32;
let height = self.tile_size.height() as u32;
let pixels = self.pixels.as_ref().expect("No pixel data in tileset");
let pixels_per_tile = (width * height) as usize;
let start_ofs = tile_index as usize * pixels_per_tile;
let raw: Vec<u8> = pixels
.clone_as_image_rgba()
.iter()
.copied()
.skip(start_ofs)
.take(pixels_per_tile)
.flat_map(|pixel| pixel.0)
.collect();
RgbaImage::from_raw(width, height, raw).expect("Mismatched image size")
}
pub fn image(&self) -> RgbaImage {
let width = self.tile_size.width() as u32;
let tile_height = self.tile_size.height() as u32;
let image_height = tile_height * self.tile_count;
let pixels = self.pixels.as_ref().expect("No pixel data in tileset");
let raw: Vec<u8> = pixels
.clone_as_image_rgba()
.iter()
.copied()
.flat_map(|pixel| pixel.0)
.collect();
RgbaImage::from_raw(width, image_height, raw).expect("Mismatched image size")
}
}
#[derive(Debug)]
pub struct TilesetsById<P = Pixels>(HashMap<TilesetId, Tileset<P>>);
impl<P> TilesetsById<P> {
pub(crate) fn new() -> Self {
Self(HashMap::new())
}
pub(crate) fn add(&mut self, tileset: Tileset<P>) {
self.0.insert(TilesetId::from_raw(tileset.id), tileset);
}
pub fn len(&self) -> u32 {
self.0.len() as u32
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Tileset<P>> {
self.0.values()
}
pub fn get(&self, id: u32) -> Option<&Tileset<P>> {
self.0.get(&TilesetId::from_raw(id))
}
}
impl TilesetsById<RawPixels> {
pub(crate) fn validate(
self,
pixel_format: &PixelFormat,
palette: Option<Arc<ColorPalette>>,
) -> Result<TilesetsById<Pixels>> {
let mut result = HashMap::with_capacity(self.0.capacity());
for (id, tileset) in self.0.into_iter() {
let _ = tileset.pixels.as_ref().ok_or_else(|| {
AsepriteParseError::UnsupportedFeature(
"Expected Tileset data to contain pixels. External file Tilesets not supported"
.into(),
)
})?;
let pixels = tileset
.pixels
.unwrap()
.validate(palette.clone(), pixel_format, false)?;
result.insert(
id,
Tileset {
pixels: Some(pixels),
id: tileset.id,
empty_tile_is_id_zero: tileset.empty_tile_is_id_zero,
tile_count: tileset.tile_count,
tile_size: tileset.tile_size,
base_index: tileset.base_index,
name: tileset.name,
external_file: tileset.external_file,
},
);
}
Ok(TilesetsById(result))
}
}
#[derive(Debug, Clone)]
pub enum TilesetImageError {
MissingTilesetId(u32),
NoPixelsInTileset(u32),
}
impl fmt::Display for TilesetImageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TilesetImageError::MissingTilesetId(tileset_id) => {
write!(f, "No tileset found with id: {}", tileset_id)
}
TilesetImageError::NoPixelsInTileset(tileset_id) => {
write!(f, "No pixel data for tileset with id: {}", tileset_id)
}
}
}
}
impl Error for TilesetImageError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}