├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── aseprite.go ├── decode.go ├── decode_test.go ├── file.go ├── go.mod ├── internal ├── blend │ ├── blend.go │ ├── blend_test.go │ └── color.go └── require │ └── require.go ├── parse.go └── testfiles ├── blendtest.aseprite ├── dst.jpg ├── slime_grayscale.aseprite ├── slime_paletted.aseprite └── src.jpg /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: '1.19' 19 | - name: Set up goveralls 20 | run: go install github.com/mattn/goveralls@latest 21 | - name: Run unit tests 22 | run: go test -race -covermode atomic -coverprofile=covprofile ./... 23 | - name: Send coverage 24 | env: 25 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 26 | run: goveralls -coverprofile=covprofile -service=github 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022, https://github.com/askeladdk 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aseprite image loader 2 | 3 | [![GoDoc](https://godoc.org/github.com/askeladdk/aseprite?status.png)](https://godoc.org/github.com/askeladdk/aseprite) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/askeladdk/aseprite)](https://goreportcard.com/report/github.com/askeladdk/aseprite) 5 | [![Coverage Status](https://coveralls.io/repos/github/askeladdk/aseprite/badge.svg?branch=master)](https://coveralls.io/github/askeladdk/aseprite?branch=master) 6 | 7 | ## Overview 8 | 9 | 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). 10 | 11 | Layers are flattened, blending modes are applied, and frames are arranged on a single texture atlas. Invisible and reference layers are ignored. 12 | 13 | Limitations: 14 | - Tilemaps are not supported. 15 | - External files are not supported. 16 | - Old aseprite format is not supported. 17 | - Color profiles are ignored. 18 | 19 | ## Install 20 | 21 | ``` 22 | go get -u github.com/askeladdk/aseprite 23 | ``` 24 | 25 | ## Quickstart 26 | 27 | Use `image.Decode` to decode an aseprite sprite file to an `image.Image`: 28 | 29 | ```go 30 | import ( 31 | _ "github.com/askeladdk/aseprite" 32 | ) 33 | 34 | img, imgformat, err := image.Decode("test.aseprite") 35 | ``` 36 | 37 | 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: 38 | 39 | ```go 40 | if imgformat == "aseprite" { 41 | sprite := img.(*aseprite.Aseprite) 42 | for _, frame := range sprite.Frames { 43 | // etc ... 44 | } 45 | } 46 | ``` 47 | 48 | Alternatively, use the `Read` function to directly decode an image to `aseprite.Aseprite`: 49 | 50 | ```go 51 | sprite, err := aseprite.Read(f) 52 | ``` 53 | 54 | Read the [documentation](https://pkg.go.dev/github.com/askeladdk/aseprite) for more information about what meta data is extracted. 55 | 56 | ## License 57 | 58 | Package aseprite is released under the terms of the ISC license. 59 | 60 | The internal blend package is released by Guillermo Estrada under the terms of the MIT license: http://github.com/phrozen/blend. 61 | -------------------------------------------------------------------------------- /aseprite.go: -------------------------------------------------------------------------------- 1 | // Package aseprite implements a decoder for Aseprite sprite files. 2 | // 3 | // Layers are flattened, blending modes are applied, 4 | // and frames are arranged on a single texture atlas. 5 | // Invisible and reference layers are ignored. 6 | // Tilesets and external files are not supported. 7 | // 8 | // Aseprite file format spec: https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md 9 | package aseprite 10 | 11 | import ( 12 | "image" 13 | "image/color" 14 | "io" 15 | "time" 16 | ) 17 | 18 | // LoopDirection enumerates all loop animation directions. 19 | type LoopDirection uint8 20 | 21 | const ( 22 | Forward LoopDirection = iota 23 | Reverse 24 | PingPong 25 | PingPongReverse 26 | ) 27 | 28 | // Tag is an animation tag. 29 | type Tag struct { 30 | // Name is the name of the tag. Can be duplicate. 31 | Name string 32 | 33 | // Lo is the first frame in the animation. 34 | Lo uint16 35 | 36 | // Hi is the last frame in the animation. 37 | Hi uint16 38 | 39 | // Repeat specifies how many times to repeat the animation. 40 | Repeat uint16 41 | 42 | // LoopDirection is the looping direction of the animation. 43 | LoopDirection LoopDirection 44 | } 45 | 46 | // Frame represents a single frame in the sprite. 47 | type Frame struct { 48 | // Bounds is the image bounds of the frame in the sprite's atlas. 49 | Bounds image.Rectangle 50 | 51 | // Duration is the time in seconds that the frame should be displayed for 52 | // in a tag animation loop. 53 | Duration time.Duration 54 | 55 | // Data lists all optional user data set in the cels that make up the frame. 56 | // The data of invisible and reference layers is not included. 57 | Data [][]byte 58 | } 59 | 60 | // Slice represents a single slice. 61 | type Slice struct { 62 | // Bounds is the bounds of the image in the texture atlas. 63 | Bounds image.Rectangle 64 | 65 | // Center is the 9-slices center relative to Bounds. 66 | Center image.Rectangle 67 | 68 | // Pivot is the pivot point relative to Bounds. 69 | Pivot image.Point 70 | 71 | // Name is the name of the slice. Can be duplicate. 72 | Name string 73 | 74 | // Data is optional user data. 75 | Data []byte 76 | 77 | // Color is the slice color. 78 | Color color.Color 79 | } 80 | 81 | // Aseprite holds the results of a parsed Aseprite image file. 82 | type Aseprite struct { 83 | // Image contains all frame images in a single image. 84 | // Frame bounds specify where the frame images are located. 85 | image.Image 86 | 87 | // Frames lists all frames that make up the sprite. 88 | Frames []Frame 89 | 90 | // Tags lists all animation tags. 91 | Tags []Tag 92 | 93 | // Slices lists all slices. 94 | Slices []Slice 95 | 96 | // LayerData lists the user data of all visible layers. 97 | LayerData [][]byte 98 | } 99 | 100 | func (spr *Aseprite) readFrom(r io.Reader) error { 101 | var f file 102 | 103 | if _, err := f.ReadFrom(r); err != nil { 104 | return err 105 | } 106 | 107 | f.initPalette() 108 | 109 | if err := f.initLayers(); err != nil { 110 | return err 111 | } 112 | 113 | if err := f.initCels(); err != nil { 114 | return err 115 | } 116 | 117 | var framesr []image.Rectangle 118 | spr.Image, framesr = f.buildAtlas() 119 | userdata := f.buildUserData() 120 | spr.Frames, userdata = f.buildFrames(framesr, userdata) 121 | spr.LayerData = f.buildLayerData(userdata) 122 | spr.Tags = f.buildTags() 123 | spr.Slices = f.buildSlices() 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package aseprite 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "io" 7 | ) 8 | 9 | // Read decodes an Aseprite image from r. 10 | func Read(r io.Reader) (*Aseprite, error) { 11 | var spr Aseprite 12 | if err := spr.readFrom(r); err != nil { 13 | return nil, err 14 | } 15 | 16 | return &spr, nil 17 | } 18 | 19 | // Decode decodes an Aseprite image from r and returns it as an image.Image. 20 | func Decode(r io.Reader) (image.Image, error) { 21 | return Read(r) 22 | } 23 | 24 | // DecodeConfig returns the color model and dimensions of an Aseprite image 25 | // without decoding the entire image. 26 | func DecodeConfig(r io.Reader) (image.Config, error) { 27 | var f file 28 | 29 | if _, err := f.ReadFrom(r); err != nil { 30 | return image.Config{}, err 31 | } 32 | 33 | fw, fh := factorPowerOfTwo(len(f.frames)) 34 | if f.framew > f.frameh { 35 | fw, fh = fh, fw 36 | } 37 | 38 | var colorModel color.Model 39 | 40 | switch f.bpp { 41 | case 8: 42 | f.initPalette() 43 | colorModel = f.palette 44 | case 16: 45 | colorModel = color.Gray16Model 46 | default: 47 | colorModel = color.RGBAModel 48 | } 49 | 50 | return image.Config{ 51 | ColorModel: colorModel, 52 | Width: f.framew * fw, 53 | Height: f.frameh * fh, 54 | }, nil 55 | } 56 | 57 | func init() { 58 | image.RegisterFormat("aseprite", "????\xE0\xA5", Decode, DecodeConfig) 59 | } 60 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package aseprite 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/png" 7 | "os" 8 | "testing" 9 | 10 | "github.com/askeladdk/aseprite/internal/require" 11 | ) 12 | 13 | func equalPalette(a, b color.Palette) bool { 14 | if len(a) != len(b) { 15 | return false 16 | } 17 | for i := range a { 18 | if a[i] != b[i] { 19 | return false 20 | } 21 | } 22 | return true 23 | } 24 | 25 | func TestDecode(t *testing.T) { 26 | for _, tt := range []struct { 27 | Name string 28 | Filename string 29 | Outfile string 30 | Frames int 31 | Tags int 32 | }{ 33 | { 34 | Name: "paletted", 35 | Filename: "./testfiles/slime_paletted.aseprite", 36 | Outfile: "slime_paletted.png", 37 | Frames: 10, 38 | Tags: 2, 39 | }, 40 | { 41 | Name: "grayscale", 42 | Filename: "./testfiles/slime_grayscale.aseprite", 43 | Outfile: "slime_grayscale.png", 44 | Frames: 10, 45 | Tags: 2, 46 | }, 47 | { 48 | Name: "blendtest", 49 | Filename: "./testfiles/blendtest.aseprite", 50 | Outfile: "blendtest.png", 51 | Frames: 1, 52 | Tags: 0, 53 | }, 54 | } { 55 | t.Run(tt.Name, func(t *testing.T) { 56 | f, err := os.Open(tt.Filename) 57 | require.NoError(t, err) 58 | defer f.Close() 59 | 60 | img, imgformat, err := image.Decode(f) 61 | require.NoError(t, err) 62 | 63 | if imgformat != "aseprite" { 64 | t.Fatal(imgformat) 65 | } 66 | 67 | aspr, ok := img.(*Aseprite) 68 | require.True(t, ok) 69 | require.True(t, len(aspr.Frames) == tt.Frames, "frames", len(aspr.Frames)) 70 | require.True(t, len(aspr.Tags) == tt.Tags, "tags", len(aspr.Tags)) 71 | 72 | out, err := os.Create(tt.Outfile) 73 | require.NoError(t, err) 74 | require.NoError(t, png.Encode(out, img)) 75 | }) 76 | } 77 | } 78 | 79 | func TestDecodeConfig(t *testing.T) { 80 | for _, tt := range []struct { 81 | Name string 82 | Filename string 83 | ImageConfig image.Config 84 | }{ 85 | { 86 | Name: "paletted", 87 | Filename: "./testfiles/slime_paletted.aseprite", 88 | ImageConfig: image.Config{ 89 | ColorModel: &color.Palette{ 90 | 0: color.Alpha16{0}, 91 | 1: color.NRGBA{247, 231, 198, 255}, 92 | 2: color.NRGBA{214, 142, 73, 255}, 93 | 3: color.NRGBA{166, 55, 37, 255}, 94 | 4: color.NRGBA{51, 30, 80, 255}, 95 | }, 96 | Width: 128, 97 | Height: 256, 98 | }, 99 | }, 100 | { 101 | Name: "grayscale", 102 | Filename: "./testfiles/slime_grayscale.aseprite", 103 | ImageConfig: image.Config{ 104 | ColorModel: color.Gray16Model, 105 | Width: 128, 106 | Height: 256, 107 | }, 108 | }, 109 | { 110 | Name: "blendtest", 111 | Filename: "./testfiles/blendtest.aseprite", 112 | ImageConfig: image.Config{ 113 | ColorModel: color.RGBAModel, 114 | Width: 640, 115 | Height: 360, 116 | }, 117 | }, 118 | } { 119 | t.Run(tt.Name, func(t *testing.T) { 120 | f, err := os.Open(tt.Filename) 121 | require.NoError(t, err) 122 | defer f.Close() 123 | 124 | conf, imgformat, err := image.DecodeConfig(f) 125 | require.NoError(t, err) 126 | require.True(t, imgformat == "aseprite", "image format", imgformat) 127 | 128 | require.True(t, conf.Height == tt.ImageConfig.Height, "height") 129 | require.True(t, conf.Width == tt.ImageConfig.Width, "width") 130 | 131 | if pal, ok := conf.ColorModel.(color.Palette); ok { 132 | imgpal := *tt.ImageConfig.ColorModel.(*color.Palette) 133 | require.True(t, equalPalette(pal, imgpal), "palette") 134 | } else { 135 | require.True(t, conf.ColorModel == tt.ImageConfig.ColorModel, "color model") 136 | } 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package aseprite 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "io" 11 | "math" 12 | "time" 13 | 14 | "github.com/askeladdk/aseprite/internal/blend" 15 | ) 16 | 17 | var errInvalidMagic = errors.New("invalid magic number") 18 | 19 | type cel struct { 20 | image image.Image 21 | mask image.Uniform 22 | data []byte 23 | } 24 | 25 | func makeCelImage8(f *file, bounds image.Rectangle, opacity byte, pix []byte) cel { 26 | img := image.Paletted{ 27 | Pix: pix, 28 | Stride: bounds.Dx(), 29 | Rect: bounds, 30 | Palette: f.palette, 31 | } 32 | 33 | mask := image.Uniform{color.Alpha{opacity}} 34 | 35 | return cel{&img, mask, nil} 36 | } 37 | 38 | func makeCelImage16(f *file, bounds image.Rectangle, opacity byte, pix []byte) cel { 39 | img := image.Gray16{ 40 | Pix: pix, 41 | Stride: bounds.Dx() * 2, 42 | Rect: bounds, 43 | } 44 | 45 | mask := image.Uniform{color.Alpha{opacity}} 46 | 47 | return cel{&img, mask, nil} 48 | } 49 | 50 | func makeCelImage32(f *file, bounds image.Rectangle, opacity byte, pix []byte) cel { 51 | img := image.NRGBA{ 52 | Pix: pix, 53 | Stride: bounds.Dx() * 4, 54 | Rect: bounds, 55 | } 56 | 57 | mask := image.Uniform{color.Alpha{opacity}} 58 | 59 | return cel{&img, mask, nil} 60 | } 61 | 62 | type layer struct { 63 | flags uint16 64 | blendMode uint16 65 | opacity byte 66 | data []byte 67 | } 68 | 69 | func (l *layer) Parse(raw []byte) error { 70 | if typ := binary.LittleEndian.Uint16(raw[2:]); typ == 2 { 71 | return errors.New("tilemap layers not supported") 72 | } 73 | l.flags = binary.LittleEndian.Uint16(raw) 74 | l.blendMode = binary.LittleEndian.Uint16(raw[10:]) 75 | l.opacity = raw[12] 76 | return nil 77 | } 78 | 79 | type chunk struct { 80 | typ int 81 | raw []byte 82 | } 83 | 84 | func (c chunk) Reader() io.Reader { 85 | return bytes.NewReader(c.raw) 86 | } 87 | 88 | func (c *chunk) Read(raw []byte) ([]byte, error) { 89 | chunkLen := binary.LittleEndian.Uint32(raw) 90 | c.typ = int(binary.LittleEndian.Uint16(raw[4:])) 91 | c.raw = raw[6:chunkLen] 92 | return raw[chunkLen:], nil 93 | } 94 | 95 | type frame struct { 96 | dur time.Duration 97 | chunks []chunk 98 | cels []cel 99 | } 100 | 101 | func (f *frame) Read(raw []byte) ([]byte, error) { 102 | if magic := binary.LittleEndian.Uint16(raw[4:]); magic != 0xF1FA { 103 | return nil, errInvalidMagic 104 | } 105 | 106 | // frameLen := binary.LittleEndian.Uint32(raw[0:]) 107 | oldChunks := binary.LittleEndian.Uint16(raw[6:]) 108 | durationMS := binary.LittleEndian.Uint16(raw[8:]) 109 | newChunks := binary.LittleEndian.Uint32(raw[12:]) 110 | 111 | f.dur = time.Millisecond * time.Duration(durationMS) 112 | 113 | nchunks := int(newChunks) 114 | if nchunks == 0 { 115 | nchunks = int(oldChunks) 116 | } 117 | 118 | f.chunks = make([]chunk, nchunks) 119 | 120 | raw = raw[16:] 121 | 122 | for i := 0; i < nchunks; i++ { 123 | var c chunk 124 | raw, _ = c.Read(raw) 125 | f.chunks[i] = c 126 | } 127 | 128 | return raw, nil 129 | } 130 | 131 | type file struct { 132 | framew int 133 | frameh int 134 | flags uint16 135 | bpp uint16 136 | transparent uint8 137 | palette color.Palette 138 | frames []frame 139 | layers []layer 140 | makeCel func(f *file, bounds image.Rectangle, opacity byte, pix []byte) cel 141 | } 142 | 143 | func (f *file) ReadFrom(r io.Reader) (int64, error) { 144 | var hdr [128]byte 145 | 146 | raw := hdr[:] 147 | 148 | if n, err := io.ReadFull(r, raw); err != nil { 149 | return int64(n), err 150 | } 151 | 152 | if magic := binary.LittleEndian.Uint16(raw[4:]); magic != 0xA5E0 { 153 | return 128, errInvalidMagic 154 | } 155 | 156 | if pixw, pixh := raw[34], raw[35]; pixw != pixh { 157 | return 128, errors.New("unsupported pixel ratio") 158 | } 159 | 160 | f.bpp = binary.LittleEndian.Uint16(raw[12:]) 161 | f.flags = binary.LittleEndian.Uint16(raw[14:]) 162 | f.frames = make([]frame, 0, binary.LittleEndian.Uint16(raw[6:])) 163 | f.framew = int(binary.LittleEndian.Uint16(raw[8:])) 164 | f.frameh = int(binary.LittleEndian.Uint16(raw[10:])) 165 | f.palette = make(color.Palette, binary.LittleEndian.Uint16(raw[32:])) 166 | f.transparent = raw[28] 167 | 168 | switch f.bpp { 169 | case 8: 170 | f.makeCel = makeCelImage8 171 | case 16: 172 | f.makeCel = makeCelImage16 173 | case 32: 174 | f.makeCel = makeCelImage32 175 | default: 176 | return 0, errors.New("invalid color depth") 177 | } 178 | 179 | for i := range f.palette { 180 | f.palette[i] = color.Black 181 | } 182 | f.palette[f.transparent] = color.Transparent 183 | 184 | fileSize := int64(binary.LittleEndian.Uint32(raw)) 185 | raw = make([]byte, fileSize-128) 186 | 187 | if n, err := io.ReadFull(r, raw); err != nil { 188 | return int64(128 + n), err 189 | } 190 | 191 | for len(raw) > 0 { 192 | var fr frame 193 | var err error 194 | if raw, err = fr.Read(raw); err != nil { 195 | return fileSize, err 196 | } 197 | 198 | f.frames = append(f.frames, fr) 199 | } 200 | 201 | return fileSize, nil 202 | } 203 | 204 | func (f *file) buildAtlas() (atlas draw.Image, framesr []image.Rectangle) { 205 | var atlasr image.Rectangle 206 | atlasr, framesr = makeAtlasFrames(len(f.frames), f.framew, f.frameh) 207 | 208 | switch f.bpp { 209 | case 8: 210 | atlas = image.NewPaletted(atlasr, f.palette) 211 | case 16: 212 | atlas = image.NewGray16(atlasr) 213 | default: 214 | atlas = image.NewRGBA(atlasr) 215 | } 216 | 217 | framebounds := image.Rect(0, 0, f.framew, f.frameh) 218 | 219 | dstblend := image.NewRGBA(framebounds) 220 | dst := image.NewRGBA(framebounds) 221 | 222 | transparent := &image.Uniform{color.Transparent} 223 | 224 | for i, fr := range f.frames { 225 | draw.Draw(dst, framebounds, transparent, image.Point{}, draw.Src) 226 | for layer, c := range fr.cels { 227 | if c.image == nil { 228 | continue 229 | } 230 | 231 | src := c.image 232 | sr := src.Bounds() 233 | sp := sr.Min 234 | 235 | if mode := f.layers[layer].blendMode; mode > 0 && int(mode) < len(blend.Modes) { 236 | draw.Draw(dstblend, framebounds, transparent, image.Point{}, draw.Src) 237 | blend.Blend(dstblend, sr.Sub(sp), src, sp, dst, sp, blend.Modes[mode]) 238 | src = dstblend 239 | sp = image.Point{} 240 | } 241 | 242 | draw.DrawMask(dst, sr, src, sp, &c.mask, image.Point{}, draw.Over) 243 | } 244 | 245 | draw.Draw(atlas, framesr[i], dst, image.Point{}, draw.Src) 246 | } 247 | 248 | return 249 | } 250 | 251 | func (f *file) buildUserData() []byte { 252 | n := 0 253 | 254 | for _, l := range f.layers { 255 | if l.flags&1 != 0 { 256 | n += len(l.data) 257 | } 258 | } 259 | 260 | for _, fr := range f.frames { 261 | for _, c := range fr.cels { 262 | n += len(c.data) 263 | } 264 | } 265 | 266 | return make([]byte, 0, n) 267 | } 268 | 269 | func (f *file) buildLayerData(userdata []byte) [][]byte { 270 | ld := make([][]byte, 0, len(f.layers)) 271 | for _, l := range f.layers { 272 | if l.flags&1 != 0 && len(l.data) > 0 { 273 | ofs := len(userdata) 274 | userdata = append(userdata, l.data...) 275 | ld = append(ld, userdata[ofs:]) 276 | } 277 | } 278 | return ld 279 | } 280 | 281 | func (f *file) buildFrames(framesr []image.Rectangle, userdata []byte) ([]Frame, []byte) { 282 | frames := make([]Frame, len(f.frames)) 283 | 284 | for i, fr := range f.frames { 285 | frames[i].Duration = fr.dur 286 | frames[i].Bounds = framesr[i] 287 | frames[i].Data = make([][]byte, 0, len(fr.cels)) 288 | for _, c := range fr.cels { 289 | if nd := len(c.data); nd > 0 { 290 | ofs := len(userdata) 291 | userdata = append(userdata, c.data...) 292 | frames[i].Data = append(frames[i].Data, userdata[ofs:]) 293 | } 294 | } 295 | } 296 | 297 | return frames, userdata 298 | } 299 | 300 | func makeAtlasFrames(nframes, framew, frameh int) (atlasr image.Rectangle, framesr []image.Rectangle) { 301 | fw, fh := factorPowerOfTwo(nframes) 302 | if framew > frameh { 303 | fw, fh = fh, fw 304 | } 305 | 306 | atlasr = image.Rect(0, 0, fw*framew, fh*frameh) 307 | 308 | for i := 0; i < nframes; i++ { 309 | x, y := i%fw, i/fw 310 | framesr = append(framesr, image.Rectangle{ 311 | Min: image.Pt(x*framew, y*frameh), 312 | Max: image.Pt((x+1)*framew, (y+1)*frameh), 313 | }) 314 | } 315 | 316 | return 317 | } 318 | 319 | // factorPowerOfTwo computes n=a*b, where a, b are powers of two and a >= b. 320 | func factorPowerOfTwo(n int) (a, b int) { 321 | x := int(math.Ceil(math.Log2(float64(n)))) 322 | a = 1 << (x - x/2) 323 | b = 1 << (x / 2) 324 | return 325 | } 326 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/askeladdk/aseprite 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /internal/blend/blend.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Guillermo Estrada. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | // http://github.com/phrozen/blend 5 | 6 | package blend 7 | 8 | import ( 9 | "image" 10 | "image/color" 11 | "image/draw" 12 | "math" 13 | ) 14 | 15 | // Constants of max and mid values for uint16 for internal use. 16 | // This can be changed to make the algorithms use uint8 instead, 17 | // but they are kept this way to provide more accurate calculations 18 | // and to support all of the color modes in the 'image' package. 19 | const ( 20 | max = float64(math.MaxUint16) // max range of color.Color 21 | mid = max / 2 22 | ) 23 | 24 | // BlendFunc blends the source color with the destination color. 25 | type BlendFunc func(dst, src color.Color) color.Color 26 | 27 | // Modes lists all blend modes that are supported by the Aseprite file format. 28 | var Modes = [19]BlendFunc{ 29 | 0: Normal, 30 | 1: Multiply, 31 | 2: Screen, 32 | 3: Overlay, 33 | 4: Darken, 34 | 5: Lighten, 35 | 6: ColorDodge, 36 | 7: ColorBurn, 37 | 8: HardLight, 38 | 9: SoftLight, 39 | 10: Difference, 40 | 11: Exclusion, 41 | 12: Hue, 42 | 13: Saturation, 43 | 14: Color, 44 | 15: Luminosity, 45 | 16: Addition, 46 | 17: Subtract, 47 | 18: Divide, 48 | } 49 | 50 | // clip clips r against each image's bounds (after translating into the 51 | // destination image's coordinate space) and shifts the points sp and mp by 52 | // the same amount as the change in r.Min. 53 | func clip(dst image.Image, r *image.Rectangle, src0 image.Image, 54 | sp0 *image.Point, src1 image.Image, sp1 *image.Point) { 55 | orig := r.Min 56 | *r = r.Intersect(dst.Bounds()) 57 | *r = r.Intersect(src0.Bounds().Add(orig.Sub(*sp0))) 58 | *r = r.Intersect(src1.Bounds().Add(orig.Sub(*sp1))) 59 | dx := r.Min.X - orig.X 60 | dy := r.Min.Y - orig.Y 61 | sp0.X += dx 62 | sp0.Y += dy 63 | sp1.X += dx 64 | sp1.Y += dy 65 | } 66 | 67 | // Blend blends src0 (top layer) into src1 (bottom layer) using mode 68 | // and stores the result in dst. 69 | func Blend(dst draw.Image, r image.Rectangle, src0 image.Image, sp0 image.Point, 70 | src1 image.Image, sp1 image.Point, mode BlendFunc) { 71 | clip(dst, &r, src0, &sp0, src1, &sp1) 72 | if r.Empty() { 73 | return 74 | } 75 | 76 | for y := r.Min.Y; y < r.Max.Y; y++ { 77 | for x := r.Min.X; x < r.Max.X; x++ { 78 | src0col := src0.At(x+sp0.X, y+sp0.Y) 79 | // aseprite does not blend transparent pixels. 80 | if _, _, _, a := src0col.RGBA(); a == 0 { 81 | dst.Set(x, y, src0col) 82 | } else { 83 | dst.Set(x, y, mode(src1.At(x+sp1.X, y+sp1.Y), src0col)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | func blendPerChannel(dst, src color.Color, blend func(float64, float64) float64) color.Color { 90 | d := color2rgbaf64(dst) 91 | s := color2rgbaf64(src) 92 | return color.RGBA{ 93 | R: clampToByte(blend(d.r, s.r)/256 + 0.5), 94 | G: clampToByte(blend(d.g, s.g)/256 + 0.5), 95 | B: clampToByte(blend(d.b, s.b)/256 + 0.5), 96 | A: clampToByte(d.a/256 + 0.5), 97 | } 98 | } 99 | 100 | // Normal. 101 | func Normal(dst, src color.Color) color.Color { 102 | return src 103 | } 104 | 105 | // Darken. 106 | func Darken(dst, src color.Color) color.Color { 107 | return blendPerChannel(dst, src, func(d, s float64) float64 { 108 | return math.Min(d, s) 109 | }) 110 | } 111 | 112 | // Multiply. 113 | func Multiply(dst, src color.Color) color.Color { 114 | return blendPerChannel(dst, src, func(d, s float64) float64 { 115 | return s * d / max 116 | }) 117 | } 118 | 119 | // Color burn. 120 | func ColorBurn(dst, src color.Color) color.Color { 121 | return blendPerChannel(dst, src, func(d, s float64) float64 { 122 | if s == 0.0 { 123 | return s 124 | } 125 | return math.Max(0.0, max-((max-d)*max/s)) 126 | }) 127 | } 128 | 129 | // Lighten. 130 | func Lighten(dst, src color.Color) color.Color { 131 | return blendPerChannel(dst, src, func(d, s float64) float64 { 132 | return math.Max(d, s) 133 | }) 134 | } 135 | 136 | // Screen. 137 | func Screen(dst, src color.Color) color.Color { 138 | return blendPerChannel(dst, src, func(d, s float64) float64 { 139 | return s + d - s*d/max 140 | }) 141 | } 142 | 143 | // Color dodge. 144 | func ColorDodge(dst, src color.Color) color.Color { 145 | return blendPerChannel(dst, src, func(d, s float64) float64 { 146 | if s == max { 147 | return s 148 | } 149 | return math.Min(max, (d * max / (max - s))) 150 | }) 151 | } 152 | 153 | // Overlay. 154 | func Overlay(dst, src color.Color) color.Color { 155 | return blendPerChannel(dst, src, func(d, s float64) float64 { 156 | if d < mid { 157 | return 2 * s * d / max 158 | } 159 | return max - 2*(max-s)*(max-d)/max 160 | }) 161 | } 162 | 163 | // Soft Light. 164 | func SoftLight(dst, src color.Color) color.Color { 165 | return blendPerChannel(dst, src, func(d, s float64) float64 { 166 | return (d / max) * (d + (2*s/max)*(max-d)) 167 | }) 168 | } 169 | 170 | // Hard Light. 171 | func HardLight(dst, src color.Color) color.Color { 172 | return blendPerChannel(dst, src, func(d, s float64) float64 { 173 | if s > mid { 174 | return d + (max-d)*((s-mid)/mid) 175 | } 176 | return d * s / mid 177 | }) 178 | } 179 | 180 | // Addition. 181 | func Addition(dst, src color.Color) color.Color { 182 | return blendPerChannel(dst, src, func(d, s float64) float64 { 183 | if s+d > max { 184 | return max 185 | } 186 | return s + d 187 | }) 188 | } 189 | 190 | // Subtract. 191 | func Subtract(dst, src color.Color) color.Color { 192 | return blendPerChannel(dst, src, func(d, s float64) float64 { 193 | if d-s < 0.0 { 194 | return 0.0 195 | } 196 | return d - s 197 | }) 198 | } 199 | 200 | // Divide. 201 | func Divide(dst, src color.Color) color.Color { 202 | return blendPerChannel(dst, src, func(d, s float64) float64 { 203 | return (d*max)/s + 1.0 204 | }) 205 | } 206 | 207 | // Difference. 208 | func Difference(dst, src color.Color) color.Color { 209 | return blendPerChannel(dst, src, func(d, s float64) float64 { 210 | return math.Abs(s - d) 211 | }) 212 | } 213 | 214 | // Exclusion. 215 | func Exclusion(dst, src color.Color) color.Color { 216 | return blendPerChannel(dst, src, func(d, s float64) float64 { 217 | return s + d - s*d/mid 218 | }) 219 | } 220 | 221 | // Hue. 222 | func Hue(dst, src color.Color) color.Color { 223 | s := rgb2hsl(src) 224 | if s.s == 0.0 { 225 | return dst 226 | } 227 | d := rgb2hsl(dst) 228 | return hsl2rgb(s.h, d.s, d.l) 229 | } 230 | 231 | // Saturation. 232 | func Saturation(dst, src color.Color) color.Color { 233 | s := rgb2hsl(src) 234 | d := rgb2hsl(dst) 235 | return hsl2rgb(d.h, s.s, d.l) 236 | } 237 | 238 | // Color. 239 | func Color(dst, src color.Color) color.Color { 240 | s := rgb2hsl(src) 241 | d := rgb2hsl(dst) 242 | return hsl2rgb(s.h, s.s, d.l) 243 | } 244 | 245 | // Luminosity. 246 | func Luminosity(dst, src color.Color) color.Color { 247 | s := rgb2hsl(src) 248 | d := rgb2hsl(dst) 249 | return hsl2rgb(d.h, d.s, s.l) 250 | } 251 | -------------------------------------------------------------------------------- /internal/blend/blend_test.go: -------------------------------------------------------------------------------- 1 | package blend 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/jpeg" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/askeladdk/aseprite/internal/require" 12 | ) 13 | 14 | func jpgDecode(filename string) (image.Image, error) { 15 | f, err := os.Open(filename) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer f.Close() 20 | return jpeg.Decode(f) 21 | } 22 | 23 | func jpgEncode(filename string, img image.Image) error { 24 | f, err := os.Create(filename) 25 | if err != nil { 26 | return err 27 | } 28 | defer f.Close() 29 | return jpeg.Encode(f, img, &jpeg.Options{Quality: 85}) 30 | } 31 | 32 | func TestBlendModes(t *testing.T) { 33 | dst, err := jpgDecode("../../testfiles/dst.jpg") 34 | require.NoError(t, err) 35 | 36 | src, err := jpgDecode("../../testfiles/src.jpg") 37 | require.NoError(t, err) 38 | 39 | for i, name := range []string{ 40 | "Normal", 41 | "Multiply", 42 | "Screen", 43 | "Overlay", 44 | "Darken", 45 | "Lighten", 46 | "ColorDodge", 47 | "ColorBurn", 48 | "HardLight", 49 | "SoftLight", 50 | "Difference", 51 | "Exclusion", 52 | "Hue", 53 | "Saturation", 54 | "Color", 55 | "Luminosity", 56 | "Addition", 57 | "Subtract", 58 | "Divide", 59 | } { 60 | t.Run(name, func(t *testing.T) { 61 | img := image.NewRGBA(src.Bounds()) 62 | Blend(img, img.Bounds(), src, image.Point{}, dst, image.Point{}, Modes[i]) 63 | 64 | require.NoError(t, jpgEncode(fmt.Sprintf("out_%s.jpg", strings.ToLower(name)), img)) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/blend/color.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Guillermo Estrada. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | // http://github.com/phrozen/blend 5 | 6 | package blend 7 | 8 | import ( 9 | "image/color" 10 | "math" 11 | ) 12 | 13 | func clampToByte(a float64) byte { 14 | if a < 0 { 15 | return 0 16 | } else if a > math.MaxUint8 { 17 | return math.MaxUint8 18 | } 19 | return byte(a) 20 | } 21 | 22 | type rgbaf64 struct { 23 | r, g, b, a float64 24 | } 25 | 26 | func color2rgbaf64(c color.Color) rgbaf64 { 27 | r, g, b, a := c.RGBA() 28 | return rgbaf64{float64(r), float64(g), float64(b), float64(a)} 29 | } 30 | 31 | type hslf64 struct { 32 | h, s, l float64 33 | } 34 | 35 | func rgb2hsl(c color.Color) hslf64 { 36 | var h, s, l float64 37 | cr, cg, cb, _ := c.RGBA() 38 | r := float64(cr) / max 39 | g := float64(cg) / max 40 | b := float64(cb) / max 41 | cmax := math.Max(math.Max(r, g), b) 42 | cmin := math.Min(math.Min(r, g), b) 43 | l = (cmax + cmin) / 2.0 44 | if cmax != cmin { // Chromatic, else Achromatic. 45 | delta := cmax - cmin 46 | if l > 0.5 { 47 | s = delta / (2.0 - cmax - cmin) 48 | } else { 49 | s = delta / (cmax + cmin) 50 | } 51 | switch cmax { 52 | case r: 53 | h = (g - b) / delta 54 | if g < b { 55 | h += 6.0 56 | } 57 | case g: 58 | h = (b-r)/delta + 2.0 59 | case b: 60 | h = (r-g)/delta + 4.0 61 | } 62 | h /= 6.0 63 | } 64 | return hslf64{h, s, l} 65 | } 66 | 67 | func hsl2rgb(h, s, l float64) color.Color { 68 | var r, g, b float64 69 | if s == 0 { 70 | r, g, b = l, l, l 71 | } else { 72 | var q float64 73 | if l < 0.5 { 74 | q = l * (1 + s) 75 | } else { 76 | q = l + s - s*l 77 | } 78 | p := 2*l - q 79 | r = hue2rgb(p, q, h+1.0/3) 80 | g = hue2rgb(p, q, h) 81 | b = hue2rgb(p, q, h-1.0/3) 82 | } 83 | return color.RGBA{ 84 | R: clampToByte(r*math.MaxUint8 + 0.5), 85 | G: clampToByte(g*math.MaxUint8 + 0.5), 86 | B: clampToByte(b*math.MaxUint8 + 0.5), 87 | A: math.MaxUint8, 88 | } 89 | } 90 | 91 | func hue2rgb(p, q, t float64) float64 { 92 | if t < 0.0 { 93 | t += 1.0 94 | } else if t > 1.0 { 95 | t -= 1.0 96 | } 97 | 98 | switch { 99 | case t < 1.0/6.0: 100 | return p + (q-p)*6.0*t 101 | case t < 0.5: 102 | return q 103 | case t < 2.0/3.0: 104 | return p + (q-p)*(2.0/3.0-t)*6.0 105 | default: 106 | return p 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/require/require.go: -------------------------------------------------------------------------------- 1 | package require 2 | 3 | import "testing" 4 | 5 | func NoError(t *testing.T, err error) { 6 | if err != nil { 7 | t.Helper() 8 | t.Fatal("unexpected error:", err) 9 | } 10 | } 11 | 12 | func True(t *testing.T, test bool, args ...any) { 13 | if !test { 14 | t.Helper() 15 | t.Fatal(args...) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package aseprite 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "errors" 8 | "image" 9 | "image/color" 10 | "io" 11 | ) 12 | 13 | func skipString(raw []byte) []byte { 14 | n := binary.LittleEndian.Uint16(raw) 15 | return raw[2+n:] 16 | } 17 | 18 | func parseString(raw []byte) string { 19 | n := binary.LittleEndian.Uint16(raw) 20 | return string(raw[2 : 2+n]) 21 | } 22 | 23 | func parseColor(raw []byte) color.Color { 24 | return color.NRGBA{ 25 | R: raw[0], 26 | G: raw[1], 27 | B: raw[2], 28 | A: raw[3], 29 | } 30 | } 31 | 32 | func parseUserData(raw []byte) (data []byte, color color.Color) { 33 | flags := binary.LittleEndian.Uint32(raw) 34 | raw = raw[4:] 35 | 36 | if flags&1 != 0 { 37 | n := binary.LittleEndian.Uint16(raw) 38 | data, raw = raw[2:2+n], raw[2+n:] 39 | } 40 | 41 | if flags&2 != 0 { 42 | color = parseColor(raw) 43 | } 44 | 45 | return 46 | } 47 | 48 | func (f *file) parseChunk2019(raw []byte) { 49 | entries := binary.LittleEndian.Uint32(raw[0:]) 50 | lo := binary.LittleEndian.Uint32(raw[4:]) 51 | 52 | raw = raw[20:] 53 | 54 | for i := uint32(0); i < entries; i++ { 55 | flags := binary.LittleEndian.Uint16(raw) 56 | f.palette[lo+i] = parseColor(raw[2:]) 57 | raw = raw[6:] 58 | 59 | if flags&1 != 0 { 60 | raw = skipString(raw) 61 | } 62 | } 63 | } 64 | 65 | func (f *file) initPalette() { 66 | for _, ch := range f.frames[0].chunks { 67 | if ch.typ == 0x2019 { 68 | f.parseChunk2019(ch.raw) 69 | break 70 | } 71 | } 72 | 73 | if f.flags&1 != 0 { 74 | f.palette[f.transparent] = color.Transparent 75 | } 76 | } 77 | 78 | func (f *file) initLayers() error { 79 | chunks := f.frames[0].chunks 80 | for i, ch := range chunks { 81 | if ch.typ == 0x2004 { 82 | var l layer 83 | if err := l.Parse(ch.raw); err != nil { 84 | return err 85 | } 86 | 87 | if i < len(chunks)-1 { 88 | if ch2 := chunks[i+1]; ch2.typ == 0x2020 { 89 | l.data, _ = parseUserData(ch2.raw) 90 | } 91 | } 92 | 93 | f.layers = append(f.layers, l) 94 | } 95 | } 96 | 97 | nlayers := len(f.layers) 98 | for i := range f.frames { 99 | f.frames[i].cels = make([]cel, nlayers) 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (f *file) parseChunk2005(frame int, raw []byte) (*cel, error) { 106 | layer := binary.LittleEndian.Uint16(raw) 107 | xpos := int(binary.LittleEndian.Uint16(raw[2:])) 108 | ypos := int(binary.LittleEndian.Uint16(raw[4:])) 109 | opacity := raw[6] 110 | celtype := binary.LittleEndian.Uint16(raw[7:]) 111 | 112 | // invisible layer 113 | if f.layers[layer].flags&1 == 0 { 114 | return nil, nil 115 | } 116 | 117 | // reference layer 118 | if f.layers[layer].flags&64 != 0 { 119 | return nil, nil 120 | } 121 | 122 | raw = raw[16:] 123 | 124 | opacity = byte((int(opacity) * int(f.layers[layer].opacity)) / 255) 125 | 126 | switch celtype { 127 | case 0: // uncompressed image 128 | width := int(binary.LittleEndian.Uint16(raw)) 129 | height := int(binary.LittleEndian.Uint16(raw[2:])) 130 | pix := raw[4:] 131 | bounds := image.Rect(xpos, ypos, xpos+width, ypos+height) 132 | cel := f.makeCel(f, bounds, opacity, pix) 133 | f.frames[frame].cels[layer] = cel 134 | case 1: // linked cel 135 | srcFrame := int(binary.LittleEndian.Uint16(raw)) 136 | srcCel := f.frames[srcFrame].cels[layer] 137 | f.frames[frame].cels[layer] = srcCel 138 | case 2: // compressed image 139 | width := int(binary.LittleEndian.Uint16(raw)) 140 | height := int(binary.LittleEndian.Uint16(raw[2:])) 141 | zr, err := zlib.NewReader(bytes.NewReader(raw[4:])) 142 | if err != nil { 143 | return nil, err 144 | } 145 | pix, err := io.ReadAll(zr) 146 | if err != nil { 147 | return nil, err 148 | } 149 | bounds := image.Rect(xpos, ypos, xpos+width, ypos+height) 150 | cel := f.makeCel(f, bounds, opacity, pix) 151 | f.frames[frame].cels[layer] = cel 152 | default: 153 | return nil, errors.New("unsupported cel type") 154 | } 155 | 156 | return &f.frames[frame].cels[layer], nil 157 | } 158 | 159 | func (f *file) initCels() error { 160 | for i := range f.frames { 161 | chunks := f.frames[i].chunks 162 | for j, ch := range chunks { 163 | if ch.typ == 0x2005 { 164 | cel, err := f.parseChunk2005(i, ch.raw) 165 | if err != nil { 166 | return err 167 | } else if cel != nil && j < (len(chunks)-1) { 168 | // user data chunk 169 | if ch2 := chunks[j+1]; ch2.typ == 0x2020 { 170 | cel.data, _ = parseUserData(ch2.raw) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func parseTag(t *Tag, raw []byte) []byte { 181 | t.Lo = binary.LittleEndian.Uint16(raw) 182 | t.Hi = binary.LittleEndian.Uint16(raw[2:]) 183 | t.LoopDirection = LoopDirection(raw[4]) 184 | t.Repeat = binary.LittleEndian.Uint16(raw[5:]) 185 | t.Name = parseString(raw[17:]) 186 | return raw[19+len(t.Name):] 187 | } 188 | 189 | func (f *file) buildTags() []Tag { 190 | for _, chunk := range f.frames[0].chunks { 191 | if chunk.typ == 0x2018 { 192 | raw := chunk.raw 193 | ntags := binary.LittleEndian.Uint16(raw) 194 | tags := make([]Tag, ntags) 195 | raw = raw[10:] 196 | for i := range tags { 197 | raw = parseTag(&tags[i], raw) 198 | } 199 | return tags 200 | } 201 | } 202 | 203 | return nil 204 | } 205 | 206 | func parseSlice(s *Slice, flags uint32, raw []byte) []byte { 207 | // framenum := binary.LittleEndian.Uint32(raw) 208 | x := int32(binary.LittleEndian.Uint32(raw[4:])) 209 | y := int32(binary.LittleEndian.Uint32(raw[8:])) 210 | w := binary.LittleEndian.Uint32(raw[12:]) 211 | h := binary.LittleEndian.Uint32(raw[16:]) 212 | raw = raw[20:] 213 | 214 | var cx, cy int32 215 | var cw, ch uint32 216 | 217 | if flags&1 != 0 { 218 | cx = int32(binary.LittleEndian.Uint32(raw)) 219 | cy = int32(binary.LittleEndian.Uint32(raw[4:])) 220 | cw = binary.LittleEndian.Uint32(raw[8:]) 221 | ch = binary.LittleEndian.Uint32(raw[12:]) 222 | raw = raw[16:] 223 | } 224 | 225 | var px, py int32 226 | 227 | if flags&2 != 0 { 228 | px = int32(binary.LittleEndian.Uint32(raw)) 229 | py = int32(binary.LittleEndian.Uint32(raw[4:])) 230 | raw = raw[8:] 231 | } 232 | 233 | s.Bounds = image.Rect(int(x), int(y), int(x)+int(w), int(y)+int(h)) 234 | s.Center = image.Rect(int(cx), int(cy), int(cx)+int(cw), int(cy)+int(ch)) 235 | s.Pivot = image.Pt(int(px), int(py)) 236 | 237 | return raw 238 | } 239 | 240 | func (f *file) buildSlices() (slices []Slice) { 241 | chunks := f.frames[0].chunks 242 | for i, chunk := range chunks { 243 | if chunk.typ == 0x2022 { 244 | ofs := len(slices) 245 | raw := chunk.raw 246 | nslices := int(binary.LittleEndian.Uint32(raw)) 247 | flags := binary.LittleEndian.Uint32(raw[4:]) 248 | name := parseString(raw[12:]) 249 | 250 | // parse each slice 251 | raw = raw[14+len(name):] 252 | for i := 0; len(raw) > 0 && i < nslices; i++ { 253 | var s Slice 254 | s.Name = name 255 | raw = parseSlice(&s, flags, raw) 256 | slices = append(slices, s) 257 | } 258 | 259 | // check for user data chunk 260 | if i < len(chunks)-1 { 261 | if ud := chunks[i+1]; ud.typ == 0x2020 { 262 | data, col := parseUserData(ud.raw) 263 | data = append([]byte{}, data...) // copy 264 | for j := ofs; j < len(slices); j++ { 265 | slices[j].Data = data 266 | slices[j].Color = col 267 | } 268 | } 269 | } 270 | } 271 | } 272 | 273 | return 274 | } 275 | -------------------------------------------------------------------------------- /testfiles/blendtest.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askeladdk/aseprite/f83d2787f8b0d3626640d2b2b806fdf22b5f4fdb/testfiles/blendtest.aseprite -------------------------------------------------------------------------------- /testfiles/dst.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askeladdk/aseprite/f83d2787f8b0d3626640d2b2b806fdf22b5f4fdb/testfiles/dst.jpg -------------------------------------------------------------------------------- /testfiles/slime_grayscale.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askeladdk/aseprite/f83d2787f8b0d3626640d2b2b806fdf22b5f4fdb/testfiles/slime_grayscale.aseprite -------------------------------------------------------------------------------- /testfiles/slime_paletted.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askeladdk/aseprite/f83d2787f8b0d3626640d2b2b806fdf22b5f4fdb/testfiles/slime_paletted.aseprite -------------------------------------------------------------------------------- /testfiles/src.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askeladdk/aseprite/f83d2787f8b0d3626640d2b2b806fdf22b5f4fdb/testfiles/src.jpg --------------------------------------------------------------------------------