From 8247ca012bbcf81245c1fd7a83cc9e58580bc5d7 Mon Sep 17 00:00:00 2001 From: commandblockguy Date: Thu, 2 Jun 2022 00:18:43 -0500 Subject: [PATCH] Add support for APNG files --- apng.cpp | 457 +++++++++++++++++++++++++++++++++++++++ apng.go | 210 ++++++++++++++++++ apng.hpp | 43 ++++ deps/build-deps-linux.sh | 2 + lilliput.go | 14 ++ opencv.go | 9 +- 6 files changed, 732 insertions(+), 3 deletions(-) create mode 100644 apng.cpp create mode 100644 apng.go create mode 100644 apng.hpp diff --git a/apng.cpp b/apng.cpp new file mode 100644 index 00000000..3c2b417a --- /dev/null +++ b/apng.cpp @@ -0,0 +1,457 @@ +#include "apng.hpp" + +#include +#include +#include + +struct apng_frame_header { + uint32_t width; + uint32_t height; + uint32_t x_offset; + uint32_t y_offset; + uint16_t delay_num; + uint16_t delay_den; + uint8_t dispose_op; + uint8_t blend_op; +}; + +struct apng_decoder_struct { + png_structp png_ptr; + png_infop info_ptr; + const cv::Mat* mat; + size_t read_pos; + uint32_t frame_num; + struct apng_frame_header frame_header; + uint8_t *prev_frame; + png_bytepp prev_rows; +}; + +struct apng_encoder_struct { + png_structp png_ptr; + png_infop info_ptr; + uint8_t *buf; + size_t buf_size; + size_t write_pos; + cv::Mat* prev_frame; +}; + +void user_read_data(png_structp png_ptr, + png_bytep data, png_size_t length) { + apng_decoder d = (apng_decoder) png_get_io_ptr(png_ptr); + if (d->read_pos + length > d->mat->total()) { + png_error(png_ptr, "Tried to read PNG data past end of file"); + } + memcpy(data, &d->mat->data[d->read_pos], length); + d->read_pos += length; +} + +void user_write_data(png_structp png_ptr, + png_bytep data, png_size_t length) { + apng_encoder e = (apng_encoder) png_get_io_ptr(png_ptr); + if (e->write_pos + length >= e->buf_size) { + png_error(png_ptr, "Tried to write PNG data past end of buffer"); + } + memcpy(&e->buf[e->write_pos], data, length); + e->write_pos += length; +} + +void user_flush_data(png_structp png_ptr) { + // Do nothing, since this isn't actually a file +} + +apng_decoder apng_decoder_create(const opencv_mat buf) { + uint32_t width, height; + apng_decoder d = new struct apng_decoder_struct(); + memset(d, 0, sizeof(struct apng_decoder_struct)); + d->mat = static_cast(buf); + + if (png_sig_cmp(d->mat->data, 0, 8) != 0) + goto error; + + d->png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!d->png_ptr) + goto error; + + d->info_ptr = png_create_info_struct(d->png_ptr); + if (!d->info_ptr) + goto error; + + if (setjmp(png_jmpbuf(d->png_ptr))) { + goto error; + } + + png_set_read_fn(d->png_ptr, d, user_read_data); + + png_read_info(d->png_ptr, d->info_ptr); + + // Apply transformations to image data + png_set_expand(d->png_ptr); + png_set_strip_16(d->png_ptr); + png_set_gray_to_rgb(d->png_ptr); + png_set_add_alpha(d->png_ptr, 0xFF, PNG_FILLER_AFTER); + png_set_bgr(d->png_ptr); + + png_read_update_info(d->png_ptr, d->info_ptr); + + if(!png_get_valid(d->png_ptr, d->info_ptr, PNG_INFO_acTL)) + goto error; + + width = png_get_image_width(d->png_ptr, d->info_ptr); + height = png_get_image_height(d->png_ptr, d->info_ptr); + + d->prev_frame = (uint8_t *) calloc(4, width * height); + if (!d->prev_frame) + goto error; + + d->prev_rows = (png_bytepp) calloc(sizeof (png_bytep), height); + if (!d->prev_rows) + goto error; + + for (int i = 0; i < height; i++) { + d->prev_rows[i] = &d->prev_frame[i * 4 * width]; + } + + return d; + +error: + apng_decoder_release(d); + return NULL; +} + +int apng_decoder_get_width(const apng_decoder d) { + return png_get_image_width(d->png_ptr, d->info_ptr); +} + +int apng_decoder_get_height(const apng_decoder d) { + return png_get_image_height(d->png_ptr, d->info_ptr); +} + +int apng_decoder_get_num_frames(const apng_decoder d) { + return png_get_num_frames(d->png_ptr, d->info_ptr); +} + +int apng_decoder_get_frame_width(const apng_decoder d) { + return d->frame_header.width; +} + +int apng_decoder_get_frame_height(const apng_decoder d) { + return d->frame_header.height; +} + +int apng_decoder_get_prev_frame_delay_num(const apng_decoder d) { + return d->frame_header.delay_num; +} + +int apng_decoder_get_prev_frame_delay_den(const apng_decoder d) { + return d->frame_header.delay_den; +} + +void apng_decoder_release(apng_decoder d) { + png_destroy_read_struct(&d->png_ptr, &d->info_ptr, NULL); + free(d->prev_frame); + free(d->prev_rows); + delete d; +} + +apng_decoder_frame_state apng_decoder_decode_frame_header(apng_decoder d) { + if(setjmp(png_jmpbuf(d->png_ptr))) { + return apng_decoder_error; + } + + if (d->frame_num >= png_get_num_frames(d->png_ptr, d->info_ptr)) { + return apng_decoder_eof; + } + + png_read_frame_head(d->png_ptr, d->info_ptr); + + if (png_get_valid(d->png_ptr, d->info_ptr, PNG_INFO_fcTL)) { + png_get_next_frame_fcTL(d->png_ptr, d->info_ptr, + &d->frame_header.width, &d->frame_header.height, + &d->frame_header.x_offset, &d->frame_header.y_offset, + &d->frame_header.delay_num, &d->frame_header.delay_den, + &d->frame_header.dispose_op, &d->frame_header.blend_op); + } + // todo: case where first frame has no fcTL and is therefore not the first frame of the image + + return apng_decoder_have_next_frame; +} + +void BlendOver(unsigned char * dst, unsigned int dst_width, unsigned char ** rows_src, unsigned int x, unsigned int y, unsigned int w, unsigned int h) +{ + unsigned int i, j; + int u, v, al; + + for (j=0; j(mat); + + uint8_t *dst = cvMat->data; + + uint32_t image_width = apng_decoder_get_width(d); + uint32_t image_height = apng_decoder_get_height(d); + + uint8_t *frame = (uint8_t *) malloc(4 * d->frame_header.width * d->frame_header.height); + if (!frame) return false; + png_bytepp row_pointers = (png_bytepp) malloc(sizeof(png_bytep) * d->frame_header.height); + if (!row_pointers) { + free(frame); + return false; + } + for(int i = 0; i < d->frame_header.height; i++) { + row_pointers[i] = &frame[i * 4 * d->frame_header.width]; + } + + png_read_image(d->png_ptr, row_pointers); + + memcpy(dst, d->prev_frame, 4 * image_width * image_height); + + switch (d->frame_header.blend_op) { + case PNG_BLEND_OP_SOURCE: + for (int i = 0; i < d->frame_header.height; i++) { + uint8_t row_offset = (d->frame_header.y_offset + i) * image_width; + uint8_t *pos = dst + (row_offset + d->frame_header.x_offset) * 4; + memcpy(pos, row_pointers[i], 4 * d->frame_header.width); + } + break; + case PNG_BLEND_OP_OVER: + BlendOver(dst, image_width, row_pointers, + d->frame_header.x_offset, d->frame_header.y_offset, + d->frame_header.width, d->frame_header.height); + break; + } + + switch (d->frame_header.dispose_op) { + case PNG_DISPOSE_OP_NONE: + memcpy(d->prev_frame, dst, 4 * image_width * image_height); + break; + case PNG_DISPOSE_OP_BACKGROUND: + memset(d->prev_frame, 0, 4 * image_width * image_height); + break; + case PNG_DISPOSE_OP_PREVIOUS: + // No-op - don't bother updating previous + break; + } + + free(row_pointers); + free(frame); + + d->frame_num++; + + return true; +} + +apng_decoder_frame_state apng_decoder_skip_frame(apng_decoder d) { + if(setjmp(png_jmpbuf(d->png_ptr))) { + return apng_decoder_error; + } + + if (d->frame_num >= png_get_num_frames(d->png_ptr, d->info_ptr)) { + return apng_decoder_eof; + } + + png_read_frame_head(d->png_ptr, d->info_ptr); + + return apng_decoder_have_next_frame; +} + +apng_encoder apng_encoder_create(void* buf, size_t buf_len) { + apng_encoder e = new struct apng_encoder_struct(); + memset(e, 0, sizeof(struct apng_encoder_struct)); + + e->buf = (uint8_t *) buf; + e->buf_size = buf_len; + + e->png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!e->png_ptr) + goto error; + + e->info_ptr = png_create_info_struct(e->png_ptr); + if (!e->info_ptr) + goto error; + + if (setjmp(png_jmpbuf(e->png_ptr))) { + goto error; + } + + png_set_write_fn(e->png_ptr, e, user_write_data, user_flush_data); + + return e; +error: + png_destroy_write_struct(&e->png_ptr, &e->info_ptr); + delete e; + return NULL; +} + +bool apng_encoder_init(apng_encoder e, int width, int height, int num_frames) { + if (setjmp(png_jmpbuf(e->png_ptr))) { + return false; + } + + png_set_IHDR(e->png_ptr, e->info_ptr, width, height, + 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + png_set_acTL(e->png_ptr, e->info_ptr, num_frames, 0); + + png_write_info(e->png_ptr, e->info_ptr); + png_set_bgr(e->png_ptr); + + e->prev_frame = new cv::Mat(height, width, CV_8UC4); + + return true; +} + +void apng_find_diff_bounds(struct apng_frame_header *hdr, const cv::Mat* frame, const cv::Mat* prev_frame) { + // count identical rows on top + for (int i = 0; i < frame->rows; i++) { + if (memcmp(frame->ptr(i, 0), prev_frame->ptr(i, 0), 4 * frame->cols) != 0) { + hdr->y_offset = i; + break; + } + } + + // count identical rows on bottom + for (int i = frame->rows - 1; i >= 0; i--) { + if (memcmp(frame->ptr(i, 0), prev_frame->ptr(i, 0), 4 * frame->cols) != 0) { + hdr->height = i - hdr->y_offset + 1; + break; + } + } + + // count identical cols on left + for (int i = 0; i < frame->cols; i++) { + bool differs = false; + for (int y = hdr->y_offset; y < hdr->y_offset + hdr->height; y++) { + if (memcmp(frame->ptr(y, i), prev_frame->ptr(y, i), 4) != 0) { + differs = true; + break; + } + } + if (differs) { + hdr->x_offset = i; + break; + } + } + + // count identical cols on right + for (int i = frame->cols - 1; i >= 0; i--) { + bool differs = false; + for (int y = hdr->y_offset; y < hdr->y_offset + hdr->height; y++) { + if (memcmp(frame->ptr(y, i), prev_frame->ptr(y, i), 4) != 0) { + differs = true; + break; + } + } + if (differs) { + hdr->width = i - hdr->x_offset + 1; + break; + } + } +} + +void apng_diff_frame(uint8_t *out, const struct apng_frame_header *hdr, const cv::Mat* frame, const cv::Mat* prev_frame) { + for (int y = hdr->y_offset; y < hdr->y_offset + hdr->height; y++) { + for (int x = hdr->x_offset; x < hdr->x_offset + hdr->width; x++) { + if (memcmp(frame->ptr(y, x), prev_frame->ptr(y, x), 4) == 0) { + // Colors match, emit transparent pixel + memset(out, 0, 4); + out += 4; + } else { + memcpy(out, frame->ptr(y, x), 3); + out += 3; + // fully opaque pixel + *out++ = 0xFF; + } + } + } +} + +bool apng_encoder_encode_frame(apng_encoder e, const opencv_mat frame, int ms) { + auto mat = static_cast(frame); + + if (setjmp(png_jmpbuf(e->png_ptr))) { + printf("jumpbuf happened\n"); + return false; + } + + struct apng_frame_header hdr; + + // find the smallest rectangle of changed pixels + apng_find_diff_bounds(&hdr, mat, e->prev_frame); + + // todo: handle case where new frame is partially transparent + + uint8_t *buf = (uint8_t*) calloc(4, hdr.width * hdr.height); + // copy differing pixels into frame + apng_diff_frame(buf, &hdr, mat, e->prev_frame); + + png_bytepp row_pointers = (png_bytepp) png_malloc(e->png_ptr, sizeof(png_bytep) * mat->rows); + if (!row_pointers) return false; + for(int i = 0; i < hdr.height; i++) { + row_pointers[i] = buf + i * hdr.width * 4; + } + + png_write_frame_head(e->png_ptr, e->info_ptr, row_pointers, + hdr.width, hdr.height, + hdr.x_offset, hdr.y_offset, + ms, 1000, /* Delay */ + PNG_DISPOSE_OP_NONE, + PNG_BLEND_OP_OVER); + + png_write_image(e->png_ptr, row_pointers); + png_write_frame_tail(e->png_ptr, e->info_ptr); + + mat->copyTo(*e->prev_frame); + + free(row_pointers); + + return true; +} + +bool apng_encoder_flush(apng_encoder e) { + if (setjmp(png_jmpbuf(e->png_ptr))) { + return false; + } + + png_write_end(e->png_ptr, NULL); + + return true; +} + +void apng_encoder_release(apng_encoder e) { + png_destroy_write_struct(&e->png_ptr, &e->info_ptr); + delete e->prev_frame; + delete e; +} + +int apng_encoder_get_output_length(apng_encoder e) { + return e->write_pos; +} \ No newline at end of file diff --git a/apng.go b/apng.go new file mode 100644 index 00000000..114b7783 --- /dev/null +++ b/apng.go @@ -0,0 +1,210 @@ +package lilliput + +// #cgo CFLAGS: -msse -msse2 -msse3 -msse4.1 -msse4.2 -mavx +// #cgo darwin CFLAGS: -I${SRCDIR}/deps/osx/include +// #cgo linux CFLAGS: -I${SRCDIR}/deps/linux/include +// #cgo CXXFLAGS: -std=c++11 +// #cgo darwin CXXFLAGS: -I${SRCDIR}/deps/osx/include +// #cgo linux CXXFLAGS: -I${SRCDIR}/deps/linux/include +// #cgo LDFLAGS: -lopencv_core -lopencv_imgcodecs -lopencv_imgproc -ljpeg -lpng -lwebp -lippicv -lz -lgif +// #cgo darwin LDFLAGS: -L${SRCDIR}/deps/osx/lib -L${SRCDIR}/deps/osx/share/OpenCV/3rdparty/lib +// #cgo linux LDFLAGS: -L${SRCDIR}/deps/linux/lib -L${SRCDIR}/deps/linux/share/OpenCV/3rdparty/lib +// #include "apng.hpp" +import "C" + +import ( + "io" + "sync/atomic" + "time" + "unsafe" +) + +type apngDecoder struct { + decoder C.apng_decoder + mat C.opencv_mat + buf []byte + frameIndex int +} + +type apngEncoder struct { + encoder C.apng_encoder + buf []byte + totalFrames int + frameIndex int + hasFlushed bool +} + +var ( + apngMaxFrameDimension uint64 +) + +// SetAPNGMaxFrameDimension sets the largest APNG width/height that can be +// decoded +func SetAPNGMaxFrameDimension(dim uint64) { + // TODO we should investigate if this can be removed/become a mat check in decoder + atomic.StoreUint64(&apngMaxFrameDimension, dim) +} + +func newApngDecoder(buf []byte) (*apngDecoder, error) { + mat := C.opencv_mat_create_from_data(C.int(len(buf)), 1, C.CV_8U, unsafe.Pointer(&buf[0]), C.size_t(len(buf))) + + if mat == nil { + return nil, ErrBufTooSmall + } + + decoder := C.apng_decoder_create(mat) + if decoder == nil { + return nil, ErrInvalidImage + } + + return &apngDecoder{ + decoder: decoder, + mat: mat, + buf: buf, + frameIndex: 0, + }, nil +} + +func (d *apngDecoder) Header() (*ImageHeader, error) { + return &ImageHeader{ + width: int(C.apng_decoder_get_width(d.decoder)), + height: int(C.apng_decoder_get_height(d.decoder)), + pixelType: PixelType(C.CV_8UC4), + orientation: OrientationTopLeft, + numFrames: int(C.apng_decoder_get_num_frames(d.decoder)), + }, nil +} + +func (d *apngDecoder) FrameHeader() (*ImageHeader, error) { + return &ImageHeader{ + width: int(C.apng_decoder_get_frame_width(d.decoder)), + height: int(C.apng_decoder_get_frame_height(d.decoder)), + pixelType: PixelType(C.CV_8UC4), + orientation: OrientationTopLeft, + numFrames: 1, + }, nil +} + +func (d *apngDecoder) Close() { + C.apng_decoder_release(d.decoder) + C.opencv_mat_release(d.mat) + d.buf = nil +} + +func (d *apngDecoder) Description() string { + return "APNG" +} + +func (d *apngDecoder) Duration() time.Duration { + return time.Duration(0) +} + +func (d *apngDecoder) DecodeTo(f *Framebuffer) error { + h, err := d.Header() + if err != nil { + return err + } + + err = f.resizeMat(h.Width(), h.Height(), h.PixelType()) + if err != nil { + return err + } + + nextFrameResult := int(C.apng_decoder_decode_frame_header(d.decoder)) + if nextFrameResult == C.apng_decoder_eof { + return io.EOF + } + if nextFrameResult == C.apng_decoder_error { + return ErrInvalidImage + } + + frameHeader, err := d.FrameHeader() + if err != nil { + return ErrInvalidImage + } + maxDim := int(atomic.LoadUint64(&apngMaxFrameDimension)) + if frameHeader.Width() > maxDim || frameHeader.Height() > maxDim { + return ErrInvalidImage + } + + ret := C.apng_decoder_decode_frame(d.decoder, f.mat) + if !ret { + return ErrDecodingFailed + } + num := C.apng_decoder_get_prev_frame_delay_num(d.decoder) + den := C.apng_decoder_get_prev_frame_delay_den(d.decoder) + f.duration = time.Second * time.Duration(num) / time.Duration(den) + d.frameIndex++ + return nil +} + +func (d *apngDecoder) SkipFrame() error { + nextFrameResult := int(C.apng_decoder_skip_frame(d.decoder)) + + if nextFrameResult == C.apng_decoder_eof { + return io.EOF + } + if nextFrameResult == C.apng_decoder_error { + return ErrInvalidImage + } + + return nil +} + +func newApngEncoder(decodedBy Decoder, buf []byte) (*apngEncoder, error) { + buf = buf[:1] + enc := C.apng_encoder_create(unsafe.Pointer(&buf[0]), C.size_t(cap(buf))) + if enc == nil { + return nil, ErrBufTooSmall + } + + hdr, err := decodedBy.Header() + if err != nil { + return nil, err + } + + return &apngEncoder{ + encoder: enc, + buf: buf, + totalFrames: hdr.numFrames, // todo: this is not necessarily accurate + frameIndex: 0, + }, nil +} + +func (e *apngEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { + if e.hasFlushed { + return nil, io.EOF + } + + if f == nil { + ret := C.apng_encoder_flush(e.encoder) + if !ret { + return nil, ErrInvalidImage + } + e.hasFlushed = true + + len := C.int(C.apng_encoder_get_output_length(e.encoder)) + + return e.buf[:len], nil + } + + if e.frameIndex == 0 { + C.apng_encoder_init(e.encoder, C.int(f.Width()), C.int(f.Height()), C.int(e.totalFrames)) + } + + if !C.apng_encoder_encode_frame(e.encoder, f.mat, C.int(f.duration.Milliseconds())) { + return nil, ErrInvalidImage + } + + e.frameIndex++ + + return nil, nil +} + +func (e *apngEncoder) Close() { + C.apng_encoder_release(e.encoder) +} + +func init() { + SetAPNGMaxFrameDimension(defaultMaxFrameDimension) +} diff --git a/apng.hpp b/apng.hpp new file mode 100644 index 00000000..42200c81 --- /dev/null +++ b/apng.hpp @@ -0,0 +1,43 @@ +#ifndef LILLIPUT_GIFLIB_HPP +#define LILLIPUT_GIFLIB_HPP + +#include "opencv.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct apng_decoder_struct* apng_decoder; +typedef struct apng_encoder_struct* apng_encoder; + +typedef enum { + apng_decoder_have_next_frame, + apng_decoder_eof, + apng_decoder_error, +} apng_decoder_frame_state; + +apng_decoder apng_decoder_create(const opencv_mat buf); +int apng_decoder_get_width(const apng_decoder d); +int apng_decoder_get_height(const apng_decoder d); +int apng_decoder_get_num_frames(const apng_decoder d); +int apng_decoder_get_frame_width(const apng_decoder d); +int apng_decoder_get_frame_height(const apng_decoder d); +int apng_decoder_get_prev_frame_delay_num(const apng_decoder d); +int apng_decoder_get_prev_frame_delay_den(const apng_decoder d); +void apng_decoder_release(apng_decoder d); +apng_decoder_frame_state apng_decoder_decode_frame_header(apng_decoder d); +bool apng_decoder_decode_frame(apng_decoder d, opencv_mat mat); +apng_decoder_frame_state apng_decoder_skip_frame(apng_decoder d); + +apng_encoder apng_encoder_create(void* buf, size_t buf_len); +bool apng_encoder_init(apng_encoder e, int width, int height, int num_frames); +bool apng_encoder_encode_frame(apng_encoder e, const opencv_mat frame, int ms); +bool apng_encoder_flush(apng_encoder e); +void apng_encoder_release(apng_encoder e); +int apng_encoder_get_output_length(apng_encoder e); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/build-deps-linux.sh b/deps/build-deps-linux.sh index 2c3e5986..ba3c3b81 100755 --- a/deps/build-deps-linux.sh +++ b/deps/build-deps-linux.sh @@ -46,6 +46,8 @@ make install mkdir -p $BASEDIR/libpng tar -xzf $SRCDIR/libpng-1.6.29.tar.gz -C $BASEDIR/libpng --strip-components 1 +cd $BASEDIR/libpng +patch -Np1 -i $SRCDIR/libpng-1.6.29-apng.patch mkdir -p $BUILDDIR/libpng cd $BUILDDIR/libpng CPPFLAGS="-I$PREFIX/include" LDFLAGS="-L$PREFIX/lib" $BASEDIR/libpng/configure --prefix=$PREFIX --disable-shared --enable-static --enable-intel-sse diff --git a/lilliput.go b/lilliput.go index a6b7ed5c..d3037943 100644 --- a/lilliput.go +++ b/lilliput.go @@ -82,6 +82,10 @@ func NewDecoder(buf []byte) (Decoder, error) { return nil, ErrInvalidImage } + if detectAPNG(buf) { + return newApngDecoder(buf) + } + isBufGIF := isGIF(buf) if isBufGIF { return newGifDecoder(buf) @@ -104,6 +108,16 @@ func NewEncoder(ext string, decodedBy Decoder, dst []byte) (Encoder, error) { return newGifEncoder(decodedBy, dst) } + if strings.ToLower(ext) == ".apng" { + return newApngEncoder(decodedBy, dst) + } + + header, err := decodedBy.Header() + + if err == nil && strings.ToLower(ext) == ".png" && header.numFrames > 1 { + return newApngEncoder(decodedBy, dst) + } + if strings.ToLower(ext) == ".mp4" || strings.ToLower(ext) == ".webm" { return nil, errors.New("Encoder cannot encode into video types") } diff --git a/opencv.go b/opencv.go index e40f5793..c22e6c75 100644 --- a/opencv.go +++ b/opencv.go @@ -190,6 +190,9 @@ func (f *Framebuffer) ResizeTo(width, height int, dst *Framebuffer) error { return err } C.opencv_mat_resize(f.mat, dst.mat, C.int(width), C.int(height), C.CV_INTER_AREA) + + dst.duration = f.duration + return nil } @@ -245,6 +248,9 @@ func (f *Framebuffer) Fit(width, height int, dst *Framebuffer) error { return err } C.opencv_mat_resize(newMat, dst.mat, C.int(width), C.int(height), C.CV_INTER_AREA) + + dst.duration = f.duration + return nil } @@ -324,9 +330,6 @@ func (d *openCVDecoder) Header() (*ImageHeader, error) { d.hasReadHeader = true numFrames := 1 - if detectAPNG(d.buf) { - numFrames = 2 - } return &ImageHeader{ width: int(C.opencv_decoder_get_width(d.decoder)),