Binary Serialization of Tilesets (Resolved)

I’ve been trying to procedurally generate .aseprite binary files for a project I’m working on, but I’m having some trouble serializing tilesets and Tilemaps

I’ve been able to create a struct that represents the Aseprite file, and I’ve been able to successfully serialize and open a generated aseprite file with a single Frame, two layers in the frame, and two cells in the frame (each pointed at one of the two layers) each filled with a different color

The api usage for that example looks something like this

	file := &aseprite.AseFile{
		Width:      32,
		Height:     32,
		ColorDepth: 32,
		Frames: []aseprite.Frame{
			{
				Duration: 100,
				Layers: []aseprite.Layer{
					{
						Flags:         3,
						Type:          0,
						ChildLevel:    0,
						DefaultWidth:  32,
						DefaultHeight: 32,
						BlendMode:     0,
						Opacity:       255,
						Name:          "Layer 0",
					},
					{
						Flags:         3,
						Type:          0,
						ChildLevel:    0,
						DefaultWidth:  32,
						DefaultHeight: 32,
						BlendMode:     0,
						Opacity:       255,
						Name:          "Layer 1",
					},
				},
				Cels: []aseprite.Cel{
					{
						LayerIndex: 0, XOffset: 0, YOffset: 0,
						Opacity: 255, CelType: 0,
						Width: 32, Height: 32,
						PixelData: makePixels(32, 32, 0, 255, 0, 255),
					},
					{
						LayerIndex: 1, XOffset: 0, YOffset: 0,
						Opacity: 255, CelType: 0,
						Width: 32, Height: 32,
						PixelData: makePixels(32, 32, 255, 0, 0, 255),
					},
				},
			},
		},
	}

I then serialize the Aseprite struct by starting with the file headers, and then serializing each of the frames (which serialize their cells and chunks)

type AseFile struct {
	Width, Height uint16
	ColorDepth    uint16
	Tilesets      []Tileset
	Frames        []Frame
}

// Serialize writes the full .aseprite file to w.
func (a *AseFile) Serialize(w io.Writer) error {
	buf := new(bytes.Buffer)

	// File header (128 bytes)
	binary.Write(buf, binary.LittleEndian, uint32(0))             // placeholder for file size
	binary.Write(buf, binary.LittleEndian, uint16(magicFile))     // magic
	binary.Write(buf, binary.LittleEndian, uint16(len(a.Frames))) // frame count
	binary.Write(buf, binary.LittleEndian, a.Width)
	binary.Write(buf, binary.LittleEndian, a.Height)
	binary.Write(buf, binary.LittleEndian, a.ColorDepth)
	binary.Write(buf, binary.LittleEndian, uint32(1))   // flags (enable tilemap layers)
	binary.Write(buf, binary.LittleEndian, uint16(100)) // speed
	binary.Write(buf, binary.LittleEndian, uint32(0))   // reserved
	binary.Write(buf, binary.LittleEndian, uint32(0))   // reserved
	buf.Write([]byte{0, 0, 0, 0})                       // palette + pad
	buf.Write(make([]byte, 128-buf.Len()))              // pad to 128 bytes

	// Frames
	for _, frame := range a.Frames {
		fb, err := frame.serialize()
		if err != nil {
			return err
		}
		buf.Write(fb)
	}

	// Patch file size
	b := buf.Bytes()
	binary.LittleEndian.PutUint32(b[0:4], uint32(len(b)))

	// Write out
	_, err := w.Write(b)
	return err
}

And here’s the frame struct which handles serialization of the Frames Layers and Cells

// Frame holds one frame’s duration and its chunks.
type Frame struct {
	Duration uint16
	Layers   []Layer
	Cels     []Cel
	Tilesets []Tileset
}

Everything has been working well, but I’ve spent all day struggling to make any progress on generating tilemaps

The motivation for this whole project is that I’m building a procedural tilemap generator, and I’d like to be able to export to native aseprite files with tilemap feature support

I guess what I’m actually asking for help with is an overview of what’s required to serialize a minimal aseprite file with the following:

  • A valid Tileset Chunk
  • A Layer Chunk with LayerType equal to Tilemap
  • A Cel Chunk with CelType equal to Compressed Tilemap

For the Tilesets, do I just serialize each of them into the First Frame of the file?

How do I link a Cel Chunk to the appropriate Layer Chunk?

I’d appreciate any help with this, and I’ll update my post if make any progress or come up with any clarifying examples

Thanks,

After spending another day on the problem, I’m left with the following console errors after loading my generated file into aseprite:

Error: tileset 0 not found

Frame 0 didn't found layer with index 2

ZLib error -3 in inflate()

Here’s the complete code for how I’m currently serializing each frame

package aseprite

import (
	"bytes"
	"encoding/binary"
)

type Frame struct {
	Duration uint16
	Layers   []Layer
	Cels     []Cel
	Tilesets []Tileset
}

func (f *Frame) serialize() []byte {
	buf := new(bytes.Buffer)

	// 1) Placeholder for frame size (DWORD)
	buf.Write([]byte{0, 0, 0, 0})

	// 2) Frame magic (WORD)
	binary.Write(buf, binary.LittleEndian, uint16(magicFrame))

	// 3) Number of sub-chunks: layers + tilesets + cels
	chunkCount := uint16(len(f.Layers) + len(f.Tilesets) + len(f.Cels))
	binary.Write(buf, binary.LittleEndian, chunkCount)

	// 4) Frame duration (WORD)
	binary.Write(buf, binary.LittleEndian, f.Duration)

	// 5) Reserved 6 bytes
	buf.Write(make([]byte, 6))

	// --- sub-chunks in *exactly* this order ---

	// Layer Chunks
	for _, l := range f.Layers {
		buf.Write(wrapChunk(chunkLayer, l.serializePayload()))
	}

	// Cel Chunks
	for _, c := range f.Cels {
		buf.Write(wrapChunk(chunkCel, c.serializePayload()))
	}

	// Tileset chunks
	for _, ts := range f.Tilesets {
		buf.Write(wrapChunk(chunkTileset, ts.serializePayload()))
	}

	// 6) Back-patch the frame size: total minus these 4 size bytes
	b := buf.Bytes()
	binary.LittleEndian.PutUint32(b[0:4], uint32(len(b)-4))

	return b
}

// wrapChunk creates the 6-byte header + payload for a chunk.
func wrapChunk(chunkType uint16, payload []byte) []byte {
	buf := new(bytes.Buffer)
	// DWORD: chunk size = header (6 bytes) + payload
	binary.Write(buf, binary.LittleEndian, uint32(6+len(payload)))
	// WORD: chunk type
	binary.Write(buf, binary.LittleEndian, chunkType)
	// payload
	buf.Write(payload)
	return buf.Bytes()
}

I was able to fix the issue and successfully generate a valid binary with a generated TileSet, Tilemap Layer, and Tilemap Cel

I’ll try to do a write up of the changes in case it helps anyone else down the line

1 Like