Skip to content

Commit 9585c49

Browse files
committed
feat: Add support to decode animated AVIF
1 parent a6a4ea6 commit 9585c49

7 files changed

Lines changed: 348 additions & 68 deletions

File tree

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# avif-go
22

3-
A Go library and CLI tool to encode/decode AVIF images without system dependencies (CGO).
3+
A Go library & CLI tool to encode/decode static and animated AVIF without external dependencies.
44

55
## 💡 Motivation
66

77
There are a couple of libraries to encode/decode AVIF images in Go, and even though they do the job well, they have some limitations that don't satisfy my needs:
88

99
- They need dependencies to be installed on the system to either build the app or later execute it.
1010
- They rely on a WASM runtime - which is actually a really smart idea! - but it has a big impact on performance.
11+
- There's currently no Go library capable of encoding animated AVIFs; this library aims to fill that gap.
1112

1213
**avif-go** uses CGO to create a static implementation of AVIF, so you don't need `libavif` (or any of its sub-dependencies) installed to build or run your Go application.
1314

@@ -51,8 +52,8 @@ err = avif.Encode(avifFile, originalImage, nil) // encode the image & save it to
5152

5253
```go
5354
var frames []image.Image = ... // a slice of image.Image frames
54-
avifFile, err := os.Create("/path/to/animation.avif") // create the file to save the animated AVIF
55-
a := &avif.AVIF{Image: frames, Delay: []int{10, 10, 10}, LoopCount: 0} // 100ms per frame, loop forever
55+
avifFile, err := os.Create("/path/to/animation.avif") // create file to save the animation
56+
a := &avif.AVIF{Image: frames, Delay: []int{10, 10, 10}, LoopCount: 0} // anim configuration
5657
err = avif.EncodeAll(avifFile, a, nil) // encode the animation & save it to the file
5758
```
5859

