Files
aego/art/aseprite_encode_test.go

317 lines
9.4 KiB
Go

package art
import (
"bytes"
"compress/zlib"
"encoding/binary"
"errors"
"image"
"io"
"reflect"
"strings"
"testing"
)
// Check the wire header and decompressed cel independently of ImportAseprite;
// a self-consistent pair of incorrect codecs must not pass by itself.
func TestAsepriteEncoderWritesRealHeaderAndCelBytes(t *testing.T) {
for _, tc := range []struct {
mode Mode
depth uint16
paint Paint
pixel []byte
}{
{RGBA, 32, Paint{Color: Color{3, 14, 251, 127}}, []byte{3, 14, 251, 127}},
{Grayscale, 16, Paint{Color: Color{87, 87, 87, 199}}, []byte{87, 199}},
{Indexed, 8, Paint{UseIndex: true, Index: 3}, []byte{3}},
} {
t.Run(string(tc.mode), func(t *testing.T) {
d, err := New(2, 2, tc.mode, Limits{})
if err != nil {
t.Fatal(err)
}
defer d.Close()
cel := d.Info().Cels[0].ID
e := edit(t, d)
if err := e.SetPixel(cel, 0, 0, tc.paint, nil, false); err != nil {
t.Fatal(err)
}
done(t, e)
before := d.Info()
s, err := d.Snapshot()
if err != nil {
t.Fatal(err)
}
data, notes, err := s.EncodeAseprite()
if err != nil || len(notes) != 1 || notes[0].Code != "aego-identities" {
t.Fatalf("export: %+v %v", notes, err)
}
if !reflect.DeepEqual(before, d.Info()) {
t.Fatal("encoding changed the document")
}
if len(data) < 144 || binary.LittleEndian.Uint32(data) != uint32(len(data)) || binary.LittleEndian.Uint16(data[4:]) != 0xa5e0 || binary.LittleEndian.Uint16(data[12:]) != tc.depth || bytes.HasPrefix(data, []byte("PK")) {
t.Fatal("not an Aseprite header")
}
if binary.LittleEndian.Uint16(data[132:]) != 0xf1fa {
t.Fatal("not an Aseprite frame")
}
count := int(binary.LittleEndian.Uint32(data[140:]))
at, found := 144, false
for i := 0; i < count; i++ {
if at+6 > len(data) {
t.Fatal("truncated chunk")
}
n := int(binary.LittleEndian.Uint32(data[at:]))
if n < 6 || at+n > len(data) {
t.Fatal("invalid chunk size")
}
if binary.LittleEndian.Uint16(data[at+4:]) == 0x2005 {
b := data[at+6 : at+n]
if len(b) < 20 || binary.LittleEndian.Uint16(b[7:]) != 2 || binary.LittleEndian.Uint16(b[16:]) != 2 || binary.LittleEndian.Uint16(b[18:]) != 2 {
t.Fatal("not a compressed 2x2 cel")
}
z, err := zlib.NewReader(bytes.NewReader(b[20:]))
if err != nil {
t.Fatal(err)
}
raw, err := io.ReadAll(z)
if err != nil {
t.Fatal(err)
}
if err := z.Close(); err != nil {
t.Fatal(err)
}
if len(raw) != int(tc.depth/8)*4 || !bytes.Equal(raw[:len(tc.pixel)], tc.pixel) {
t.Fatalf("wrong raw pixels: %v", raw)
}
found = true
}
at += n
}
if !found || at != len(data) {
t.Fatal("cel missing or unexpected trailing bytes")
}
q, importNotes, err := ImportAseprite(data, Limits{})
if err != nil || len(importNotes) != 0 {
t.Fatalf("reimport: %+v %v", importNotes, err)
}
defer q.Close()
got, err := q.Pixel(q.Info().Cels[0].ID, 0, 0)
if err != nil || got != decode(d.current, d.current.Images[d.current.Cels[0].Image].at(0, 0)) {
t.Fatalf("reimport pixel: %+v %v", got, err)
}
})
}
}
func TestAsepriteEncoderLayersFramesLinksTagsAndStaticPivot(t *testing.T) {
d, layer, first, cel := doc(t, RGBA)
defer d.Close()
e := edit(t, d)
group, err := e.AddLayer("Group", Group, "")
if err != nil {
t.Fatal(err)
}
l := e.Info().Layers[0]
l.Parent, l.Blend, l.Opacity = group, BlendMultiply, 211
if err := e.SetLayer(l); err != nil {
t.Fatal(err)
}
if _, err := e.AddLayer("Foreground", Raster, ""); err != nil {
t.Fatal(err)
}
if err := e.SetPixel(cel, 1, 2, Paint{Color: red}, nil, false); err != nil {
t.Fatal(err)
}
frames := []ID{first}
for i := 1; i < 8; i++ {
frame, err := e.AddFrame(frames[i-1], 100+i*7)
if err != nil {
t.Fatal(err)
}
frames = append(frames, frame)
if i == 1 {
linked, err := e.CopyCel(cel, layer, frame, true)
if err != nil {
t.Fatal(err)
}
if err := e.SetCelPosition(linked, -1, 3); err != nil {
t.Fatal(err)
}
}
}
if _, err := e.SetTag(Tag{Name: "Walk", From: frames[0], To: frames[3], Direction: Forward}); err != nil {
t.Fatal(err)
}
if _, err := e.SetTag(Tag{Name: "Return", From: frames[4], To: frames[7], Direction: PingPong}); err != nil {
t.Fatal(err)
}
if _, err := e.SetSlice(Slice{Name: "pivot", Rect: image.Rect(2, 3, 10, 12), Pivot: image.Pt(6, 8)}); err != nil {
t.Fatal(err)
}
done(t, e)
s, err := d.Snapshot()
if err != nil {
t.Fatal(err)
}
data, notes, err := s.EncodeAseprite()
if err != nil || len(notes) != 1 {
t.Fatalf("export: %+v %v", notes, err)
}
q, importNotes, err := ImportAseprite(data, Limits{})
if err != nil || len(importNotes) != 0 {
t.Fatalf("reimport: %+v %v", importNotes, err)
}
defer q.Close()
info := q.Info()
if len(info.Layers) != 3 || info.Layers[0].Name != "Group" || info.Layers[1].Parent != info.Layers[0].ID || info.Layers[1].Blend != BlendMultiply || info.Layers[1].Opacity != 211 || info.Layers[2].Name != "Foreground" {
t.Fatalf("layer hierarchy: %+v", info.Layers)
}
if len(info.Frames) != 8 || len(info.Tags) != 2 || info.Tags[0].From != info.Frames[0].ID || info.Tags[1].To != info.Frames[7].ID || info.Tags[1].Direction != PingPong {
t.Fatalf("frames/tags: %+v %+v", info.Frames, info.Tags)
}
for i := 1; i < 8; i++ {
if info.Frames[i].DurationMS != 100+i*7 {
t.Fatalf("frame duration %d: %d", i, info.Frames[i].DurationMS)
}
}
if len(info.Cels) != 2 || info.Cels[0].Image != info.Cels[1].Image || info.Cels[1].X != -1 || info.Cels[1].Y != 3 {
t.Fatalf("linked cels: %+v", info.Cels)
}
if len(info.Slices) != 1 || info.Slices[0].Frame != "" || info.Slices[0].Rect != image.Rect(2, 3, 10, 12) || info.Slices[0].Pivot != image.Pt(6, 8) {
t.Fatalf("static slice: %+v", info.Slices)
}
for _, frame := range info.Frames {
if pivot, ok := info.FramePivot(frame.ID); !ok || pivot != image.Pt(6, 8) {
t.Fatalf("pivot not global on frame %s", frame.ID)
}
}
}
func TestAsepriteEncoderRejectsUnrepresentedDataWithoutMutatingSource(t *testing.T) {
for _, tc := range []struct {
name, diagnostic string
modify func(*testing.T, *Edit, ID, ID, ID)
}{
{"extra surface", "surface-channel", func(t *testing.T, e *Edit, l, f, _ ID) {
if _, err := e.EnsureCel(l, f, Normal); err != nil {
t.Fatal(err)
}
}},
{"alpha lock", "alpha-lock", func(t *testing.T, e *Edit, _, _, _ ID) {
l := e.Info().Layers[0]
l.AlphaLock = true
if err := e.SetLayer(l); err != nil {
t.Fatal(err)
}
}},
{"properties", "properties", func(_ *testing.T, e *Edit, _, _, _ ID) {
e.work.Properties["aego.example"] = "keep"
e.structural = true
}},
{"cross-layer link", "cross-layer-link", func(t *testing.T, e *Edit, _, f, c ID) {
l, err := e.AddLayer("Other", Raster, "")
if err != nil {
t.Fatal(err)
}
if _, err := e.CopyCel(c, l, f, true); err != nil {
t.Fatal(err)
}
}},
{"long duration", "frame-duration", func(t *testing.T, e *Edit, _, f, _ ID) {
if err := e.SetDuration(f, 70000); err != nil {
t.Fatal(err)
}
}},
{"frame slice", "slice-timing", func(t *testing.T, e *Edit, _, f, _ ID) {
if _, err := e.AddFrame(f, 100); err != nil {
t.Fatal(err)
}
if _, err := e.SetSlice(Slice{Name: "pivot", Frame: f, Rect: image.Rect(0, 0, 16, 16)}); err != nil {
t.Fatal(err)
}
}},
} {
t.Run(tc.name, func(t *testing.T) {
d, l, f, c := doc(t, RGBA)
defer d.Close()
e := edit(t, d)
tc.modify(t, e, l, f, c)
done(t, e)
before := d.Info()
s, err := d.Snapshot()
if err != nil {
t.Fatal(err)
}
data, notes, err := s.EncodeAseprite()
if err == nil || data != nil || len(notes) == 0 || notes[0].Code != tc.diagnostic {
t.Fatalf("did not reject %s: %v %+v %v", tc.name, data, notes, err)
}
if !reflect.DeepEqual(before, d.Info()) {
t.Fatal("failed export changed the source")
}
})
}
}
func TestAsepriteEncoderUsesOneSnapshotAndHonorsOutputLimit(t *testing.T) {
d, _, _, cel := doc(t, RGBA)
defer d.Close()
s, err := d.Snapshot()
if err != nil {
t.Fatal(err)
}
e := edit(t, d)
if err := e.SetPixel(cel, 0, 0, Paint{Color: red}, nil, false); err != nil {
t.Fatal(err)
}
done(t, e)
data, _, err := s.EncodeAseprite()
if err != nil {
t.Fatal(err)
}
q, _, err := ImportAseprite(data, Limits{})
if err != nil {
t.Fatal(err)
}
defer q.Close()
pixel(t, q, q.Info().Cels[0].ID, 0, 0, Color{})
s.view.limits.MaxFileBytes = 128
data, _, err = s.EncodeAseprite()
if data != nil || !errors.Is(err, ErrLimit) {
t.Fatalf("output limit: %v %v", data, err)
}
if err := d.Publish(s, func() error { t.Fatal("stale snapshot reached publication"); return nil }); !errors.Is(err, ErrConflict) {
t.Fatalf("stale snapshot: %v", err)
}
}
func TestAsepriteImporterReportsPersistentSliceKeys(t *testing.T) {
w := aseFixture{}
w.u32(2)
w.u32(2)
w.u32(0)
w.str("pivot")
for frame := uint32(0); frame < 2; frame++ {
w.u32(frame)
w.u32(3)
w.u32(4)
w.u32(4)
w.u32(4)
w.u32(2)
w.u32(1)
}
data := aseFile(32, 0, aseFrame(100, aseLayerChunk("L", 0, 0, 255), aseChunk(0x2022, w.Bytes())), aseFrame(100))
d, notes, err := ImportAseprite(data, Limits{})
if err != nil {
t.Fatal(err)
}
defer d.Close()
if len(notes) != 1 || notes[0].Code != "slice-keyframe-timing" || !strings.Contains(notes[0].Message, "persist") {
t.Fatalf("missing timing warning: %+v", notes)
}
if slices := d.Info().Slices; len(slices) != 2 || slices[0].Pivot != image.Pt(5, 5) {
t.Fatalf("relative pivot: %+v", slices)
}
}