alpine-alpaca / asefile

Library for loading Aseprite files. Directly reads binary Aseprite files and does not require you to export files to JSON.
MIT License
43 stars 15 forks source link

Indexed color pixel access #10

Open KeyboardDanni opened 3 years ago

KeyboardDanni commented 3 years ago

Hi, thanks for making this library!

Right now the only way to get the image contents is in already-processed RGBA pixels (via the 'image' crate, which isn't exactly a lightweight dependency). Since Aseprite is a pixel art editor, and it already exports indexed-color PNGs, it would be nice to be able to get the indexed color pixels in this library too.

This is going to be used for an asset pipeline to an engine that knows how to load indexed-color images (and may do more with them in the future).

At the very least I'd like to have access to the raw source image data (width, height, color format, palette, pixel data as array of bytes). Unfortunately the raw form is currently not exposed to consumers of the library as it's marked with pub(crate). Could this be opened up? Not everyone wants to use the high-level image-driven interface.

Thanks in advance!

KeyboardDanni commented 3 years ago

Alternatively a new function could be added to get a paletted image using GrayImage from the image crate instead of an RgbaImage to store one byte per pixel but this would be a bit semantically weird.

alpine-alpaca commented 3 years ago

I've decided to depend on image because, well, we're producing images. To keep it small I disable most features. And the remaining png feature is actually only needed for the tests, so that can probably be removed as well.

Note that the interaction of indexed pixels and blend modes may produce images outside of the image palette. How would you handle that use case?

Regarding accessing the source data, would you want to access each Cel or only the blended images?

KeyboardDanni commented 3 years ago

Correct, blending results in colors outside the palette. But us pixel artists also rarely use blending anyway. When Aseprite exports a paletted image, it creates an indexed color PNG using the exact same palette colors as defined in the editor. Blending would have required that this palette be changed. Since this could modify the palette in unexpected ways, Aseprite just does the export with blending disabled.

Pixel art is very much about using deliberately chosen and arranged colors, so I would rather have the chosen palette preserved. Since blending is a feature that isn't really used in pixel art, I'd expect the blending to simply be ignored when producing a merged paletted image, and that this behavior be documented in the API. My take is that if someone wants to use blending they should be asking for an RgbaImage.

As for why Aseprite supports RGBA image editing, I imagine it's for situations where you want to put multiple images in one, like for a showcase (it can also be useful for doing color conversions to/from different palettes).

KeyboardDanni commented 3 years ago

As for source data, it'd be useful to be able to access individual cels. The idea is that I can store user data in special layers that aren't merged into the imported image. For example, Aseprite to my knowledge does not have a way to specify an origin point, but this is important in order to have accurate collision and nice looking H-flip in-game. So a specially-named layer could be used, where a pixel is placed to specify where this origin point should be located.

alpine-alpaca commented 3 years ago

Ok, so the problem is that the result type now depends on runtime data. An RgbaImage can handle all input data.

I'd prefer to have the image methods not to be able to fail. So, one option would be to put the expected pixel type in the AsepriteFile type and then fail on loading if the image has too many colors. I.e., something like:

pub struct AsepriteFile<Pixel=Rgba<u8>> { ... }

But that could make a few parts of the API and a lot of internal code quite messy.

Alternatively, we might have some "raw data" + some conversion utils.

pub fn image_data(&self) -> (Vec<u8>, ImageFormat, (usize, usize));

pub struct RawImage {
     pub data: Vec<u8>,
     pub format: ImageFormat,
     pub dimensions: (usize, usize)
}

pub enum ImageFormat {
     Rgba,  // 4 byte per pixel, data.len() == dimensions.0 * dimensions.1 * 4
     Indexed, // 1 byte per pixel
     // or possibly Indexed(Arc<Palette>)
     Greyscale, // 1 byte per pixel
}

impl RawImage {
     pub fn to_rgba(&self, palette: Option<&Palette>) -> Result<RgbaImage> { ... } // maybe
     pub fn index(&self, x: usize, y: usize) -> usize { ... } // maybe
}
KeyboardDanni commented 3 years ago

The ImageFormat and format-specific data is already stored inside the AsepriteFile object. I don't see why it should be statically typed. Especially since I'd like something that can handle both RGBA and indexed formats.

But it seems like the current processing/validation code turns the internal format into Rgba anyway. That would need to be changed.

I think functions to get the image as RGBA or indexed pixels would be good. I think RGBA could probably be done without needing a Result object, but indexed would definitely need it in the case that the source data isn't indexed. Having functions like this instead of using type parameters would also allow for color conversions too.

alpine-alpaca commented 3 years ago

Ok. In the meantime, you can use this utility function from 0.3: https://docs.rs/asefile/0.3.1/asefile/util/fn.to_indexed_image.html It's not the most efficient implementation, but it should do the job.

You need to enable features = ["utils"] to enable it.

Version 0.3 also adds access to user data and slices in case you're using that.

KeyboardDanni commented 2 years ago

So after a break I got back to work on my importing system. I looked at to_indexed_image and the issue is that it takes an already-processed RGBA image. If two palette entries share the same color and are both used in the image, this method won't preserve the original indexes.

For now I think I'll just convert images to RGBA and display a warning when this occurs.