From 1c72789ca07e89ab21512a3bbe8c25a4c1b9f77f Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sun, 27 Jun 2021 13:25:04 -0700 Subject: [PATCH 01/16] remove empty file --- src/image.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/image.rs diff --git a/src/image.rs b/src/image.rs deleted file mode 100644 index e69de29..0000000 From 60ad51fc5726bd58da86bdebab97aef5d317a00d Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sun, 27 Jun 2021 14:05:11 -0700 Subject: [PATCH 02/16] add tileset image export function --- src/tileset.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/tileset.rs b/src/tileset.rs index ddcbda7..7b10d4c 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -3,8 +3,12 @@ use std::{ io::{Read, Seek}, }; -use crate::{pixel::Pixels, AsepriteParseError, ColorPalette, PixelFormat, Result}; +use crate::{ + pixel::{self, Pixels}, + AsepriteParseError, ColorPalette, PixelFormat, Result, +}; use bitflags::bitflags; +use image::{Pixel, RgbaImage}; use crate::{external_file::ExternalFileId, reader::AseReader}; @@ -126,6 +130,43 @@ impl Tileset { self.external_file.as_ref() } + /// 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 image(&self) -> RgbaImage { + let Tileset { + tile_size, + tile_count, + pixels, + .. + } = self; + let TileSize { + width: tile_width, + height: tile_height, + } = tile_size; + let image_height = tile_count * *tile_height as u32; + let mut image = RgbaImage::new(*tile_width as u32, image_height); + if let Some(pixel::Pixels::Rgba(rgba_pixels)) = pixels { + for tile_idx in 0..*tile_count { + for tile_x in 0..*tile_width { + for tile_y in 0..*tile_height { + let pixel_y = tile_y as u32 * tile_idx; + let pixel_idx = pixel_y + tile_x as u32; + let pixel = rgba_pixels[pixel_idx as usize]; + let image_pixel = image::Rgba::from_channels( + pixel.red, + pixel.green, + pixel.blue, + pixel.alpha, + ); + image.put_pixel(tile_x.into(), 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)?; From aedba15c6e75c27dc76832d03ade4af9c3643ec5 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Fri, 2 Jul 2021 19:05:49 -0700 Subject: [PATCH 03/16] fix tileset image, add test --- src/tests.rs | 26 +++++++++++----- src/tileset.rs | 28 +++++++++++------- tests/data/tilemap.aseprite | Bin 0 -> 2448 bytes tests/data/tilemap.png | Bin 0 -> 3021 bytes ...exed.aseprite => tilemap_indexed.aseprite} | Bin ...ileset_indexed.png => tilemap_indexed.png} | Bin tests/data/tileset.png | Bin 3021 -> 3165 bytes 7 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 tests/data/tilemap.aseprite create mode 100644 tests/data/tilemap.png rename tests/data/{tileset_indexed.aseprite => tilemap_indexed.aseprite} (100%) rename tests/data/{tileset_indexed.png => tilemap_indexed.png} (100%) diff --git a/src/tests.rs b/src/tests.rs index 0db87d0..aa37c58 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -327,8 +327,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 +337,33 @@ 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 tileset_indexed() { - let f = load_test_file("tileset_indexed"); +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 + 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_indexed"); +} + +#[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 = tileset.image(); + + compare_with_reference_image(img, "tileset"); } /* diff --git a/src/tileset.rs b/src/tileset.rs index 7b10d4c..26ff118 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -139,26 +139,32 @@ impl Tileset { pixels, .. } = self; - let TileSize { - width: tile_width, - height: tile_height, - } = tile_size; - let image_height = tile_count * *tile_height as u32; - let mut image = RgbaImage::new(*tile_width as u32, image_height); + 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); if let Some(pixel::Pixels::Rgba(rgba_pixels)) = pixels { for tile_idx in 0..*tile_count { - for tile_x in 0..*tile_width { - for tile_y in 0..*tile_height { - let pixel_y = tile_y as u32 * tile_idx; - let pixel_idx = pixel_y + tile_x as u32; + 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 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 pixel = rgba_pixels[pixel_idx as usize]; + dbg!((pixel_idx, tile_x, pixel_y)); let image_pixel = image::Rgba::from_channels( pixel.red, pixel.green, pixel.blue, pixel.alpha, ); - image.put_pixel(tile_x.into(), pixel_y, image_pixel); + // Absolute pixel x is equal to tile_x. + image.put_pixel(tile_x, pixel_y, image_pixel); } } } diff --git a/tests/data/tilemap.aseprite b/tests/data/tilemap.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..d4ffb5ef46c063b983c491c5fa494bb328e62c96 GIT binary patch literal 2448 zcmcImdpMNq7XRFHAGgYmF_R*dTuVcCb{g8HNOCE;n^CD;qZvw?j6{l1}cSEZuyUPIDpj=KTn|ekIXU@Kv1Bb z6|h#@BZ2MRH3O`Qi!c%s69cRcNf@bnItQ%P0V!a60~CQ>$xc1216Zd)7^!FBoqn_P#-8;}1c%8A-1+RG*6J@56Kp0= z5WZy2;`4v+VlGBgSSTHxX%FEN^tc>Y+N&DneBCD1RL*l!)V5q};!SbxdIx!8V*XsM z4^s5fcUX1hL9_z{DQ;o@$zwd*%JZBsuseq1th7U|N4v=TM#qzdw%??yW=qs-v{-A3t>)Rf z`!Z(W9<&KqEmJbkVxTl)1iGU!b^UQA5#hu`*>zksSwIiuA z{f{lY4*Mm?A81!%Ut0ZFt3&IB>pJwqq~ZQHtK`c!o|w*wex^{_bbS3tpOLHC zG2f&#G2_#1O8iOF4Z5`YsvtyX1j~CNy>!gAT9C~e+hqD~HD&WFx5^`TjvHx8rTTao z3yjVty|HC?zy>~y$93Kd-23F>WsPphz%IEf_-eD~26yyK-yIsS)p(TPB<{>66tv}+ zw85pYN8#>_8@ao15IHKlzTy<()aRFmeYUdQqbl)_dSv9w*QBhrIx&X8ES{J6Xk{nz z4W7K`Vw1NjD;-(T6XE)4cI_?iV*@A4Ubb{9%$7a8d*0@4aDcW-|TSR_a{#K*`g_(b#BLQi@#R zq)6o8M8dV=(fD@q7c&04=Mn2OG;7J&T*+%#WrqSZ&w~LY^TeDpNsPUIYvy#?l(SJR z6mPDm;I8v0QbaDsM-}6|6#J*jREN_vQK}jK;z`Njs8KR&fbn}cQ}N1IdZpc@m~Gk4 z7dzB-C;cc>U1DPrTydP3WvmAhE8xsU5?uJQ6|Z-?>3;FchFclr=yt3dQ{u{?_+ouN zLHd|Dugt{MaAW&#<04#9oM`Q3_#eeL%jyi@X?iFgk?8j$7o}dH+AHR)=};My=dQwC zB}ccbF^o|8)+Gm#TyU(|5!1MVIa8rhO( z;b9+QJJWK&)gfHrt4h@2_rAtUIGlpz;tnLDR)!+o8&GjEhIt4Zq>n(KJMZ78?j&En z*N>z+yLH}fo?=2dN_y{WYqPSmlDxaYLLG#v57Vd2wbfA;gc4(Uszv`d_#hzx1gTZ>rq`=B`57!JtwSapo!)b#P`N ztU}sOtKML<)L3m-zSFv6CUYt80Q4-8G^J z25^rE@&2OxiMFu}?jhW!+S`=Pydz%+P%Zszy{KMXyd`HoeXMP)mwOhsvzAQR#tTIi z?(a=wRox!y)Evzk8644Mo?vFBaW=PCHAhciBuy7Hhq9RYuVj|}7Fx>pbXO*d_sm|? zjr6&?Yzc8eVkKj4;;L8ny%i<&FeS>E@#c`TKVXK`FKGJ<5?j%u+M$kbf`r>Mnp^!x zO+t^Yc)e0|S-K)!<(?c?a*KC*G@+KTNU%izND}GS>B;oepUK49#KlC*gTiFSpd-EM zVUDK8)&)&GoDauq@wM;>J_3*ABk}9`KK?}+UWTW+#_k$NrCF&*b3Lh8>N#oz^%9jx zO{S8l+0=Y$u}0#lm9l(H<)73ab}cH9xZLK2)&8vp6-{ zUGW>Oryve)?_%wl)xDau5tND}#2``dToG_JfTSTOFtX2h2O*4v!9RgYAcP298^l(g p5GWCa5W;JO7Kk8#S^egzJ6X_UZOr21V+RMg1jKve*@&hKh*#L literal 0 HcmV?d00001 diff --git a/tests/data/tilemap.png b/tests/data/tilemap.png new file mode 100644 index 0000000000000000000000000000000000000000..e138bcf34d7e0a2f464ec93bf41d25f4bce36089 GIT binary patch literal 3021 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 D49_Ih literal 3021 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 P00000NkvXXu0mjfim Date: Fri, 2 Jul 2021 19:13:00 -0700 Subject: [PATCH 04/16] remove dbg, fix comment --- src/tileset.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tileset.rs b/src/tileset.rs index 5797b1e..5cde3f6 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -160,13 +160,12 @@ impl Tileset { 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 position of the pixel on the image. + // 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 pixel = rgba_pixels[pixel_idx as usize]; - dbg!((pixel_idx, tile_x, pixel_y)); let image_pixel = image::Rgba::from_channels( pixel.red, pixel.green, From 933e5fd3801cfb35d5819eb05445c006a204c2bf Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sun, 4 Jul 2021 13:33:39 -0700 Subject: [PATCH 05/16] update index resolution --- src/cel.rs | 81 +++++----------- src/file.rs | 135 +++++++++++--------------- src/palette.rs | 16 ++- src/parse.rs | 32 +----- src/pixel.rs | 59 +++++++---- src/tests.rs | 26 ++++- src/tileset.rs | 67 ++++--------- tests/data/grayscale.aseprite | Bin 0 -> 6419 bytes tests/data/grayscale.png | Bin 0 -> 5775 bytes tests/data/tilemap_grayscale.aseprite | Bin 0 -> 2546 bytes tests/data/tilemap_grayscale.png | Bin 0 -> 1554 bytes 11 files changed, 180 insertions(+), 236 deletions(-) create mode 100644 tests/data/grayscale.aseprite create mode 100644 tests/data/grayscale.png create mode 100644 tests/data/tilemap_grayscale.aseprite create mode 100644 tests/data/tilemap_grayscale.png 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..4ca836a 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}, }; use crate::{cel::Cel, *}; use cel::{CelContent, RawCel}; -use image::{Pixel, Rgba, RgbaImage}; +use image::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,24 @@ 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) -> Option { + let tileset = self.tilesets.get(tileset_id)?; + let palette = self.palette.as_ref(); + let layer_is_background = false; + let transparent_color_index = self.pixel_format.transparent_color_index(); + let index_resolver = |indexed_pixel: &pixel::Indexed| { + let palette = palette.expect("Expected a palette present when resolving indexed image. Should have been caught by TilesetsById::validate()"); + let transparent_color_index = transparent_color_index.expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught by TilesetsById::validate()"); + crate::pixel::resolve_indexed(*indexed_pixel, &palette, transparent_color_index, layer_is_background).expect("Failed to resolve indexed pixels. Shoul have been caught by TilesetsById::validate()") + }; + let pixels = tileset.pixels.as_ref()?; + + let image_pixels = pixels.clone_as_image_rgba(index_resolver); + Some(tileset.image(image_pixels)) + } + // pub fn color_profile(&self) -> Option<&ColorProfile> { // self.color_profile.as_ref() // } @@ -234,24 +260,22 @@ 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 palette = self.palette.as_ref(); + let layer_is_background = self.layers[layer.id()].is_background(); + let transparent_color_index = self.pixel_format.transparent_color_index(); + let index_resolver = |indexed_pixel: &pixel::Indexed| { + let palette = palette.expect("Expected a palette present when resolving indexed image. Should have been caught by TilesetsById::validate()"); + let transparent_color_index = transparent_color_index.expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught by TilesetsById::validate()"); + crate::pixel::resolve_indexed(*indexed_pixel, &palette, transparent_color_index, layer_is_background).expect("Failed to resolve indexed pixels. Shoul have been caught by TilesetsById::validate()") + }; 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(index_resolver); + write_raw_cel_to_image(image, data, size, image_pixels, &blend_mode); } CelContent::Tilemap(tilemap_data) => { let layer_type = layer.layer_type(); @@ -271,58 +295,17 @@ 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(index_resolver); + + write_tilemap_cel_to_image( + image, + data, + tilemap_data, + tileset, + rgba_pixels, + &blend_mode, + ); } CelContent::Linked(frame) => { if let Some(cel) = self.framedata.cel(*frame, data.layer_index) { @@ -428,11 +411,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 +423,7 @@ fn write_tilemap_cel_to_image( cel_data: &CelData, tilemap_data: &Tilemap, tileset: &Tileset, - pixels: &[crate::pixel::Rgba], + pixels: Vec>, blend_mode: &BlendMode, ) { let CelData { x, y, opacity, .. } = cel_data; @@ -467,15 +446,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 +466,7 @@ fn write_raw_cel_to_image( image: &mut RgbaImage, cel_data: &CelData, image_size: &ImageSize, - pixels: &[crate::pixel::Rgba], + pixels: Vec>, blend_mode: &BlendMode, ) { let ImageSize { width, height } = image_size; @@ -510,9 +487,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/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..c86dc65 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -1,3 +1,5 @@ +use image::Pixel; + use crate::{reader::AseReader, AsepriteParseError, ColorPalette, PixelFormat, Result}; use std::io::Read; @@ -28,6 +30,9 @@ impl Rgba { alpha, }) } + fn as_image_rgba(&self) -> image::Rgba { + image::Rgba::from_channels(self.red, self.green, self.blue, self.alpha) + } } #[derive(Debug, Clone, Copy)] @@ -43,13 +48,13 @@ impl Grayscale { let alpha = reader.byte()?; Ok(Self { value, alpha }) } - pub(crate) fn as_rgba(&self) -> Rgba { + pub(crate) fn to_rgba(self) -> Rgba { let Self { value, alpha } = self; Rgba { - red: *value, - green: *value, - blue: *value, - alpha: *alpha, + red: value, + green: value, + blue: value, + alpha, } } } @@ -153,10 +158,30 @@ impl Pixels { Pixels::Indexed(v) => v.len(), } } + + pub(crate) fn clone_as_image_rgba(&self, index_resolver: F) -> Vec> + where + F: Fn(&Indexed) -> Rgba, + { + match self { + Pixels::Rgba(rgba) => rgba + .iter() + .map(|px| image::Rgba::from_channels(px.red, px.green, px.blue, px.alpha)) + .collect(), + Pixels::Grayscale(grayscale) => grayscale + .iter() + .map(|gs| gs.to_rgba().as_image_rgba()) + .collect(), + Pixels::Indexed(indexed) => indexed + .iter() + .map(|idx| index_resolver(idx).as_image_rgba()) + .collect(), + } + } } pub(crate) fn resolve_indexed( - pixel: &Indexed, + pixel: Indexed, palette: &ColorPalette, transparent_color_index: u8, layer_is_background: bool, @@ -172,14 +197,14 @@ pub(crate) fn resolve_indexed( }) } -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) 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() +// } diff --git a/src/tests.rs b/src/tests.rs index aa37c58..d9cabe0 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"); @@ -354,14 +362,28 @@ fn tilemap_indexed() { compare_with_reference_image(img, "tilemap_indexed"); } +#[test] +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 + .tilesets() + .get(&tileset::TilesetId::new(0)) + .expect("No tileset found"); + assert_eq!(ts.name(), "test_tileset"); + + compare_with_reference_image(img, "tilemap_grayscale"); +} + #[test] fn tileset_export() { let f = load_test_file("tileset"); - let tileset = f + let img = f .tilesets() .get(&tileset::TilesetId::new(0)) + .and_then(|ts| f.tileset_image(ts.id())) .expect("No tileset found"); - let img = tileset.image(); compare_with_reference_image(img, "tileset"); } diff --git a/src/tileset.rs b/src/tileset.rs index 5cde3f6..afa4eae 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,11 +1,8 @@ use std::{collections::HashMap, io::Read}; -use crate::{ - pixel::{self, Pixels}, - AsepriteParseError, ColorPalette, PixelFormat, Result, -}; +use crate::{pixel::Pixels, AsepriteParseError, ColorPalette, PixelFormat, Result}; use bitflags::bitflags; -use image::{Pixel, RgbaImage}; +use image::RgbaImage; use crate::{external_file::ExternalFileId, reader::AseReader}; @@ -140,13 +137,10 @@ impl Tileset { self.external_file.as_ref() } - /// 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 image(&self) -> RgbaImage { + pub(crate) fn image(&self, image_pixels: Vec>) -> RgbaImage { let Tileset { tile_size, tile_count, - pixels, .. } = self; let TileSize { width, height } = tile_size; @@ -155,26 +149,18 @@ impl Tileset { 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); - if let Some(pixel::Pixels::Rgba(rgba_pixels)) = pixels { - 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 pixel = rgba_pixels[pixel_idx as usize]; - let image_pixel = image::Rgba::from_channels( - pixel.red, - pixel.green, - pixel.blue, - pixel.alpha, - ); - // Absolute pixel x is equal to tile_x. - image.put_pixel(tile_x, pixel_y, image_pixel); - } + 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); } } } @@ -267,24 +253,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 diff --git a/tests/data/grayscale.aseprite b/tests/data/grayscale.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..6347aa7601f3511537f3ac7fce4c4cbac838752e GIT binary patch literal 6419 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 Date: Sun, 4 Jul 2021 13:50:28 -0700 Subject: [PATCH 06/16] use image method --- src/pixel.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pixel.rs b/src/pixel.rs index c86dc65..35874a3 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -164,10 +164,7 @@ impl Pixels { F: Fn(&Indexed) -> Rgba, { match self { - Pixels::Rgba(rgba) => rgba - .iter() - .map(|px| image::Rgba::from_channels(px.red, px.green, px.blue, px.alpha)) - .collect(), + Pixels::Rgba(rgba) => rgba.iter().map(|px| px.as_image_rgba()).collect(), Pixels::Grayscale(grayscale) => grayscale .iter() .map(|gs| gs.to_rgba().as_image_rgba()) From 20cadaa91776906055bda03897542fb188740abb Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sun, 4 Jul 2021 17:14:40 -0700 Subject: [PATCH 07/16] cleanup some resolver logic --- src/file.rs | 31 +++++++++++++++---------------- src/pixel.rs | 29 ----------------------------- 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/file.rs b/src/file.rs index 4ca836a..b28945a 100644 --- a/src/file.rs +++ b/src/file.rs @@ -221,17 +221,9 @@ impl AsepriteFile { /// 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) -> Option { let tileset = self.tilesets.get(tileset_id)?; - let palette = self.palette.as_ref(); - let layer_is_background = false; - let transparent_color_index = self.pixel_format.transparent_color_index(); - let index_resolver = |indexed_pixel: &pixel::Indexed| { - let palette = palette.expect("Expected a palette present when resolving indexed image. Should have been caught by TilesetsById::validate()"); - let transparent_color_index = transparent_color_index.expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught by TilesetsById::validate()"); - crate::pixel::resolve_indexed(*indexed_pixel, &palette, transparent_color_index, layer_is_background).expect("Failed to resolve indexed pixels. Shoul have been caught by TilesetsById::validate()") - }; let pixels = tileset.pixels.as_ref()?; - let image_pixels = pixels.clone_as_image_rgba(index_resolver); + let image_pixels = pixels.clone_as_image_rgba(|px| self.resolve_indexed_pixel(false, px)); Some(tileset.image(image_pixels)) } @@ -259,18 +251,25 @@ impl AsepriteFile { image } + fn resolve_indexed_pixel( + &self, + layer_is_background: bool, + pixel: &pixel::Indexed, + ) -> pixel::Rgba { + let palette = self.palette.as_ref().expect("Expected a palette present when resolving indexed image. Should have been caught in validation"); + let transparent_color_index = self.pixel_format.transparent_color_index().expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught in validation"); + pixel + .as_rgba(palette, transparent_color_index, layer_is_background) + .expect("Indexed pixel out of range. Should have been caught in validation") + } + fn write_cel(&self, image: &mut RgbaImage, cel: &RawCel) { let RawCel { data, content } = cel; let layer = self.layer(data.layer_index as u32); let blend_mode = layer.blend_mode(); - let palette = self.palette.as_ref(); let layer_is_background = self.layers[layer.id()].is_background(); - let transparent_color_index = self.pixel_format.transparent_color_index(); - let index_resolver = |indexed_pixel: &pixel::Indexed| { - let palette = palette.expect("Expected a palette present when resolving indexed image. Should have been caught by TilesetsById::validate()"); - let transparent_color_index = transparent_color_index.expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught by TilesetsById::validate()"); - crate::pixel::resolve_indexed(*indexed_pixel, &palette, transparent_color_index, layer_is_background).expect("Failed to resolve indexed pixels. Shoul have been caught by TilesetsById::validate()") - }; + let index_resolver = + |px: &pixel::Indexed| self.resolve_indexed_pixel(layer_is_background, px); match &content { CelContent::Raw(image_content) => { let ImageContent { size, pixels } = image_content; diff --git a/src/pixel.rs b/src/pixel.rs index 35874a3..f70de75 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -176,32 +176,3 @@ impl Pixels { } } } - -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() - )) - }) -} - -// 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() -// } From ffe432b7b5a731049f29837ef664a69dffedde4a Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 5 Jul 2021 10:27:45 -0700 Subject: [PATCH 08/16] tweak line breaks --- src/file.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/file.rs b/src/file.rs index b28945a..ffef3a1 100644 --- a/src/file.rs +++ b/src/file.rs @@ -274,11 +274,11 @@ impl AsepriteFile { CelContent::Raw(image_content) => { let ImageContent { size, pixels } = image_content; let image_pixels = pixels.clone_as_image_rgba(index_resolver); + write_raw_cel_to_image(image, data, size, image_pixels, &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 { @@ -294,7 +294,6 @@ impl AsepriteFile { .pixels .as_ref() .expect("Expected Tileset data to contain pixels. Should have been caught by TilesetsById::validate()"); - let rgba_pixels = tileset_pixels.clone_as_image_rgba(index_resolver); write_tilemap_cel_to_image( From 14583fc157b2b32ac48347e468a4007d8a55c7db Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 5 Jul 2021 18:42:42 -0700 Subject: [PATCH 09/16] increment version number --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 8151afa90bea27175001de0cf4135c16ba623c56 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sat, 10 Jul 2021 16:13:01 -0700 Subject: [PATCH 10/16] refactor some review changes --- src/file.rs | 62 +++++++++++++++-------------- src/pixel.rs | 103 +++++++++++++++++++++++-------------------------- src/tests.rs | 4 +- src/tileset.rs | 30 ++++++++++++-- 4 files changed, 110 insertions(+), 89 deletions(-) diff --git a/src/file.rs b/src/file.rs index ffef3a1..751610d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -11,11 +11,11 @@ use crate::{ layer::{Layer, LayerType, LayersData}, tile::TileId, tilemap::Tilemap, - tileset::{TileSize, Tileset, TilesetsById}, + tileset::{TileSize, Tileset, TilesetImageError, TilesetsById}, }; use crate::{cel::Cel, *}; use cel::{CelContent, RawCel}; -use image::RgbaImage; +use image::{Rgba, RgbaImage}; /// A parsed Aseprite file. #[derive(Debug)] @@ -219,12 +219,26 @@ impl AsepriteFile { /// 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) -> Option { - let tileset = self.tilesets.get(tileset_id)?; - let pixels = tileset.pixels.as_ref()?; - - let image_pixels = pixels.clone_as_image_rgba(|px| self.resolve_indexed_pixel(false, px)); - Some(tileset.image(image_pixels)) + pub fn tileset_image( + &self, + tileset_id: &TilesetId, + ) -> std::result::Result { + let tileset = self + .tilesets + .get(tileset_id) + .ok_or_else(|| TilesetImageError::MissingTilesetId(*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> { @@ -251,31 +265,21 @@ impl AsepriteFile { image } - fn resolve_indexed_pixel( - &self, - layer_is_background: bool, - pixel: &pixel::Indexed, - ) -> pixel::Rgba { - let palette = self.palette.as_ref().expect("Expected a palette present when resolving indexed image. Should have been caught in validation"); - let transparent_color_index = self.pixel_format.transparent_color_index().expect("Indexed tilemap pixels in non-indexed pixel format. Should have been caught in validation"); - pixel - .as_rgba(palette, transparent_color_index, layer_is_background) - .expect("Indexed pixel out of range. Should have been caught in validation") - } - fn write_cel(&self, image: &mut RgbaImage, cel: &RawCel) { let RawCel { data, content } = cel; let layer = self.layer(data.layer_index as u32); let blend_mode = layer.blend_mode(); - let layer_is_background = self.layers[layer.id()].is_background(); - let index_resolver = - |px: &pixel::Indexed| self.resolve_indexed_pixel(layer_is_background, px); + 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; - let image_pixels = pixels.clone_as_image_rgba(index_resolver); + let image_pixels = pixels.clone_as_image_rgba(resolver_data); - write_raw_cel_to_image(image, data, size, image_pixels, &blend_mode); + write_raw_cel_to_image(image, data, size, image_pixels.as_ref(), &blend_mode); } CelContent::Tilemap(tilemap_data) => { let layer_type = layer.layer_type(); @@ -294,14 +298,14 @@ impl AsepriteFile { .pixels .as_ref() .expect("Expected Tileset data to contain pixels. Should have been caught by TilesetsById::validate()"); - let rgba_pixels = tileset_pixels.clone_as_image_rgba(index_resolver); + let rgba_pixels = tileset_pixels.clone_as_image_rgba(resolver_data); write_tilemap_cel_to_image( image, data, tilemap_data, tileset, - rgba_pixels, + rgba_pixels.as_ref(), &blend_mode, ); } @@ -421,7 +425,7 @@ fn write_tilemap_cel_to_image( cel_data: &CelData, tilemap_data: &Tilemap, tileset: &Tileset, - pixels: Vec>, + pixels: &[Rgba], blend_mode: &BlendMode, ) { let CelData { x, y, opacity, .. } = cel_data; @@ -464,7 +468,7 @@ fn write_raw_cel_to_image( image: &mut RgbaImage, cel_data: &CelData, image_size: &ImageSize, - pixels: Vec>, + pixels: &[Rgba], blend_mode: &BlendMode, ) { let ImageSize { width, height } = image_size; diff --git a/src/pixel.rs b/src/pixel.rs index f70de75..34feba1 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -1,38 +1,21 @@ -use image::Pixel; +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 as_image_rgba(&self) -> image::Rgba { - image::Rgba::from_channels(self.red, self.green, self.blue, self.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)] @@ -48,14 +31,10 @@ impl Grayscale { let alpha = reader.byte()?; Ok(Self { value, alpha }) } - pub(crate) fn to_rgba(self) -> Rgba { + + pub(crate) fn into_rgba(self) -> Rgba { let Self { value, alpha } = self; - Rgba { - red: value, - green: value, - blue: value, - alpha, - } + Rgba::from_channels(value, value, value, alpha) } } @@ -72,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 { @@ -80,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) }) } } @@ -96,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), } @@ -123,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) } } @@ -159,20 +133,39 @@ impl Pixels { } } - pub(crate) fn clone_as_image_rgba(&self, index_resolver: F) -> Vec> - where - F: Fn(&Indexed) -> Rgba, - { + // 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) => rgba.iter().map(|px| px.as_image_rgba()).collect(), - Pixels::Grayscale(grayscale) => grayscale - .iter() - .map(|gs| gs.to_rgba().as_image_rgba()) - .collect(), - Pixels::Indexed(indexed) => indexed - .iter() - .map(|idx| index_resolver(idx).as_image_rgba()) - .collect(), + 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 tilemap 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) 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 d9cabe0..dea2ce1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -379,11 +379,11 @@ fn tilemap_grayscale() { #[test] fn tileset_export() { let f = load_test_file("tileset"); - let img = f + let tileset = f .tilesets() .get(&tileset::TilesetId::new(0)) - .and_then(|ts| f.tileset_image(ts.id())) .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 afa4eae..c511b05 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, fmt, io::Read}; use crate::{pixel::Pixels, AsepriteParseError, ColorPalette, PixelFormat, Result}; use bitflags::bitflags; -use image::RgbaImage; +use image::{Rgba, RgbaImage}; use crate::{external_file::ExternalFileId, reader::AseReader}; @@ -20,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 { @@ -137,7 +142,7 @@ impl Tileset { self.external_file.as_ref() } - pub(crate) fn image(&self, image_pixels: Vec>) -> RgbaImage { + pub(crate) fn write_to_image(&self, image_pixels: &[Rgba]) -> RgbaImage { let Tileset { tile_size, tile_count, @@ -273,3 +278,22 @@ impl TilesetsById { Ok(()) } } + +/// An error occured while generating a tileset image. +#[derive(Debug)] +pub enum TilesetImageError { + MissingTilesetId(TilesetId), + NoPixelsInTileset(TilesetId), +} +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) + } + } + } +} From 1426743c2df495b4d2cb481fe9cc1e8fbacb92f1 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Sun, 11 Jul 2021 18:16:36 -0700 Subject: [PATCH 11/16] update README with grayscale feature --- README.md | 2 -- 1 file changed, 2 deletions(-) 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 From e9017bab6e8edf527efb915031fbc55fb6005298 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Thu, 15 Jul 2021 12:30:35 -0700 Subject: [PATCH 12/16] expose TilesetImageError publicly --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, +}; From 093de377f79a0fed23ac9bb454b681dde8e2cc3f Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 19 Jul 2021 11:47:57 -0500 Subject: [PATCH 13/16] tweak pixel error message --- src/pixel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pixel.rs b/src/pixel.rs index 34feba1..016567a 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -152,7 +152,7 @@ impl Pixels { } = 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 tilemap pixels in non-indexed pixel format. Should have been caught in validation", + "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) From 1ebfc1a206d24fe8f52fdeab0c42c5aebd8850d5 Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 19 Jul 2021 11:58:20 -0500 Subject: [PATCH 14/16] document enum cases for TilesetImageError --- src/tileset.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tileset.rs b/src/tileset.rs index c511b05..537d31f 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -282,7 +282,9 @@ impl TilesetsById { /// An error occured while generating a tileset image. #[derive(Debug)] pub enum TilesetImageError { + /// No tileset was found for the provided id. MissingTilesetId(TilesetId), + /// No pixel data is contained in the tileset with the provided id. NoPixelsInTileset(TilesetId), } impl fmt::Display for TilesetImageError { From 1eaddd1e017ea340d7aa7fd21748759b37d7ac6f Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 19 Jul 2021 11:59:03 -0500 Subject: [PATCH 15/16] rename MissingTilesetId enum --- src/file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file.rs b/src/file.rs index 751610d..8c4a0eb 100644 --- a/src/file.rs +++ b/src/file.rs @@ -226,7 +226,7 @@ impl AsepriteFile { let tileset = self .tilesets .get(tileset_id) - .ok_or_else(|| TilesetImageError::MissingTilesetId(*tileset_id))?; + .ok_or_else(|| TilesetImageError::TilesetNotFound(*tileset_id))?; let pixels = tileset .pixels .as_ref() From e8327ae408fbf099048d0b25c3d77e4be5a4b00a Mon Sep 17 00:00:00 2001 From: "Bruce Reif (Buswolley)" Date: Mon, 19 Jul 2021 12:00:45 -0500 Subject: [PATCH 16/16] update enum case name --- src/tileset.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tileset.rs b/src/tileset.rs index 537d31f..2ddf693 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -283,14 +283,14 @@ impl TilesetsById { #[derive(Debug)] pub enum TilesetImageError { /// No tileset was found for the provided id. - MissingTilesetId(TilesetId), + 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::MissingTilesetId(tileset_id) => { + TilesetImageError::TilesetNotFound(tileset_id) => { write!(f, "No tileset found with id: {}", tileset_id) } TilesetImageError::NoPixelsInTileset(tileset_id) => {