Skip to content

Commit

Permalink
Improve documentation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
askeladdk committed Mar 11, 2023
1 parent 83c73b0 commit 45e8c2e
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 71 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@

## Overview

Package aseprite implements a decoder for Aseprite sprite files (`.ase` and `.aseprite` files).
Package aseprite implements a decoder for [Aseprite sprite files](https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md) (`.ase` and `.aseprite` files).

Layers are flattened, blending modes are applied, and frames are arranged on a single texture atlas. Invisible and reference layers are ignored. Tilesets and external files are not supported.
Layers are flattened, blending modes are applied, and frames are arranged on a single texture atlas. Invisible and reference layers are ignored.

Limitations:
- Tilemaps are not supported.
- External files are not supported.
- Old aseprite format is not supported.
- Color profiles are ignored.

## Install

Expand All @@ -23,7 +29,7 @@ Use `image.Decode` to decode an aseprite sprite file to an `image.Image`:
img, imgformat, err := image.Decode("test.aseprite")
```

This works fine when loading single images, but if the sprite contains multiple frames they will be organized as a texture atlas. In that case, type cast the image to a `aseprite.Aseprite` to access the metadata:
This is enough to decode single frame images. Multiple frames are arranged as a texture atlas in a single image. Type cast the image to `aseprite.Aseprite` to access the frame data, as well as other meta data extracted from the sprite file:

```go
if imgformat == "aseprite" {
Expand All @@ -34,6 +40,10 @@ if imgformat == "aseprite" {
}
```

Read the [documentation](https://pkg.go.dev/github.com/askeladdk/aseprite) for more information about what meta data is extracted.

## License

Package aseprite is released under the terms of the ISC license.

The internal blend package is released by Guillermo Estrada under the terms of the MIT license: http://github.com/phrozen/blend.
25 changes: 10 additions & 15 deletions aseprite.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

// LoopDirection enumerates all loop animation directions.
type LoopDirection int
type LoopDirection uint8

const (
Forward LoopDirection = 0
Expand All @@ -30,22 +30,17 @@ type Tag struct {
// Name is the name of the tag. Can be duplicate.
Name string

// Flags is a bitset of flags.
Flags int

// Repeat specifies how many times to repeat the animation.
Repeat int

// Lo is the first frame in the animation.
Lo int
Lo uint16

// Hi is the last frame in the animation.
Hi int
}
Hi uint16

// Repeat specifies how many times to repeat the animation.
Repeat uint16

// LoopDirection returns the looping direction of the animation.
func (t *Tag) LoopDirection() LoopDirection {
return LoopDirection(t.Flags & 3)
// LoopDirection is the looping direction of the animation.
LoopDirection LoopDirection
}

// Frame represents a single frame in the sprite.
Expand Down Expand Up @@ -73,7 +68,7 @@ type Slice struct {
// Pivot is the pivot point relative to Bounds.
Pivot image.Point

// Name is name of the slice. Can be duplicate.
// Name is the name of the slice. Can be duplicate.
Name string

// Data is optional user data.
Expand All @@ -83,7 +78,7 @@ type Slice struct {
Color color.Color
}

// Aseprite holds the results of a parsed .ase or .aseprite file.
// Aseprite holds the results of a parsed Aseprite image file.
type Aseprite struct {
// Image contains all frame images in a single image.
// Frame bounds specify where the frame images are located.
Expand Down
42 changes: 22 additions & 20 deletions decode.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package aseprite

import (
"encoding/binary"
"image"
"image/color"
"io"
)

// Decode reads a Aseprite image from r and returns it as an image.Image.
func Decode(r io.Reader) (image.Image, error) {
// DecodeAseprite decodes an Aseprite image from r.
func DecodeAseprite(r io.Reader) (*Aseprite, error) {
var spr Aseprite
if err := spr.readFrom(r); err != nil {
return nil, err
Expand All @@ -17,38 +16,41 @@ func Decode(r io.Reader) (image.Image, error) {
return &spr, nil
}

// Decode decodes an Aseprite image from r and returns it as an image.Image.
func Decode(r io.Reader) (image.Image, error) {
return DecodeAseprite(r)
}

// DecodeConfig returns the color model and dimensions of an Aseprite image
// without decoding the entire image.
func DecodeConfig(r io.Reader) (image.Config, error) {
var raw [14]byte
var f file

if _, err := io.ReadFull(r, raw[:]); err != nil {
if _, err := f.ReadFrom(r); err != nil {
return image.Config{}, err
}

if magic := binary.LittleEndian.Uint16(raw[4:]); magic != 0xA5E0 {
return image.Config{}, errInvalidMagic
fw, fh := factorPowerOfTwo(len(f.frames))
if f.framew > f.frameh {
fw, fh = fh, fw
}

nframes := int(binary.LittleEndian.Uint16(raw[6:]))
framew := int(binary.LittleEndian.Uint16(raw[8:]))
frameh := int(binary.LittleEndian.Uint16(raw[10:]))
bpp := binary.LittleEndian.Uint16(raw[12:])
var colorModel color.Model

colorModel := color.RGBAModel
if bpp == 16 {
switch f.bpp {
case 8:
f.initPalette()
colorModel = f.palette
case 16:
colorModel = color.Gray16Model
}

fw, fh := factorPowerOfTwo(nframes)
if framew > frameh {
fw, fh = fh, fw
default:
colorModel = color.RGBAModel
}

return image.Config{
ColorModel: colorModel,
Width: framew * fw,
Height: frameh * fh,
Width: f.framew * fw,
Height: f.frameh * fh,
}, nil
}

Expand Down
66 changes: 46 additions & 20 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,72 @@ import (
"image/png"
"os"
"testing"

"github.com/askeladdk/aseprite/internal/require"
)

func assertNoError(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
func equalPalette(a, b color.Palette) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

func TestDecode(t *testing.T) {
for _, tt := range []struct {
Name string
Filename string
Outfile string
Frames int
Tags int
}{
{
Name: "paletted",
Filename: "./testfiles/slime_paletted.aseprite",
Outfile: "slime_paletted.png",
Frames: 10,
Tags: 2,
},
{
Name: "grayscale",
Filename: "./testfiles/slime_grayscale.aseprite",
Outfile: "slime_grayscale.png",
Frames: 10,
Tags: 2,
},
{
Name: "blendtest",
Filename: "./testfiles/blendtest.aseprite",
Outfile: "blendtest.png",
Frames: 1,
Tags: 0,
},
} {
t.Run(tt.Name, func(t *testing.T) {
f, err := os.Open(tt.Filename)
assertNoError(t, err)
require.NoError(t, err)
defer f.Close()

img, imgformat, err := image.Decode(f)
assertNoError(t, err)
require.NoError(t, err)

if imgformat != "aseprite" {
t.Fatal(imgformat)
}

if _, ok := img.(*Aseprite); !ok {
t.Fatal()
}
aspr, ok := img.(*Aseprite)
require.True(t, ok)
require.True(t, len(aspr.Frames) == tt.Frames, "frames", len(aspr.Frames))
require.True(t, len(aspr.Tags) == tt.Tags, "tags", len(aspr.Tags))

out, err := os.Create(tt.Outfile)
assertNoError(t, err)
assertNoError(t, png.Encode(out, img))
require.NoError(t, err)
require.NoError(t, png.Encode(out, img))
})
}
}
Expand All @@ -69,9 +86,15 @@ func TestDecodeConfig(t *testing.T) {
Name: "paletted",
Filename: "./testfiles/slime_paletted.aseprite",
ImageConfig: image.Config{
ColorModel: color.RGBAModel,
Width: 128,
Height: 256,
ColorModel: &color.Palette{
0: color.Alpha16{0},
1: color.NRGBA{247, 231, 198, 255},
2: color.NRGBA{214, 142, 73, 255},
3: color.NRGBA{166, 55, 37, 255},
4: color.NRGBA{51, 30, 80, 255},
},
Width: 128,
Height: 256,
},
},
{
Expand All @@ -95,18 +118,21 @@ func TestDecodeConfig(t *testing.T) {
} {
t.Run(tt.Name, func(t *testing.T) {
f, err := os.Open(tt.Filename)
assertNoError(t, err)
require.NoError(t, err)
defer f.Close()

conf, imgformat, err := image.DecodeConfig(f)
assertNoError(t, err)
require.NoError(t, err)
require.True(t, imgformat == "aseprite", "image format", imgformat)

if imgformat != "aseprite" {
t.Fatal(imgformat)
}
require.True(t, conf.Height == tt.ImageConfig.Height, "height")
require.True(t, conf.Width == tt.ImageConfig.Width, "width")

if conf != tt.ImageConfig {
t.Fatal(conf)
if pal, ok := conf.ColorModel.(color.Palette); ok {
imgpal := *tt.ImageConfig.ColorModel.(*color.Palette)
require.True(t, equalPalette(pal, imgpal), "palette")
} else {
require.True(t, conf.ColorModel == tt.ImageConfig.ColorModel, "color model")
}
})
}
Expand Down
14 changes: 5 additions & 9 deletions internal/blend/blend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ import (
"os"
"strings"
"testing"
)

func assertNoError(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}
"github.com/askeladdk/aseprite/internal/require"
)

func jpgDecode(filename string) (image.Image, error) {
f, err := os.Open(filename)
Expand All @@ -35,10 +31,10 @@ func jpgEncode(filename string, img image.Image) error {

func TestBlendModes(t *testing.T) {
dst, err := jpgDecode("../../testfiles/dst.jpg")
assertNoError(t, err)
require.NoError(t, err)

src, err := jpgDecode("../../testfiles/src.jpg")
assertNoError(t, err)
require.NoError(t, err)

for i, name := range []string{
"Normal",
Expand All @@ -65,7 +61,7 @@ func TestBlendModes(t *testing.T) {
img := image.NewRGBA(src.Bounds())
Blend(img, img.Bounds(), src, image.Point{}, dst, image.Point{}, Modes[i])

assertNoError(t, jpgEncode(fmt.Sprintf("out_%s.jpg", strings.ToLower(name)), img))
require.NoError(t, jpgEncode(fmt.Sprintf("out_%s.jpg", strings.ToLower(name)), img))
})
}
}
17 changes: 17 additions & 0 deletions internal/require/require.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package require

import "testing"

func NoError(t *testing.T, err error) {
if err != nil {
t.Helper()
t.Fatal("unexpected error:", err)
}
}

func True(t *testing.T, test bool, args ...any) {
if !test {
t.Helper()
t.Fatal(args...)
}
}
8 changes: 4 additions & 4 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ func (f *file) initCels() error {
}

func parseTag(t *Tag, raw []byte) []byte {
t.Lo = int(binary.LittleEndian.Uint16(raw))
t.Hi = int(binary.LittleEndian.Uint16(raw[2:]))
t.Flags = int(raw[4])
t.Repeat = int(binary.LittleEndian.Uint16(raw[5:]))
t.Lo = binary.LittleEndian.Uint16(raw)
t.Hi = binary.LittleEndian.Uint16(raw[2:])
t.LoopDirection = LoopDirection(raw[4])
t.Repeat = binary.LittleEndian.Uint16(raw[5:])
t.Name = parseString(raw[17:])
return raw[19+len(t.Name):]
}
Expand Down

0 comments on commit 45e8c2e

Please sign in to comment.