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

Aseprite v1.3 file spec changes #2

Closed B-Reif closed 3 years ago

B-Reif commented 3 years ago

v1.3 File Spec Changes

This PR adds new features to support the upcoming changes to the Aseprite file spec.. This involves both additional features and refactors of existing code. I'll document changes in the order provided by the spec, and then append changes specific to the crate.

What's new

Tile data type

From the spec:

TILE: Tilemaps: Each tile can be a 8-bit (BYTE), 16-bit (WORD), or 32-bit (DWORD) value and there are masks related to the meaning of each bit.

The new Tile struct in tile.rs represents this type. Currently, Aseprite itself only supports 32-bit TILE values, so this PR only targets 32-bit values as well.

Tilemap layer type

The new Tilemap LayerType enum case stores a Tileset index (DWORD) value.

Tilemap cel

From the spec:

For cel type = 3 (Compressed Tilemap) WORD Width in number of tiles WORD Height in number of tiles WORD Bits per tile (at the moment it's always 32-bit per tile) DWORD Bitmask for tile ID (e.g. 0x1fffffff for 32-bit tiles) DWORD Bitmask for X flip DWORD Bitmask for Y flip DWORD Bitmask for 90CW rotation BYTE[10] Reserved TILE[] Row by row, from top to bottom tile by tile compressed with ZLIB method (see NOTE.3)

The new Tilemap struct stores this data. The Cel struct has been refactored to support a new Tilemap enum case.

External files chunk

From the spec:

A list of external files linked with this file. It might be used to reference external palettes or tilesets. DWORD Number of entries BYTE[8] Reserved (set to zero) For each entry DWORD Entry ID (this ID is referenced by tilesets or palettes) BYTE[8] Reserved (set to zero) STRING External file name

The new external_file module supports parsing these chunks and the AsepriteFile struct now exposes them. Aseprite itself currently has no way of creating .aseprite files with these chunks, so for now we have no test cases against this.

User data chunk changes

From the spec:

After the tags chunk, you can write one user data chunk for each tag. E.g. if there are 10 tags, you can then write 10 user data chunks one for each tag. ... In version 1.3 a sprite has associated user data, to consider this case there is an User Data Chunk at the first frame after the Palette Chunk.

Asefile currently just parses UserData chunks and discards them. To support these chunks properly, we'll need to also contextualize them (e.g. Sprite UserData, Tag UserData, etc). This PR does not include any changes around this.

Tileset chunk

Link to the spec

The new tileset module supports creating these chunks and exposing them as part of the exported AsepriteFile struct.

What's changed

AseReader

The new AseReader struct wraps over some common reading behaviors. This change touches most lines of 'input' code to use AseReader methods.

Pixel

The new pixel module supports mapping the various pixel formats of Aseprite to structs. This supports image logic for both Tilesets and Cels, which each contain pixel data.

Tests

Two new test cases test against RGBA and Indexed Tilemap files respectively. We can add more tests (and functionality) once Aseprite itself catches up to its file spec.

alpine-alpaca commented 3 years ago

Looking good (well, except for the build failure ;) ). Would be important to have a few test files, though. I haven't yet figured out how to install Aseprite 1.3 without disabling my existing 1.2 version.

B-Reif commented 3 years ago

Still a bit more to do to support External File and Tileset chunks. I'll be sure to include some test files.

B-Reif commented 3 years ago

Tilesets can contain the actual image data (in the spec as PIXEL[]). This means some actual pixel-parsing logic needs to be refactored out of the Cel module and into some shared Pixel module.

Added a basic Tileset test, which currently fails because it doesn't parse pixel data correctly.

B-Reif commented 3 years ago

I don't exactly understand how to parse a Tilemap cel. It has a list of 'tiles', and a 'tile' is a value. I'm not really sure how to interpret the Tile value to get pixel data. do you have any insight on this? @alpine-alpaca

alpine-alpaca commented 3 years ago

I don't exactly understand how to parse a Tilemap cel. It has a list of 'tiles', and a 'tile' is a value. I'm not really sure how to interpret the Tile value to get pixel data. do you have any insight on this? @alpine-alpaca

My guess is that the tile size divides the cel into regions of tile width x tile height. And then the image data to be used is indicated by the tile id (ie. TILE). And the actual data is stored in the Tileset chunk, which stores its data as an image of width tile width and height (tiles x tile height). So the cel data is just an array of ids. and it should be be of size (ceil(image width / tile width) * ceil(image height / tile height).

B-Reif commented 3 years ago

Okay, I've marked this as Ready and updated the OP with a quick tour of the PR. Tests are passing, including the new Tilemap tests.

B-Reif commented 3 years ago

Ok, I've changed all of the panics in functions that already return Result. We can either refactor/extend the validation logic upstream, or we can change the file module functions to return a Result<()>.

B-Reif commented 3 years ago

Pushed a commit to return Result<()> from .image methods. We can revert this for something else if we want to pursue a different strategy.

EDIT: I see now that the idea is that validation happens during initial parsing so that the Image part can be guaranteed. I'll investigate moving these checks to validate then.

alpine-alpaca commented 3 years ago

Yes, in earlier versions I had image return a result, but then changed it because I felt it's difficult to deal with errors at that stage and it's much nicer to check the assumptions/invariants during parsing where errors must be expected anyway.

alpine-alpaca commented 3 years ago

We can either refactor/extend the validation logic upstream, or we can change the file module functions to return a Result<()>.

I'm not sure what you mean about upstream. Do you mean Aseprite? That wouldn't be enough. This library shouldn't make any assumptions without explicitly validating them. The file could be corrupted in arbitrary ways (intentionally or not) and this library should not crash (just loading the file should return an error). Before a version 1.0 I would like to extend the CI to do some fuzz testing to ensure that it is the case.

B-Reif commented 3 years ago

Sorry I was talking about refactoring in either validate() or in image(), that's all.

B-Reif commented 3 years ago

A few new changes:

B-Reif commented 3 years ago

oops didn't mean to press Close

alpine-alpaca commented 3 years ago

Ok, getting there. Some things I noticed:

B-Reif commented 3 years ago

Similarly, TilesetId probably needs to be exported. Not clear to me ATM how users are meant to construct them.

My thinking was that the underlying u32 would be abstracted away and users would only use these ids for lookup/comparison. I added a public constructor in case there's a need for it.

B-Reif commented 3 years ago

We might be able to further reduce the number of expect calls with some changes to the types, but they would require substantial refactoring and probably need a separate PR. The validate functions should work for now. Let me know if there's anything else you'd like changed.

alpine-alpaca commented 3 years ago

Yes, I think we can make things more robust by encapsulating the preconditions/invariants into types some more. A.k.a. "Parse, don't validate" or "Make illegal states unrepresentable".