diff --git a/README.md b/README.md index 0ab44fd..e8ef8ef 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ func main() { ## TODO - Incremental decoding API -- Animation decoding ## License diff --git a/examples/images/weather-anim-frames/00.png b/examples/images/weather-anim-frames/00.png new file mode 100644 index 0000000..9112505 Binary files /dev/null and b/examples/images/weather-anim-frames/00.png differ diff --git a/examples/images/weather-anim-frames/01.png b/examples/images/weather-anim-frames/01.png new file mode 100644 index 0000000..97bd198 Binary files /dev/null and b/examples/images/weather-anim-frames/01.png differ diff --git a/examples/images/weather-anim-frames/02.png b/examples/images/weather-anim-frames/02.png new file mode 100644 index 0000000..9974039 Binary files /dev/null and b/examples/images/weather-anim-frames/02.png differ diff --git a/examples/images/weather-anim-frames/03.png b/examples/images/weather-anim-frames/03.png new file mode 100644 index 0000000..bc43b23 Binary files /dev/null and b/examples/images/weather-anim-frames/03.png differ diff --git a/examples/images/weather-anim-frames/04.png b/examples/images/weather-anim-frames/04.png new file mode 100644 index 0000000..3349d1e Binary files /dev/null and b/examples/images/weather-anim-frames/04.png differ diff --git a/examples/images/weather-anim-frames/05.png b/examples/images/weather-anim-frames/05.png new file mode 100644 index 0000000..d8cf321 Binary files /dev/null and b/examples/images/weather-anim-frames/05.png differ diff --git a/examples/images/weather-anim-frames/06.png b/examples/images/weather-anim-frames/06.png new file mode 100644 index 0000000..e2a9513 Binary files /dev/null and b/examples/images/weather-anim-frames/06.png differ diff --git a/examples/images/weather-anim-frames/07.png b/examples/images/weather-anim-frames/07.png new file mode 100644 index 0000000..5d19e9f Binary files /dev/null and b/examples/images/weather-anim-frames/07.png differ diff --git a/examples/images/weather-anim-frames/08.png b/examples/images/weather-anim-frames/08.png new file mode 100644 index 0000000..e763b3a Binary files /dev/null and b/examples/images/weather-anim-frames/08.png differ diff --git a/examples/images/weather-anim-frames/09.png b/examples/images/weather-anim-frames/09.png new file mode 100644 index 0000000..da4e516 Binary files /dev/null and b/examples/images/weather-anim-frames/09.png differ diff --git a/examples/images/weather-anim-frames/10.png b/examples/images/weather-anim-frames/10.png new file mode 100644 index 0000000..063a65f Binary files /dev/null and b/examples/images/weather-anim-frames/10.png differ diff --git a/examples/images/weather-anim-frames/11.png b/examples/images/weather-anim-frames/11.png new file mode 100644 index 0000000..ca9252b Binary files /dev/null and b/examples/images/weather-anim-frames/11.png differ diff --git a/examples/images/weather-anim-frames/12.png b/examples/images/weather-anim-frames/12.png new file mode 100644 index 0000000..2bc5fda Binary files /dev/null and b/examples/images/weather-anim-frames/12.png differ diff --git a/examples/images/weather-anim-frames/13.png b/examples/images/weather-anim-frames/13.png new file mode 100644 index 0000000..c5ff9ca Binary files /dev/null and b/examples/images/weather-anim-frames/13.png differ diff --git a/examples/images/weather-anim-frames/14.png b/examples/images/weather-anim-frames/14.png new file mode 100644 index 0000000..b3053fd Binary files /dev/null and b/examples/images/weather-anim-frames/14.png differ diff --git a/examples/images/weather-anim-frames/15.png b/examples/images/weather-anim-frames/15.png new file mode 100644 index 0000000..f71bb7f Binary files /dev/null and b/examples/images/weather-anim-frames/15.png differ diff --git a/examples/images/weather-anim-frames/16.png b/examples/images/weather-anim-frames/16.png new file mode 100644 index 0000000..de05762 Binary files /dev/null and b/examples/images/weather-anim-frames/16.png differ diff --git a/examples/images/weather-anim-frames/17.png b/examples/images/weather-anim-frames/17.png new file mode 100644 index 0000000..e2d1851 Binary files /dev/null and b/examples/images/weather-anim-frames/17.png differ diff --git a/examples/images/weather-anim.webp b/examples/images/weather-anim.webp new file mode 100644 index 0000000..e015a54 Binary files /dev/null and b/examples/images/weather-anim.webp differ diff --git a/webp/anim_decode.go b/webp/anim_decode.go new file mode 100644 index 0000000..e013de7 --- /dev/null +++ b/webp/anim_decode.go @@ -0,0 +1,123 @@ +package webp + +/* +#cgo LDFLAGS: -lwebpdemux + +#include +#include +*/ +import "C" + +import ( + "encoding/binary" + "errors" + "image" + "image/color" + "unsafe" +) + +// AnimationDecoder decodes an animated WebP. +type AnimationDecoder struct { + opts C.WebPAnimDecoderOptions + c *C.WebPAnimDecoder + cData *C.WebPData +} + +// AnimationInfo represents properties of an animation. +type AnimationInfo struct { + CanvasWidth int + CanvasHeight int + LoopCount int + FrameCount int + BackgroundColor color.RGBA +} + +// Animation represents a decoded WebP animation. +type Animation struct { + AnimationInfo + + // Image is the list of decoded frames. + Image []*image.RGBA + + // Timestamp of each frame in milliseconds. + Timestamp []int +} + +// NewAnimationDecoder initializes a new decoder. +func NewAnimationDecoder(data []byte) (*AnimationDecoder, error) { + ad := &AnimationDecoder{} + + if C.WebPAnimDecoderOptionsInit(&ad.opts) == 0 { + return nil, errors.New("failed to initialize animation decoder config") + } + ad.opts.color_mode = C.MODE_RGBA + + ad.cData = &C.WebPData{} + C.WebPDataInit(ad.cData) + + ad.cData.bytes = (*C.uint8_t)(C.CBytes(data)) + ad.cData.size = (C.size_t)(len(data)) + + ad.c = C.WebPAnimDecoderNew(ad.cData, &ad.opts) + if ad.c == nil { + C.free(unsafe.Pointer(ad.cData.bytes)) + return nil, errors.New("failed to initialize animation decoder") + } + + return ad, nil +} + +// GetInfo retrieves properties of the animation. +func (ad *AnimationDecoder) GetInfo() (*AnimationInfo, error) { + info := &C.WebPAnimInfo{} + + if C.WebPAnimDecoderGetInfo(ad.c, info) == 0 { + return nil, errors.New("error in WebPAnimDecoderGetInfo") + } + + b := make([]uint8, 4) + binary.BigEndian.PutUint32(b, uint32(info.bgcolor)) + + return &AnimationInfo{ + CanvasWidth: int(info.canvas_width), + CanvasHeight: int(info.canvas_height), + LoopCount: int(info.loop_count), + FrameCount: int(info.frame_count), + BackgroundColor: color.RGBA{b[0], b[1], b[2], b[3]}, + }, nil +} + +// Decode decodes a WebP animation. +func (ad *AnimationDecoder) Decode() (*Animation, error) { + info, err := ad.GetInfo() + if err != nil { + return nil, err + } + + anim := &Animation{ + AnimationInfo: *info, + } + + for C.WebPAnimDecoderHasMoreFrames(ad.c) != 0 { + var ts C.int + var pix *C.uint8_t + + if C.WebPAnimDecoderGetNext(ad.c, &pix, &ts) == 0 { + return nil, errors.New("error in WebPAnimDecoderGetNext") + } + + img := image.NewRGBA(image.Rect(0, 0, info.CanvasWidth, info.CanvasHeight)) + C.memcpy(unsafe.Pointer(&img.Pix[0]), unsafe.Pointer(pix), C.size_t(len(img.Pix))) + + anim.Image = append(anim.Image, img) + anim.Timestamp = append(anim.Timestamp, int(ts)) + } + + return anim, nil +} + +// Close deletes the decoder and frees resources. +func (ad *AnimationDecoder) Close() { + C.free(unsafe.Pointer(ad.cData.bytes)) + C.WebPAnimDecoderDelete(ad.c) +} diff --git a/webp/anim_decode_test.go b/webp/anim_decode_test.go new file mode 100644 index 0000000..5b68103 --- /dev/null +++ b/webp/anim_decode_test.go @@ -0,0 +1,77 @@ +package webp_test + +import ( + "fmt" + "testing" + + "github.com/harukasan/go-libwebp/test/util" + "github.com/harukasan/go-libwebp/webp" +) + +func TestDecodeAnimationInfo(t *testing.T) { + data := util.ReadFile("weather-anim.webp") + + dec, err := webp.NewAnimationDecoder(data) + if err != nil { + t.Fatalf("initializing decoder: %v", err) + } + defer dec.Close() + + info, err := dec.GetInfo() + if err != nil { + t.Fatalf("getting animatiion info: %v", err) + } + + if got := info.CanvasWidth; got != 64 { + t.Errorf("Expected CanvasWidth: %v, but got %v", 64, got) + } + if got := info.CanvasHeight; got != 32 { + t.Errorf("Expected CanvasHeight: %v, but got %v", 32, got) + } + if got := info.LoopCount; got != 0 { + t.Errorf("Expected LoopCount: %v, but got %v", 0, got) + } + if got := info.FrameCount; got != 18 { + t.Errorf("Expected FrameCount: %v, but got %v", 18, got) + } +} + +func TestDecodeAnimation(t *testing.T) { + data := util.ReadFile("weather-anim.webp") + + dec, err := webp.NewAnimationDecoder(data) + if err != nil { + t.Fatalf("initializing decoder: %v", err) + } + defer dec.Close() + + anim, err := dec.Decode() + if err != nil { + t.Fatalf("error decoding: %v", err) + } + + if got := len(anim.Image); got != 18 { + t.Errorf("Expected len(Image): %v, but got %v", 18, got) + } + if got := len(anim.Timestamp); got != 18 { + t.Errorf("Expected len(Timestamp): %v, but got %v", 18, got) + } + + for i := 0; i < 18; i++ { + frame := util.ReadPNG(fmt.Sprintf("weather-anim-frames/%02d.png", i)) + got := anim.Image[i] + + for y := 0; y < got.Bounds().Dy(); y++ { + for x := 0; x < got.Bounds().Dx(); x++ { + if got.At(x, y) != frame.At(x, y) { + t.Fatalf( + "Expected frame %d (%d,%d): %v, but got %v", + i, x, y, + frame.At(x, y), + got.At(x, y), + ) + } + } + } + } +}