@@ -64,6 +65,16 @@ avifFile, err := os.Open("/path/to/image.avif") // open the AVIF file to be deco
6465
avifImage, _, err := image.Decode(avifFile) // decode the image
6566
```
6667

68+
#### Decoding Animation
69+
70+
```go
71+
avifFile, err := os.Open("/path/to/animation.avif") // open the animated AVIF file
72+
a, err := avif.DecodeAll(avifFile) // decode all frames
73+
// a.Image contains the frames, a.Delay the timing, a.LoopCount the loop behavior
74+
```
75+
76+
For more examples on how to use this library, you can check the [cmd/](cmd) directory.
77+
6778
### CLI
6879

6980
If you want to decode an AVIF image, run the following command:

assets/image.avif

16 Bytes
Binary file not shown.

avif.go

Lines changed: 67 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,10 @@ const char* get_error_string(avifResult result) {
1010
return avifResultToString(result);
1111
}
1212
13-
// Full decode: creates a decoder, sets up the memory I/O, and decodes the image.
14-
// Returns the avifImage pointer (which contains width, height, etc.) and leaves the
15-
// decoder pointer for cleanup. Returns error result via outResult.
16-
avifImage* decode_avif_image(const uint8_t * data, size_t size, avifDecoder ** outDecoder, avifResult *outResult) {
17-
avifDecoder* decoder = avifDecoderCreate();
18-
// Force libavif to use the dav1d backend.
19-
decoder->codecChoice = AVIF_CODEC_CHOICE_DAV1D;
20-
21-
*outResult = avifDecoderSetIOMemory(decoder, data, size);
22-
if (*outResult != AVIF_RESULT_OK) {
23-
avifDecoderDestroy(decoder);
24-
return NULL;
25-
}
26-
27-
*outResult = avifDecoderParse(decoder);
28-
if (*outResult != AVIF_RESULT_OK) {
29-
avifDecoderDestroy(decoder);
30-
return NULL;
31-
}
32-
33-
*outResult = avifDecoderNextImage(decoder);
34-
if (*outResult != AVIF_RESULT_OK) {
35-
avifDecoderDestroy(decoder);
36-
return NULL;
37-
}
38-
39-
if (outDecoder) {
40-
*outDecoder = decoder;
41-
}
42-
return decoder->image;
43-
}
44-
4513
// Config-only decode: reads the header and returns width and height.
4614
// Returns error result via outResult.
4715
void get_avif_config(const uint8_t * data, size_t size, uint32_t * width, uint32_t * height, avifResult *outResult) {
4816
avifDecoder* decoder = avifDecoderCreate();
49-
// Force libavif to use the dav1d backend.
5017
decoder->codecChoice = AVIF_CODEC_CHOICE_DAV1D;
5118
5219
*outResult = avifDecoderSetIOMemory(decoder, data, size);
@@ -321,51 +288,101 @@ func createAVIFTile(pixels []byte, width, height, stride, col, row int) (*C.avif
321288
return avifImage, nil
322289
}
323290

324-
// decodeAVIFToRGBA decodes AVIF image data to an RGBA image.
291+
// decodeAVIFToRGBA decodes the first frame of AVIF image data to an RGBA image.
325292
func decodeAVIFToRGBA(data []byte) (*image.RGBA, error) {
293+
frames, _, _, err := decodeAllAVIFToRGBA(data)
294+
if err != nil {
295+
return nil, err
296+
}
297+
return frames[0], nil
298+
}
299+
300+
// decodeAllAVIFToRGBA decodes all frames from an AVIF image sequence.
301+
// Returns the frames as RGBA images, delays in centiseconds, and the repetition count.
302+
func decodeAllAVIFToRGBA(data []byte) ([]*image.RGBA, []int, int, error) {
326303
if len(data) == 0 {
327-
return nil, fmt.Errorf("cannot decode empty data")
304+
return nil, nil, 0, fmt.Errorf("cannot decode empty data")
328305
}
329306

330-
// Allocate C memory and copy data.
331307
cData := C.CBytes(data)
332308
defer C.free(cData)
333309

334-
var decoder *C.avifDecoder
335-
var result C.avifResult
336-
avifImg := C.decode_avif_image((*C.uint8_t)(cData), C.size_t(len(data)), &decoder, &result)
337-
if avifImg == nil {
338-
errStr := C.GoString(C.get_error_string(result))
339-
return nil, fmt.Errorf("failed to decode AVIF image: %s", errStr)
310+
// Create and configure decoder
311+
decoder := C.avifDecoderCreate()
312+
if decoder == nil {
313+
return nil, nil, 0, fmt.Errorf("failed to create AVIF decoder")
340314
}
341315
defer C.avifDecoderDestroy(decoder)
342316

343-
// Set up an avifRGBImage struct to hold the converted image.
317+
decoder.codecChoice = C.AVIF_CODEC_CHOICE_DAV1D
318+
319+
result := C.avifDecoderSetIOMemory(decoder, (*C.uint8_t)(cData), C.size_t(len(data)))
320+
if result != C.AVIF_RESULT_OK {
321+
errStr := C.GoString(C.get_error_string(result))
322+
return nil, nil, 0, fmt.Errorf("failed to set decoder I/O: %s", errStr)
323+
}
324+
325+
result = C.avifDecoderParse(decoder)
326+
if result != C.AVIF_RESULT_OK {
327+
errStr := C.GoString(C.get_error_string(result))
328+
return nil, nil, 0, fmt.Errorf("failed to parse AVIF: %s", errStr)
329+
}
330+
331+
frameCount := int(decoder.imageCount)
332+
repetitionCount := int(decoder.repetitionCount)
333+
334+
frames := make([]*image.RGBA, 0, frameCount)
335+
delays := make([]int, 0, frameCount)
336+
337+
for i := 0; i < frameCount; i++ {
338+
result = C.avifDecoderNextImage(decoder)
339+
if result != C.AVIF_RESULT_OK {
340+
errStr := C.GoString(C.get_error_string(result))
341+
return nil, nil, 0, fmt.Errorf("failed to decode frame %d: %s", i, errStr)
342+
}
343+
344+
img, err := avifImageToRGBA(decoder.image)
345+
if err != nil {
346+
return nil, nil, 0, fmt.Errorf("frame %d: %w", i, err)
347+
}
348+
frames = append(frames, img)
349+
350+
// Get frame timing and convert to centiseconds
351+
var timing C.avifImageTiming
352+
C.avifDecoderNthImageTiming(decoder, C.uint32_t(i), &timing)
353+
delay := 0
354+
if timing.timescale > 0 {
355+
delay = int(timing.durationInTimescales * 100 / timing.timescale)
356+
}
357+
delays = append(delays, delay)
358+
}
359+
360+
return frames, delays, repetitionCount, nil
361+
}
362+
363+
// avifImageToRGBA converts a C avifImage (YUV) to a Go image.RGBA.
364+
func avifImageToRGBA(avifImg *C.avifImage) (*image.RGBA, error) {
344365
var rgb C.avifRGBImage
345366
C.avifRGBImageSetDefaults(&rgb, avifImg)
346367
rgb.format = C.AVIF_RGB_FORMAT_RGBA
347-
rgb.depth = 8 // 8-bit per channel
368+
rgb.depth = 8
348369

349-
// Allocate pixel buffer for the RGB data.
350370
if C.avifRGBImageAllocatePixels(&rgb) != C.AVIF_RESULT_OK {
351371
return nil, fmt.Errorf("failed to allocate RGB pixels")
352372
}
353373
defer C.avifRGBImageFreePixels(&rgb)
354374

355-
// Convert the image from YUV to RGB.
356-
result = C.avifImageYUVToRGB(avifImg, &rgb)
375+
result := C.avifImageYUVToRGB(avifImg, &rgb)
357376
if result != C.AVIF_RESULT_OK {
358377
errStr := C.GoString(C.get_error_string(result))
359-
return nil, fmt.Errorf("failed to convert image to RGB: %s", errStr)
378+
return nil, fmt.Errorf("failed to convert YUV to RGB: %s", errStr)
360379
}
361380

362381
width := int(avifImg.width)
363382
height := int(avifImg.height)
364383
img := image.NewRGBA(image.Rect(0, 0, width, height))
365384
rowBytes := int(rgb.rowBytes)
366385

367-
// Copy the pixel data row by row into the Go image using direct pointer access.
368-
// This avoids the extra allocation from C.GoBytes for the entire buffer.
369386
for y := 0; y < height; y++ {
370387
srcPtr := unsafe.Add(unsafe.Pointer(rgb.pixels), y*rowBytes)
371388
dstOffset := y * img.Stride

cmd/utils.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"image"
66
"image/color"
7+
"image/color/palette"
78
"image/draw"
89
"image/gif"
910
"image/jpeg"
@@ -177,10 +178,10 @@ func decodeAvif(input, output string) (image.Image, os.FileInfo, error) {
177178
if err != nil {
178179
return nil, nil, err
179180
}
180-
181181
defer inputFile.Close()
182182

183-
img, _, err := image.Decode(inputFile)
183+
// Decode all frames from the AVIF
184+
a, err := avif.DecodeAll(inputFile)
184185
if err != nil {
185186
return nil, nil, err
186187
}
@@ -189,20 +190,40 @@ func decodeAvif(input, output string) (image.Image, os.FileInfo, error) {
189190
if err != nil {
190191
return nil, nil, err
191192
}
192-
193193
defer outputFile.Close()
194194

195-
switch ext {
196-
case ".bmp":
197-
err = bmp.Encode(outputFile, img)
198-
case ".gif":
199-
err = gif.Encode(outputFile, img, nil)
200-
case ".jpg", ".jpeg":
201-
err = jpeg.Encode(outputFile, img, nil)
202-
case ".png":
203-
err = png.Encode(outputFile, img)
204-
case ".tiff":
205-
err = tiff.Encode(outputFile, img, nil)
195+
// Animated AVIF → animated GIF
196+
if ext == ".gif" && len(a.Image) > 1 {
197+
g := &gif.GIF{
198+
LoopCount: a.LoopCount,
199+
Delay: a.Delay,
200+
Image: make([]*image.Paletted, len(a.Image)),
201+
}
202+
203+
for i, frame := range a.Image {
204+
bounds := frame.Bounds()
205+
paletted := image.NewPaletted(bounds, palette.Plan9)
206+
draw.FloydSteinberg.Draw(paletted, bounds, frame, bounds.Min)
207+
g.Image[i] = paletted
208+
}
209+
210+
err = gif.EncodeAll(outputFile, g)
211+
} else {
212+
// Single frame or non-GIF output: use first frame
213+
img := a.Image[0]
214+
215+
switch ext {
216+
case ".bmp":
217+
err = bmp.Encode(outputFile, img)
218+
case ".gif":
219+
err = gif.Encode(outputFile, img, nil)
220+
case ".jpg", ".jpeg":
221+
err = jpeg.Encode(outputFile, img, nil)
222+
case ".png":
223+
err = png.Encode(outputFile, img)
224+
case ".tiff":
225+
err = tiff.Encode(outputFile, img, nil)
226+
}
206227
}
207228

208229
if err != nil {
@@ -214,5 +235,5 @@ func decodeAvif(input, output string) (image.Image, os.FileInfo, error) {
214235
return nil, nil, err
215236
}
216237

217-
return img, info, nil
238+
return a.Image[0], info, nil
218239
}

decoder.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,42 @@ func Decode(reader io.Reader) (image.Image, error) {
2727
return decodeAVIFToRGBA(data)
2828
}
2929

30+
// DecodeAll reads AVIF data from the provided io.Reader and decodes all frames into an AVIF struct.
31+
//
32+
// For still images, the returned AVIF struct will contain a single frame.
33+
// For image sequences (animations), it will contain all frames with their timing and loop information.
34+
func DecodeAll(reader io.Reader) (*AVIF, error) {
35+
data, err := io.ReadAll(reader)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to decode AVIF data: %w", err)
38+
}
39+
40+
frames, delays, repetitionCount, err := decodeAllAVIFToRGBA(data)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
// Convert []*image.RGBA to []image.Image
46+
images := make([]image.Image, len(frames))
47+
for i, f := range frames {
48+
images[i] = f
49+
}
50+
51+
// Map AVIF repetitionCount to LoopCount (inverse of EncodeAll mapping)
52+
loopCount := 0 // default: infinite
53+
if repetitionCount == 0 {
54+
loopCount = -1 // play once
55+
} else if repetitionCount > 0 {
56+
loopCount = repetitionCount
57+
}
58+
59+
return &AVIF{
60+
Image: images,
61+
Delay: delays,
62+
LoopCount: loopCount,
63+
}, nil
64+
}
65+
3066
// DecodeConfig reads the configuration of an AVIF image from the provided io.Reader.
3167
//
3268
// It returns an image.Config containing the width, height, and color model of the image, or an error if the

0 commit comments

Comments
 (0)