Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tileset export feature #5

Closed
wants to merge 17 commits into from
Closed
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "asefile"
version = "0.2.0"
version = "0.3.0"
authors = ["Alponso <[email protected]>"]
edition = "2018"
license = "MIT"
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 22 additions & 59 deletions src/cel.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(())
Expand Down
141 changes: 59 additions & 82 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<u8> {
match self {
PixelFormat::Indexed {
transparent_color_index,
} => Some(*transparent_color_index),
_ => None,
}
}
}

impl AsepriteFile {
Expand Down Expand Up @@ -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<RgbaImage, TilesetImageError> {
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()
// }
Expand All @@ -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 {
Expand All @@ -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<pixel::Rgba> =
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) {
Expand Down Expand Up @@ -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;
Expand All @@ -444,7 +425,7 @@ fn write_tilemap_cel_to_image(
cel_data: &CelData,
tilemap_data: &Tilemap,
tileset: &Tileset,
pixels: &[crate::pixel::Rgba],
pixels: &[Rgba<u8>],
blend_mode: &BlendMode,
) {
let CelData { x, y, opacity, .. } = cel_data;
Expand All @@ -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);
Expand All @@ -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<u8>],
blend_mode: &BlendMode,
) {
let ImageSize { width, height } = image_size;
Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
16 changes: 15 additions & 1 deletion src/palette.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Loading