diff --git a/Cargo.toml b/Cargo.toml index 7c4affd..affa6b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asefile" -version = "0.2.0" +version = "0.3.0" authors = ["Alponso "] edition = "2018" license = "MIT" diff --git a/README.md b/README.md index 77e2c45..2ba883d 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,8 @@ fn main() { The following features of Aseprite 1.2.25 are currently not supported: -- grayscale images - color profiles - # Bug compatibility - For indexed color files Aseprite supports blend modes, but ignores them when diff --git a/src/cel.rs b/src/cel.rs index 3a7ca5c..3cba98a 100644 --- a/src/cel.rs +++ b/src/cel.rs @@ -1,14 +1,13 @@ use crate::layer::{LayerData, LayerType}; -use crate::pixel::{self, Pixels}; +use crate::pixel::Pixels; use crate::reader::AseReader; use crate::tilemap::Tilemap; -use crate::{ - layer::LayersData, AsepriteFile, AsepriteParseError, ColorPalette, PixelFormat, Result, -}; +use crate::ColorPalette; +use crate::{layer::LayersData, AsepriteFile, AsepriteParseError, PixelFormat, Result}; use image::RgbaImage; +use std::fmt; use std::io::Read; -use std::{fmt, ops::DerefMut}; /// A reference to a single Cel. This contains the image data at a specific /// layer and frame. In the timeline view these are the dots. @@ -130,21 +129,26 @@ impl CelsData { } } - fn validate_cel(&self, frame: u32, layer_index: usize, layer: &LayerData) -> Result<()> { + fn validate_cel( + &self, + frame: u32, + layer_index: usize, + layer: &LayerData, + palette: Option<&ColorPalette>, + ) -> Result<()> { let by_layer = &self.data[frame as usize]; if let Some(ref cel) = by_layer[layer_index] { match &cel.content { - CelContent::Raw(image_content) => match image_content.pixels { + CelContent::Raw(image_content) => match &image_content.pixels { Pixels::Rgba(_) => {} - Pixels::Grayscale(_) => { - return Err(AsepriteParseError::UnsupportedFeature( - "Grayscale images not supported".into(), - )) - } - Pixels::Indexed(_) => { - return Err(AsepriteParseError::InvalidInput( - "Internal error: unresolved Indexed data".into(), - )); + Pixels::Grayscale(_) => {} + Pixels::Indexed(indexed_pixels) => { + let palette = palette.ok_or_else(|| { + AsepriteParseError::InvalidInput( + "No palette present for indexed pixel data".into(), + ) + })?; + palette.validate_indexed_pixels(indexed_pixels)?; } }, CelContent::Linked(other_frame) => { @@ -181,53 +185,12 @@ impl CelsData { Ok(()) } - // Turn indexed-color cels into rgba cels. - pub(crate) fn resolve_palette( - &mut self, - palette: &ColorPalette, - transparent_color_index: u8, - layer_info: &LayersData, - ) -> Result<()> { - let max_col = palette.num_colors(); - dbg!( - max_col, - transparent_color_index, - palette.color(0), - palette.color(1) - ); - for frame in 0..self.num_frames { - let layers = &mut self.data[frame as usize]; - for mut cel in layers { - if let Some(cel) = cel.deref_mut() { - if let CelContent::Raw(data) = &cel.content { - if let Pixels::Indexed(pixels) = &data.pixels { - let layer_index = cel.data.layer_index as u32; - let layer = &layer_info[layer_index]; - let layer_is_background = layer.is_background(); - let rgba_pixels = pixel::resolve_indexed_pixels( - pixels, - palette, - transparent_color_index, - layer_is_background, - )?; - cel.content = CelContent::Raw(ImageContent { - size: data.size, - pixels: Pixels::Rgba(rgba_pixels), - }) - } - } - } - } - } - Ok(()) - } - - pub fn validate(&self, layers_data: &LayersData) -> Result<()> { + pub fn validate(&self, layers_data: &LayersData, palette: Option<&ColorPalette>) -> Result<()> { for frame in 0..self.num_frames { let by_layer = &self.data[frame as usize]; for layer_index in 0..by_layer.len() { let layer = &layers_data[layer_index as u32]; - self.validate_cel(frame, layer_index, layer)?; + self.validate_cel(frame, layer_index, layer, palette)?; } } Ok(()) diff --git a/src/file.rs b/src/file.rs index e773d97..8c4a0eb 100644 --- a/src/file.rs +++ b/src/file.rs @@ -9,14 +9,13 @@ use crate::{ cel::{CelData, CelsData, ImageContent, ImageSize}, external_file::{ExternalFile, ExternalFileId, ExternalFilesById}, layer::{Layer, LayerType, LayersData}, - pixel::Grayscale, tile::TileId, tilemap::Tilemap, - tileset::{TileSize, Tileset, TilesetsById}, + tileset::{TileSize, Tileset, TilesetImageError, TilesetsById}, }; use crate::{cel::Cel, *}; use cel::{CelContent, RawCel}; -use image::{Pixel, Rgba, RgbaImage}; +use image::{Rgba, RgbaImage}; /// A parsed Aseprite file. #[derive(Debug)] @@ -65,6 +64,15 @@ impl PixelFormat { PixelFormat::Indexed { .. } => 1, } } + /// When Indexed, returns the index of the transparent color. + pub fn transparent_color_index(&self) -> Option { + match self { + PixelFormat::Indexed { + transparent_color_index, + } => Some(*transparent_color_index), + _ => None, + } + } } impl AsepriteFile { @@ -209,6 +217,30 @@ impl AsepriteFile { &self.tilesets } + /// Construct the image of each tile in the [Tileset]. + /// The image has width equal to the tile width and height equal to (tile_height * tile_count). + pub fn tileset_image( + &self, + tileset_id: &TilesetId, + ) -> std::result::Result { + let tileset = self + .tilesets + .get(tileset_id) + .ok_or_else(|| TilesetImageError::TilesetNotFound(*tileset_id))?; + let pixels = tileset + .pixels + .as_ref() + .ok_or_else(|| TilesetImageError::NoPixelsInTileset(*tileset_id))?; + let resolver_data = pixel::IndexResolverData { + palette: self.palette.as_ref(), + transparent_color_index: self.pixel_format.transparent_color_index(), + layer_is_background: false, + }; + let image_pixels = pixels.clone_as_image_rgba(resolver_data); + + Ok(tileset.write_to_image(image_pixels.as_ref())) + } + // pub fn color_profile(&self) -> Option<&ColorProfile> { // self.color_profile.as_ref() // } @@ -234,28 +266,23 @@ impl AsepriteFile { } fn write_cel(&self, image: &mut RgbaImage, cel: &RawCel) { - assert!(self.pixel_format != PixelFormat::Grayscale); let RawCel { data, content } = cel; let layer = self.layer(data.layer_index as u32); let blend_mode = layer.blend_mode(); + let resolver_data = pixel::IndexResolverData { + palette: self.palette.as_ref(), + transparent_color_index: self.pixel_format.transparent_color_index(), + layer_is_background: self.layers[layer.id()].is_background(), + }; match &content { CelContent::Raw(image_content) => { let ImageContent { size, pixels } = image_content; - match pixels { - pixel::Pixels::Rgba(pixels) => { - write_raw_cel_to_image(image, data, size, pixels, &blend_mode); - } - pixel::Pixels::Grayscale(_) => { - panic!("Grayscale cel. Should have been caught by CelsData::validate"); - } - pixel::Pixels::Indexed(_) => { - panic!("Indexed data cel. Should have been caught by CelsData::validate"); - } - } + let image_pixels = pixels.clone_as_image_rgba(resolver_data); + + write_raw_cel_to_image(image, data, size, image_pixels.as_ref(), &blend_mode); } CelContent::Tilemap(tilemap_data) => { let layer_type = layer.layer_type(); - let tileset_id = if let LayerType::Tilemap(tileset_id) = layer_type { tileset_id } else { @@ -271,58 +298,16 @@ impl AsepriteFile { .pixels .as_ref() .expect("Expected Tileset data to contain pixels. Should have been caught by TilesetsById::validate()"); - match tileset_pixels { - pixel::Pixels::Rgba(pixels) => write_tilemap_cel_to_image( - image, - data, - tilemap_data, - tileset, - pixels, - &blend_mode, - ), - pixel::Pixels::Grayscale(grayscale_pixels) => { - let pixels: Vec = - grayscale_pixels.iter().map(Grayscale::as_rgba).collect(); - write_tilemap_cel_to_image( - image, - data, - tilemap_data, - tileset, - &pixels, - &blend_mode, - ); - } - pixel::Pixels::Indexed(indexed_pixels) => { - let palette = self - .palette - .as_ref() - .expect("Expected a palette present when resolving indexed image. Should have been caught by TilesetsById::validate()"); - let transparent_color_index = if let PixelFormat::Indexed { - transparent_color_index, - } = self.pixel_format - { - transparent_color_index - } else { - panic!("Indexed tilemap pixels in non-indexed pixel format. Should have been caught by TilesetsById::validate()") - }; - let layer_is_background = self.layers[layer.id()].is_background(); - let pixels = crate::pixel::resolve_indexed_pixels( - indexed_pixels, - &palette, - transparent_color_index, - layer_is_background, - ) - .expect("Failed to resolve indexed pixels. Shoul have been caught by TilesetsById::validate()"); - write_tilemap_cel_to_image( - image, - data, - tilemap_data, - tileset, - &pixels, - &blend_mode, - ); - } - }; + let rgba_pixels = tileset_pixels.clone_as_image_rgba(resolver_data); + + write_tilemap_cel_to_image( + image, + data, + tilemap_data, + tileset, + rgba_pixels.as_ref(), + &blend_mode, + ); } CelContent::Linked(frame) => { if let Some(cel) = self.framedata.cel(*frame, data.layer_index) { @@ -428,11 +413,7 @@ fn blend_mode_to_blend_fn(mode: BlendMode) -> BlendFn { } } -fn tile_pixels<'a>( - pixels: &'a [pixel::Rgba], - tile_size: &TileSize, - tile_id: &TileId, -) -> &'a [pixel::Rgba] { +fn tile_slice<'a, T>(pixels: &'a [T], tile_size: &TileSize, tile_id: &TileId) -> &'a [T] { let pixels_per_tile = tile_size.pixels_per_tile() as usize; let start = pixels_per_tile * (tile_id.0 as usize); let end = start + pixels_per_tile; @@ -444,7 +425,7 @@ fn write_tilemap_cel_to_image( cel_data: &CelData, tilemap_data: &Tilemap, tileset: &Tileset, - pixels: &[crate::pixel::Rgba], + pixels: &[Rgba], blend_mode: &BlendMode, ) { let CelData { x, y, opacity, .. } = cel_data; @@ -467,15 +448,13 @@ fn write_tilemap_cel_to_image( let tilemap_tile_idx = (tile_x + (tile_y * tilemap_width)) as usize; let tile = &tiles[tilemap_tile_idx]; let tile_id = &tile.id; - let tile_pixels = tile_pixels(pixels, tile_size, tile_id); + let tile_pixels = tile_slice(&pixels, tile_size, tile_id); for pixel_y in 0..tile_height { for pixel_x in 0..tile_width { let pixel_idx = ((pixel_y * tile_width) + pixel_x) as usize; - let pixel = tile_pixels[pixel_idx]; + let image_pixel = tile_pixels[pixel_idx]; let image_x = ((tile_x * tile_width) + pixel_x + cel_x) as u32; let image_y = ((tile_y * tile_height) + pixel_y + cel_y) as u32; - let image_pixel = - Rgba::from_channels(pixel.red, pixel.green, pixel.blue, pixel.alpha); let src = *image.get_pixel(image_x, image_y); let new = blend_fn(src, image_pixel, *opacity); image.put_pixel(image_x, image_y, new); @@ -489,7 +468,7 @@ fn write_raw_cel_to_image( image: &mut RgbaImage, cel_data: &CelData, image_size: &ImageSize, - pixels: &[crate::pixel::Rgba], + pixels: &[Rgba], blend_mode: &BlendMode, ) { let ImageSize { width, height } = image_size; @@ -510,9 +489,7 @@ fn write_raw_cel_to_image( continue; } let idx = (y - y0) as usize * *width as usize + (x - x0) as usize; - let pixel = &pixels[idx]; - let image_pixel = Rgba::from_channels(pixel.red, pixel.green, pixel.blue, pixel.alpha); - + let image_pixel = pixels[idx]; let src = *image.get_pixel(x as u32, y as u32); let new = blend_fn(src, image_pixel, *opacity); image.put_pixel(x as u32, y as u32, new); diff --git a/src/lib.rs b/src/lib.rs index 8589877..235a553 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,4 +125,6 @@ pub use file::{AsepriteFile, Frame, LayersIter, PixelFormat}; pub use layer::{BlendMode, Layer, LayerFlags}; pub use palette::{ColorPalette, ColorPaletteEntry}; pub use tags::{AnimationDirection, Tag}; -pub use tileset::{ExternalTilesetReference, TileSize, Tileset, TilesetId, TilesetsById}; +pub use tileset::{ + ExternalTilesetReference, TileSize, Tileset, TilesetId, TilesetImageError, TilesetsById, +}; diff --git a/src/palette.rs b/src/palette.rs index e568ef6..b201ef6 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -1,4 +1,4 @@ -use crate::{reader::AseReader, AsepriteParseError, Result}; +use crate::{pixel, reader::AseReader, AsepriteParseError, Result}; use nohash::IntMap; /// The color palette embedded in the file. @@ -30,6 +30,20 @@ impl ColorPalette { pub fn color(&self, index: u32) -> Option<&ColorPaletteEntry> { self.entries.get(&index) } + + pub(crate) fn validate_indexed_pixels(&self, indexed_pixels: &[pixel::Indexed]) -> Result<()> { + for pixel in indexed_pixels { + let color = self.color(pixel.value().into()); + color.ok_or_else(|| { + AsepriteParseError::InvalidInput(format!( + "Index out of range: {} (max: {})", + pixel.value(), + self.num_colors() + )) + })?; + } + Ok(()) + } } impl ColorPaletteEntry { diff --git a/src/parse.rs b/src/parse.rs index 2fad654..cb01949 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -42,7 +42,7 @@ impl ParseInfo { layers.validate(&tilesets)?; let framedata = self.framedata; - framedata.validate(&layers)?; + framedata.validate(&layers, palette.as_ref())?; Ok(ValidatedParseInfo { layers, @@ -125,36 +125,6 @@ pub fn read_aseprite(input: R) -> Result { parse_frame(&mut reader, frame_id, pixel_format, &mut parse_info)?; } - let layers = parse_info - .layers - .as_ref() - .ok_or_else(|| AsepriteParseError::InvalidInput("No layers found".to_owned()))?; - - // println!("==== Layers ====\n{:#?}", layers); - // println!("{:#?}", parse_info.framedata); - - // println!("bytes: {}, size: {}x{}", size, width, height); - // println!("color_depth: {}, num_colors: {}", color_depth, num_colors); - - //println!("framedata: {:#?}", parse_info.framedata); - match pixel_format { - PixelFormat::Rgba => {} - PixelFormat::Grayscale => {} - PixelFormat::Indexed { - transparent_color_index, - } => { - if let Some(ref palette) = parse_info.palette { - parse_info - .framedata - .resolve_palette(palette, transparent_color_index, &layers)?; - } else { - return Err(AsepriteParseError::InvalidInput( - "Input file uses indexed color mode but does not contain a palette".into(), - )); - } - } - } - let ValidatedParseInfo { layers, tilesets, diff --git a/src/pixel.rs b/src/pixel.rs index 121788d..016567a 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -1,33 +1,21 @@ +use image::{Pixel, Rgba}; + use crate::{reader::AseReader, AsepriteParseError, ColorPalette, PixelFormat, Result}; -use std::io::Read; +use std::{borrow::Cow, io::Read}; // From Aseprite file spec: // PIXEL: One pixel, depending on the image pixel format: // Grayscale: BYTE[2], each pixel have 2 bytes in the order Value, Alpha. // Indexed: BYTE, Each pixel uses 1 byte (the index). // RGBA: BYTE[4], each pixel have 4 bytes in this order Red, Green, Blue, Alpha. -#[derive(Debug, Clone, Copy)] -pub(crate) struct Rgba { - pub red: u8, - pub green: u8, - pub blue: u8, - pub alpha: u8, -} -impl Rgba { - fn new(chunk: &[u8]) -> Result { - let mut reader = AseReader::new(chunk); - let red = reader.byte()?; - let green = reader.byte()?; - let blue = reader.byte()?; - let alpha = reader.byte()?; - Ok(Self { - red, - green, - blue, - alpha, - }) - } +fn read_rgba(chunk: &[u8]) -> Result> { + let mut reader = AseReader::new(chunk); + let red = reader.byte()?; + let green = reader.byte()?; + let blue = reader.byte()?; + let alpha = reader.byte()?; + Ok(Rgba::from_channels(red, green, blue, alpha)) } #[derive(Debug, Clone, Copy)] @@ -43,14 +31,10 @@ impl Grayscale { let alpha = reader.byte()?; Ok(Self { value, alpha }) } - pub(crate) fn as_rgba(&self) -> Rgba { + + pub(crate) fn into_rgba(self) -> Rgba { let Self { value, alpha } = self; - Rgba { - red: *value, - green: *value, - blue: *value, - alpha: *alpha, - } + Rgba::from_channels(value, value, value, alpha) } } @@ -67,7 +51,7 @@ impl Indexed { palette: &ColorPalette, transparent_color_index: u8, layer_is_background: bool, - ) -> Option { + ) -> Option> { let index = self.0; palette.color(index as u32).map(|c| { let alpha = if transparent_color_index == index && !layer_is_background { @@ -75,12 +59,7 @@ impl Indexed { } else { c.alpha() }; - Rgba { - red: c.red(), - green: c.green(), - blue: c.blue(), - alpha, - } + Rgba::from_channels(c.red(), c.green(), c.blue(), alpha) }) } } @@ -91,7 +70,7 @@ fn output_size(pixel_format: PixelFormat, expected_pixel_count: usize) -> usize #[derive(Debug)] pub(crate) enum Pixels { - Rgba(Vec), + Rgba(Vec>), Grayscale(Vec), Indexed(Vec), } @@ -118,7 +97,7 @@ impl Pixels { "Incorrect length of bytes for RGBA image data".to_string(), )); } - let pixels: Result> = bytes.chunks_exact(4).map(Rgba::new).collect(); + let pixels: Result> = bytes.chunks_exact(4).map(read_rgba).collect(); pixels.map(Self::Rgba) } } @@ -153,33 +132,40 @@ impl Pixels { Pixels::Indexed(v) => v.len(), } } -} -pub(crate) fn resolve_indexed( - pixel: &Indexed, - palette: &ColorPalette, - transparent_color_index: u8, - layer_is_background: bool, -) -> Result { - pixel - .as_rgba(palette, transparent_color_index, layer_is_background) - .ok_or_else(|| { - AsepriteParseError::InvalidInput(format!( - "Index out of range: {} (max: {})", - pixel.value(), - palette.num_colors() - )) - }) + // Returns a Borrowed Cow if the Pixels struct already contains Rgba pixels. + // Otherwise clones them to create an Owned Cow. + pub(crate) fn clone_as_image_rgba( + &self, + index_resolver_data: IndexResolverData<'_>, + ) -> Cow>> { + match self { + Pixels::Rgba(rgba) => Cow::Borrowed(rgba), + Pixels::Grayscale(grayscale) => { + Cow::Owned(grayscale.iter().map(|gs| gs.into_rgba()).collect()) + } + Pixels::Indexed(indexed) => { + let IndexResolverData { + palette, + transparent_color_index, + layer_is_background, + } = index_resolver_data; + let palette = palette.expect("Expected a palette when resolving indexed pixels. Should have been caught in validation"); + let transparent_color_index = transparent_color_index.expect( + "Indexed pixels in non-indexed pixel format. Should have been caught in validation", + ); + let resolver = |px: &Indexed| { + px.as_rgba(palette, transparent_color_index, layer_is_background) + .expect("Indexed pixel out of range. Should have been caught in validation") + }; + Cow::Owned(indexed.iter().map(resolver).collect()) + } + } + } } -pub(crate) fn resolve_indexed_pixels( - pixels: &[Indexed], - palette: &ColorPalette, - transparent_color_index: u8, - layer_is_background: bool, -) -> Result> { - pixels - .iter() - .map(|px| resolve_indexed(px, palette, transparent_color_index, layer_is_background)) - .collect() +pub(crate) struct IndexResolverData<'a> { + pub(crate) palette: Option<&'a ColorPalette>, + pub(crate) transparent_color_index: Option, + pub(crate) layer_is_background: bool, } diff --git a/src/tests.rs b/src/tests.rs index 0db87d0..dea2ce1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -316,6 +316,14 @@ fn indexed() { compare_with_reference_image(f.frame(0).image(), "indexed_01"); } +#[test] +fn grayscale() { + let f = load_test_file("grayscale"); + assert_eq!(f.size(), (64, 64)); + + compare_with_reference_image(f.frame(0).image(), "grayscale"); +} + #[test] fn palette() { let f = load_test_file("palette"); @@ -327,8 +335,8 @@ fn palette() { } #[test] -fn tileset() { - let f = load_test_file("tileset"); +fn tilemap() { + let f = load_test_file("tilemap"); let img = f.frame(0).image(); assert_eq!(f.size(), (32, 32)); let ts = f @@ -337,21 +345,47 @@ fn tileset() { .expect("No tileset found"); assert_eq!(ts.name(), "test_tileset"); - compare_with_reference_image(img, "tileset"); + compare_with_reference_image(img, "tilemap"); +} + +#[test] +fn tilemap_indexed() { + let f = load_test_file("tilemap_indexed"); + let img = f.frame(0).image(); + assert_eq!(f.size(), (32, 32)); + let ts = f + .tilesets() + .get(&tileset::TilesetId::new(0)) + .expect("No tileset found"); + assert_eq!(ts.name(), "test_tileset"); + + compare_with_reference_image(img, "tilemap_indexed"); } #[test] -fn tileset_indexed() { - let f = load_test_file("tileset_indexed"); +fn tilemap_grayscale() { + let f = load_test_file("tilemap_grayscale"); let img = f.frame(0).image(); assert_eq!(f.size(), (32, 32)); - let ts = &f + let ts = f .tilesets() .get(&tileset::TilesetId::new(0)) .expect("No tileset found"); assert_eq!(ts.name(), "test_tileset"); - compare_with_reference_image(img, "tileset_indexed"); + compare_with_reference_image(img, "tilemap_grayscale"); +} + +#[test] +fn tileset_export() { + let f = load_test_file("tileset"); + let tileset = f + .tilesets() + .get(&tileset::TilesetId::new(0)) + .expect("No tileset found"); + let img = f.tileset_image(tileset.id()).unwrap(); + + compare_with_reference_image(img, "tileset"); } /* diff --git a/src/tileset.rs b/src/tileset.rs index a52ce2f..2ddf693 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,7 +1,10 @@ -use crate::{external_file::ExternalFileId, reader::AseReader}; +use std::{collections::HashMap, fmt, io::Read}; + use crate::{pixel::Pixels, AsepriteParseError, ColorPalette, PixelFormat, Result}; use bitflags::bitflags; -use std::{collections::HashMap, io::Read}; +use image::{Rgba, RgbaImage}; + +use crate::{external_file::ExternalFileId, reader::AseReader}; /// An id for a [Tileset]. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] @@ -17,6 +20,11 @@ impl TilesetId { &self.0 } } +impl fmt::Display for TilesetId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TilesetId({})", self.0) + } +} bitflags! { struct TilesetFlags: u32 { @@ -134,6 +142,37 @@ impl Tileset { self.external_file.as_ref() } + pub(crate) fn write_to_image(&self, image_pixels: &[Rgba]) -> RgbaImage { + let Tileset { + tile_size, + tile_count, + .. + } = self; + let TileSize { width, height } = tile_size; + let tile_width = *width as u32; + let tile_height = *height as u32; + let pixels_per_tile = tile_size.pixels_per_tile() as u32; + let image_height = tile_count * tile_height; + let mut image = RgbaImage::new(tile_width, image_height); + for tile_idx in 0..*tile_count { + let pixel_idx_offset = tile_idx * pixels_per_tile; + // tile_y and tile_x are positions relative to the current tile. + for tile_y in 0..tile_height { + // pixel_y is the absolute y position of the pixel on the image. + let pixel_y = tile_y + (tile_idx * tile_height); + for tile_x in 0..tile_width { + let sub_index = (tile_y * tile_width) + tile_x; + let pixel_idx = sub_index + pixel_idx_offset; + let image_pixel = image_pixels[pixel_idx as usize]; + // Absolute pixel x is equal to tile_x. + image.put_pixel(tile_x, pixel_y, image_pixel); + } + } + } + + image + } + pub(crate) fn parse_chunk(data: &[u8], pixel_format: PixelFormat) -> Result { let mut reader = AseReader::new(data); let id = reader.dword().map(TilesetId)?; @@ -219,24 +258,13 @@ impl TilesetsById { })?; if let Pixels::Indexed(indexed_pixels) = pixels { - if let Some(palette) = palette { - // Validates that all indexed pixels are in the palette's range. - for pixel in indexed_pixels { - let color = palette.color(pixel.value().into()); - color.ok_or_else(|| { - AsepriteParseError::InvalidInput(format!( - "Index out of range: {} (max: {})", - pixel.value(), - palette.num_colors() - )) - })?; - } - } else { - // Validates that a palette is present if the Tileset is Indexed. - return Err(AsepriteParseError::InvalidInput( + let palette = palette.as_ref().ok_or_else(|| { + AsepriteParseError::InvalidInput( "Expected a palette present when resolving indexed image".into(), - )); - } + ) + })?; + palette.validate_indexed_pixels(indexed_pixels)?; + // Validates that the file PixelFormat is indexed if the Tileset is indexed. if let PixelFormat::Indexed { .. } = pixel_format { // Format matches tileset content, ok @@ -250,3 +278,24 @@ impl TilesetsById { Ok(()) } } + +/// An error occured while generating a tileset image. +#[derive(Debug)] +pub enum TilesetImageError { + /// No tileset was found for the provided id. + TilesetNotFound(TilesetId), + /// No pixel data is contained in the tileset with the provided id. + NoPixelsInTileset(TilesetId), +} +impl fmt::Display for TilesetImageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TilesetImageError::TilesetNotFound(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) + } + } + } +} diff --git a/tests/data/grayscale.aseprite b/tests/data/grayscale.aseprite new file mode 100644 index 0000000..6347aa7 Binary files /dev/null and b/tests/data/grayscale.aseprite differ diff --git a/tests/data/grayscale.png b/tests/data/grayscale.png new file mode 100644 index 0000000..9609975 Binary files /dev/null and b/tests/data/grayscale.png differ diff --git a/tests/data/tilemap.aseprite b/tests/data/tilemap.aseprite new file mode 100644 index 0000000..d4ffb5e Binary files /dev/null and b/tests/data/tilemap.aseprite differ diff --git a/tests/data/tilemap.png b/tests/data/tilemap.png new file mode 100644 index 0000000..e138bcf Binary files /dev/null and b/tests/data/tilemap.png differ diff --git a/tests/data/tilemap_grayscale.aseprite b/tests/data/tilemap_grayscale.aseprite new file mode 100644 index 0000000..4a09bbf Binary files /dev/null and b/tests/data/tilemap_grayscale.aseprite differ diff --git a/tests/data/tilemap_grayscale.png b/tests/data/tilemap_grayscale.png new file mode 100644 index 0000000..8f52c7b Binary files /dev/null and b/tests/data/tilemap_grayscale.png differ diff --git a/tests/data/tileset_indexed.aseprite b/tests/data/tilemap_indexed.aseprite similarity index 100% rename from tests/data/tileset_indexed.aseprite rename to tests/data/tilemap_indexed.aseprite diff --git a/tests/data/tileset_indexed.png b/tests/data/tilemap_indexed.png similarity index 100% rename from tests/data/tileset_indexed.png rename to tests/data/tilemap_indexed.png diff --git a/tests/data/tileset.png b/tests/data/tileset.png index e138bcf..459f47f 100644 Binary files a/tests/data/tileset.png and b/tests/data/tileset.png differ