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
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
135 changes: 54 additions & 81 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},
};
use crate::{cel::Cel, *};
use cel::{CelContent, RawCel};
use image::{Pixel, Rgba, RgbaImage};
use image::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,16 @@ 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<RgbaImage> {
B-Reif marked this conversation as resolved.
Show resolved Hide resolved
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 color_profile(&self) -> Option<&ColorProfile> {
// self.color_profile.as_ref()
// }
Expand All @@ -233,29 +251,34 @@ 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function is called per-pixel it probably shouldn't have so many failure points. These first two should always be valid after the first invocation of the function. So, maybe we can make this a standalone function (i.e., no self argument) and pass in the palette reference and transparent color index as arguments. Then do the checks where you construct index resolver closure below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closure would still need to be constructed conditionally to avoid unwrapping stuff when the closure's not needed. I briefly tried a "closure factory" signature (e.g || -> |px| -> rgba) and that caused some lifetime issues. I decided to just have Pixels accept the optional arguments, and in the Indexed case, do the unwrapping, and then iterate.

.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) {
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 layer_is_background = self.layers[layer.id()].is_background();
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;
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();

let tileset_id = if let LayerType::Tilemap(tileset_id) = layer_type {
tileset_id
} else {
Expand All @@ -271,58 +294,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(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) {
Expand Down Expand Up @@ -428,11 +409,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 +421,7 @@ fn write_tilemap_cel_to_image(
cel_data: &CelData,
tilemap_data: &Tilemap,
tileset: &Tileset,
pixels: &[crate::pixel::Rgba],
pixels: Vec<image::Rgba<u8>>,
blend_mode: &BlendMode,
) {
let CelData { x, y, opacity, .. } = cel_data;
Expand All @@ -467,15 +444,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 +464,7 @@ fn write_raw_cel_to_image(
image: &mut RgbaImage,
cel_data: &CelData,
image_size: &ImageSize,
pixels: &[crate::pixel::Rgba],
pixels: Vec<image::Rgba<u8>>,
blend_mode: &BlendMode,
) {
let ImageSize { width, height } = image_size;
Expand All @@ -510,9 +485,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
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