diff --git a/Makefile.vc b/Makefile.vc index e973df09..5b5e1f4d 100644 --- a/Makefile.vc +++ b/Makefile.vc @@ -247,6 +247,7 @@ ENC_OBJS = \ $(DIROBJ)\enc\webpenc.obj \ MUX_OBJS = \ + $(DIROBJ)\mux\anim_encode.obj \ $(DIROBJ)\mux\muxedit.obj \ $(DIROBJ)\mux\muxinternal.obj \ $(DIROBJ)\mux\muxread.obj \ diff --git a/makefile.unix b/makefile.unix index 57f2fe46..4a99fa7a 100644 --- a/makefile.unix +++ b/makefile.unix @@ -179,6 +179,7 @@ GIF2WEBP_UTIL_OBJS = \ examples/gif2webp_util.o \ MUX_OBJS = \ + src/mux/anim_encode.o \ src/mux/muxedit.o \ src/mux/muxinternal.o \ src/mux/muxread.o \ diff --git a/src/mux/Makefile.am b/src/mux/Makefile.am index 40c39b43..7d7d5e05 100644 --- a/src/mux/Makefile.am +++ b/src/mux/Makefile.am @@ -1,6 +1,7 @@ lib_LTLIBRARIES = libwebpmux.la libwebpmux_la_SOURCES = +libwebpmux_la_SOURCES += anim_encode.c libwebpmux_la_SOURCES += muxedit.c libwebpmux_la_SOURCES += muxi.h libwebpmux_la_SOURCES += muxinternal.c diff --git a/src/mux/anim_encode.c b/src/mux/anim_encode.c new file mode 100644 index 00000000..b192ee59 --- /dev/null +++ b/src/mux/anim_encode.c @@ -0,0 +1,1129 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the COPYING file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. +// ----------------------------------------------------------------------------- +// +// AnimEncoder implementation. +// + +#include +#include + +#include "../utils/utils.h" +#include "../webp/format_constants.h" +#include "../webp/mux.h" + +//------------------------------------------------------------------------------ +// Internal structs. + +// Stores frame rectangle dimensions. +typedef struct { + int x_offset_, y_offset_, width_, height_; +} FrameRect; + +// Used to store two candidates of encoded data for an animation frame. One of +// the two will be chosen later. +typedef struct { + WebPMuxFrameInfo sub_frame_; // Encoded frame rectangle. + WebPMuxFrameInfo key_frame_; // Encoded frame if it is a key-frame. + int is_key_frame_; // True if 'key_frame' has been chosen. +} EncodedFrame; + +struct WebPAnimEncoder { + const int canvas_width_; // Canvas width. + const int canvas_height_; // Canvas height. + const WebPAnimEncoderOptions options_; // Global encoding options. + + FrameRect prev_webp_rect; // Previous WebP frame rectangle. + + WebPPicture* curr_canvas_; // Only pointer; we don't own memory. + + // Canvas buffers. + WebPPicture curr_canvas_mod_; // Possibly modified current canvas. + WebPPicture prev_canvas_; // Previous canvas. + WebPPicture prev_canvas_disposed_; // Previous canvas disposed to background. + WebPPicture prev_to_prev_canvas_; // Previous to previous canvas. + + // Encoded data. + EncodedFrame* encoded_frames_; // Array of encoded frames. + size_t size_; // Number of allocated frames. + size_t start_; // Frame start index. + size_t count_; // Number of valid frames. + int flush_count_; // If >0, 'flush_count' frames starting from + // 'start' are ready to be added to mux. + + // key-frame related. + int64_t best_delta_; // min(canvas size - frame size) over the frames. + // Can be negative in certain cases due to + // transparent pixels in a frame. + int keyframe_; // Index of selected key-frame relative to 'start_'. + size_t count_since_key_frame_; // Frames seen since the last key-frame. + int prev_candidate_undecided_; // True if it's not yet decided if previous + // frame would be a sub-frame or a key-frame. + + // Misc. + int is_first_frame_; // True if first frame is yet to be added/being added. + size_t frame_count_; // Number of frames added to mux so far. + + WebPMux* mux_; // Muxer to assemble the WebP bitstream. +}; + +// ----------------------------------------------------------------------------- +// Life of WebPAnimEncoder object. + +#define DELTA_INFINITY (1ULL << 32) +#define KEYFRAME_NONE (-1) + +// Reset the counters in the WebPAnimEncoder. +static void ResetCounters(WebPAnimEncoder* const enc) { + enc->start_ = 0; + enc->count_ = 0; + enc->flush_count_ = 0; + enc->best_delta_ = DELTA_INFINITY; + enc->keyframe_ = KEYFRAME_NONE; +} + +static void DisableKeyframes(WebPAnimEncoderOptions* const enc_options) { + enc_options->kmax = ~0; + enc_options->kmin = enc_options->kmax - 1; +} + +#define MAX_CACHED_FRAMES 30 + +static void SanitizeEncoderOptions(WebPAnimEncoderOptions* const enc_options) { + int print_warning = enc_options->verbose; + + if (enc_options->minimize_size) { + DisableKeyframes(enc_options); + } + + if (enc_options->kmin == 0) { + DisableKeyframes(enc_options); + print_warning = 0; + } + if (enc_options->kmax == 0) { + enc_options->kmax = ~0; + print_warning = 0; + } + + if (enc_options->kmin >= enc_options->kmax) { + enc_options->kmin = enc_options->kmax - 1; + if (print_warning) { + fprintf(stderr, "WARNING: Setting kmin = %d, so that kmin < kmax.\n", + (int)enc_options->kmin); + } + } else if (enc_options->kmin < (enc_options->kmax / 2 + 1)) { + // This ensures that enc.keyframe + kmin >= kmax is always true. So, we + // can flush all the frames in the 'count_since_key_frame == kmax' case. + enc_options->kmin = (enc_options->kmax / 2 + 1); + if (print_warning) { + fprintf(stderr, + "WARNING: Setting kmin = %d, so that kmin >= kmax / 2 + 1.\n", + (int)enc_options->kmin); + } + } + // Limit the max number of frames that are allocated. + if (enc_options->kmax - enc_options->kmin > MAX_CACHED_FRAMES) { + enc_options->kmin = enc_options->kmax - MAX_CACHED_FRAMES; + if (print_warning) { + fprintf(stderr, + "WARNING: Setting kmin = %d, so that kmax - kmin <= %d.\n", + (int)enc_options->kmin, MAX_CACHED_FRAMES); + } + } +} + +#undef MAX_CACHED_FRAMES + +static void DefaultEncoderOptions(WebPAnimEncoderOptions* const enc_options) { + enc_options->anim_params.loop_count = 0; + enc_options->anim_params.bgcolor = 0xffffffff; // White. + enc_options->minimize_size = 0; + DisableKeyframes(enc_options); + enc_options->allow_mixed = 0; + enc_options->verbose = 0; +} + +#define TRANSPARENT_COLOR 0x00ffffff + +static void ClearRectangle(WebPPicture* const picture, + int left, int top, int width, int height) { + int j; + for (j = top; j < top + height; ++j) { + uint32_t* const dst = picture->argb + j * picture->argb_stride; + int i; + for (i = left; i < left + width; ++i) { + dst[i] = TRANSPARENT_COLOR; + } + } +} + +static void WebPUtilClearPic(WebPPicture* const picture, + const FrameRect* const rect) { + if (rect != NULL) { + ClearRectangle(picture, rect->x_offset_, rect->y_offset_, + rect->width_, rect->height_); + } else { + ClearRectangle(picture, 0, 0, picture->width, picture->height); + } +} + +WebPAnimEncoder* WebPAnimEncoderNewInternal( + int width, int height, const WebPAnimEncoderOptions* enc_options, + int abi_version) { + WebPAnimEncoder* enc; + + if (WEBP_ABI_IS_INCOMPATIBLE(abi_version, WEBP_MUX_ABI_VERSION)) { + return NULL; + } + if (width <= 0 || height <= 0 || + (width * (uint64_t)height) >= MAX_IMAGE_AREA) { + return NULL; + } + + enc = (WebPAnimEncoder*)WebPSafeCalloc(1, sizeof(*enc)); + if (enc == NULL) return NULL; + // sanity inits, so we can call WebPAnimEncoderDelete(): + enc->encoded_frames_ = NULL; + enc->mux_ = NULL; + + // Dimensions and options. + *(int*)&enc->canvas_width_ = width; + *(int*)&enc->canvas_height_ = height; + if (enc_options != NULL) { + *(WebPAnimEncoderOptions*)&enc->options_ = *enc_options; + SanitizeEncoderOptions((WebPAnimEncoderOptions*)&enc->options_); + } else { + DefaultEncoderOptions((WebPAnimEncoderOptions*)&enc->options_); + } + + // Canvas buffers. + if (!WebPPictureInit(&enc->curr_canvas_mod_) || + !WebPPictureInit(&enc->prev_canvas_) || + !WebPPictureInit(&enc->prev_canvas_disposed_) || + !WebPPictureInit(&enc->prev_to_prev_canvas_)) { + return NULL; + } + enc->curr_canvas_mod_.width = width; + enc->curr_canvas_mod_.height = height; + enc->curr_canvas_mod_.use_argb = 1; + if (!WebPPictureAlloc(&enc->curr_canvas_mod_) || + !WebPPictureCopy(&enc->curr_canvas_mod_, &enc->prev_canvas_) || + !WebPPictureCopy(&enc->curr_canvas_mod_, &enc->prev_canvas_disposed_) || + !WebPPictureCopy(&enc->curr_canvas_mod_, &enc->prev_to_prev_canvas_)) { + goto Err; + } + WebPUtilClearPic(&enc->prev_canvas_, NULL); + WebPUtilClearPic(&enc->prev_to_prev_canvas_, NULL); + + // Encoded frames. + ResetCounters(enc); + // Note: one extra storage is for the previous frame. + enc->size_ = enc->options_.kmax - enc->options_.kmin + 1; + enc->encoded_frames_ = + (EncodedFrame*)WebPSafeCalloc(enc->size_, sizeof(*enc->encoded_frames_)); + if (enc->encoded_frames_ == NULL) goto Err; + + enc->mux_ = WebPMuxNew(); + if (enc->mux_ == NULL) goto Err; + + enc->count_since_key_frame_ = 0; + enc->prev_candidate_undecided_ = 0; + enc->is_first_frame_ = 1; + + return enc; // All OK. + + Err: + WebPAnimEncoderDelete(enc); + return NULL; +} + +// Release the data contained by 'encoded_frame'. +static void FrameRelease(EncodedFrame* const encoded_frame) { + if (encoded_frame != NULL) { + WebPDataClear(&encoded_frame->sub_frame_.bitstream); + WebPDataClear(&encoded_frame->key_frame_.bitstream); + memset(encoded_frame, 0, sizeof(*encoded_frame)); + } +} + +void WebPAnimEncoderDelete(WebPAnimEncoder* enc) { + if (enc != NULL) {; + WebPPictureFree(&enc->curr_canvas_mod_); + WebPPictureFree(&enc->prev_canvas_); + WebPPictureFree(&enc->prev_canvas_disposed_); + WebPPictureFree(&enc->prev_to_prev_canvas_); + if (enc->encoded_frames_ != NULL) { + size_t i; + for (i = 0; i < enc->size_; ++i) { + FrameRelease(&enc->encoded_frames_[i]); + } + WebPSafeFree(enc->encoded_frames_); + } + WebPMuxDelete(enc->mux_); + WebPSafeFree(enc); + } +} + +// ----------------------------------------------------------------------------- +// Frame addition. + +// Initialize frame options to reasonable defaults. +static void DefaultFrameOptions( + WebPAnimEncoderFrameOptions* const frame_options) { + WebPConfigInit(&frame_options->config); + frame_options->config.lossless = 1; +} + +// Returns cached frame at the given 'position'. +static EncodedFrame* GetFrame(const WebPAnimEncoder* const enc, + size_t position) { + assert(enc->start_ + position < enc->size_); + return &enc->encoded_frames_[enc->start_ + position]; +} + +// Returns true if 'length' number of pixels in 'src' and 'dst' are identical, +// assuming the given step sizes between pixels. +static WEBP_INLINE int ComparePixels(const uint32_t* src, int src_step, + const uint32_t* dst, int dst_step, + int length) { + assert(length > 0); + while (length-- > 0) { + if (*src != *dst) { + return 0; + } + src += src_step; + dst += dst_step; + } + return 1; +} + +// Assumes that an initial valid guess of change rectangle 'rect' is passed. +static void MinimizeChangeRectangle(const WebPPicture* const src, + const WebPPicture* const dst, + FrameRect* const rect) { + int i, j; + // Sanity checks. + assert(src->width == dst->width && src->height == dst->height); + assert(rect->x_offset_ + rect->width_ <= dst->width); + assert(rect->y_offset_ + rect->height_ <= dst->height); + + // Left boundary. + for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) { + const uint32_t* const src_argb = + &src->argb[rect->y_offset_ * src->argb_stride + i]; + const uint32_t* const dst_argb = + &dst->argb[rect->y_offset_ * dst->argb_stride + i]; + if (ComparePixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride, + rect->height_)) { + --rect->width_; // Redundant column. + ++rect->x_offset_; + } else { + break; + } + } + if (rect->width_ == 0) goto End; + + // Right boundary. + for (i = rect->x_offset_ + rect->width_ - 1; i >= rect->x_offset_; --i) { + const uint32_t* const src_argb = + &src->argb[rect->y_offset_ * src->argb_stride + i]; + const uint32_t* const dst_argb = + &dst->argb[rect->y_offset_ * dst->argb_stride + i]; + if (ComparePixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride, + rect->height_)) { + --rect->width_; // Redundant column. + } else { + break; + } + } + if (rect->width_ == 0) goto End; + + // Top boundary. + for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) { + const uint32_t* const src_argb = + &src->argb[j * src->argb_stride + rect->x_offset_]; + const uint32_t* const dst_argb = + &dst->argb[j * dst->argb_stride + rect->x_offset_]; + if (ComparePixels(src_argb, 1, dst_argb, 1, rect->width_)) { + --rect->height_; // Redundant row. + ++rect->y_offset_; + } else { + break; + } + } + if (rect->height_ == 0) goto End; + + // Bottom boundary. + for (j = rect->y_offset_ + rect->height_ - 1; j >= rect->y_offset_; --j) { + const uint32_t* const src_argb = + &src->argb[j * src->argb_stride + rect->x_offset_]; + const uint32_t* const dst_argb = + &dst->argb[j * dst->argb_stride + rect->x_offset_]; + if (ComparePixels(src_argb, 1, dst_argb, 1, rect->width_)) { + --rect->height_; // Redundant row. + } else { + break; + } + } + if (rect->height_ == 0) goto End; + + if (rect->width_ == 0 || rect->height_ == 0) { + End: + // TODO(later): This rare case can happen for a bad GIF. In such a case, the + // frame should not be encoded at all and the duration of prev frame should + // be increased instead. For now, we just create a 1x1 frame at zero offset. + rect->x_offset_ = 0; + rect->y_offset_ = 0; + rect->width_ = 1; + rect->height_ = 1; + } +} + +// Snap rectangle to even offsets (and adjust dimensions if needed). +static WEBP_INLINE void SnapToEvenOffsets(FrameRect* const rect) { + rect->width_ += (rect->x_offset_ & 1); + rect->height_ += (rect->y_offset_ & 1); + rect->x_offset_ &= ~1; + rect->y_offset_ &= ~1; +} + +// Given previous and current canvas, picks the optimal rectangle for the +// current frame. The initial guess for 'rect' will be the full canvas. +static int GetSubRect(const WebPPicture* const prev_canvas, + const WebPPicture* const curr_canvas, int is_key_frame, + FrameRect* const rect, WebPPicture* const sub_frame) { + rect->x_offset_ = 0; + rect->y_offset_ = 0; + rect->width_ = curr_canvas->width; + rect->height_ = curr_canvas->height; + if (!is_key_frame) { // Optimize frame rectangle. + MinimizeChangeRectangle(prev_canvas, curr_canvas, rect); + } + SnapToEvenOffsets(rect); + + return WebPPictureView(curr_canvas, rect->x_offset_, rect->y_offset_, + rect->width_, rect->height_, sub_frame); +} + +// TODO: Also used in picture.c. Move to a common location? +// Copy width x height pixels from 'src' to 'dst' honoring the strides. +static void CopyPlane(const uint8_t* src, int src_stride, + uint8_t* dst, int dst_stride, int width, int height) { + while (height-- > 0) { + memcpy(dst, src, width); + src += src_stride; + dst += dst_stride; + } +} + +// Copy pixels from 'src' to 'dst' honoring strides. 'src' and 'dst' are assumed +// to be already allocated. +static void CopyPixels(const WebPPicture* const src, WebPPicture* const dst) { + assert(src->width == dst->width && src->height == dst->height); + assert(src->use_argb && dst->use_argb); + CopyPlane((uint8_t*)src->argb, 4 * src->argb_stride, (uint8_t*)dst->argb, + 4 * dst->argb_stride, 4 * src->width, src->height); +} + +static void DisposeFrameRectangle(int dispose_method, + const FrameRect* const rect, + WebPPicture* const curr_canvas) { + assert(rect != NULL); + if (dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { + WebPUtilClearPic(curr_canvas, rect); + } +} + +static uint32_t RectArea(const FrameRect* const rect) { + return (uint32_t)rect->width_ * rect->height_; +} + +static int IsBlendingPossible(const WebPPicture* const src, + const WebPPicture* const dst, + const FrameRect* const rect) { + int i, j; + assert(src->width == dst->width && src->height == dst->height); + assert(rect->x_offset_ + rect->width_ <= dst->width); + assert(rect->y_offset_ + rect->height_ <= dst->height); + for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) { + for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) { + const uint32_t src_pixel = src->argb[j * src->argb_stride + i]; + const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i]; + const uint32_t dst_alpha = dst_pixel >> 24; + if (dst_alpha != 0xff && src_pixel != dst_pixel) { + // In this case, if we use blending, we can't attain the desired + // 'dst_pixel' value for this pixel. So, blending is not possible. + return 0; + } + } + } + return 1; +} + +#define MIN_COLORS_LOSSY 31 // Don't try lossy below this threshold. +#define MAX_COLORS_LOSSLESS 194 // Don't try lossless above this threshold. +#define MAX_COLOR_COUNT 256 // Power of 2 greater than MAX_COLORS_LOSSLESS. +#define HASH_SIZE (MAX_COLOR_COUNT * 4) +#define HASH_RIGHT_SHIFT 22 // 32 - log2(HASH_SIZE). + +// TODO(urvang): Also used in enc/vp8l.c. Move to utils. +// If the number of colors in the 'pic' is at least MAX_COLOR_COUNT, return +// MAX_COLOR_COUNT. Otherwise, return the exact number of colors in the 'pic'. +static int GetColorCount(const WebPPicture* const pic) { + int x, y; + int num_colors = 0; + uint8_t in_use[HASH_SIZE] = { 0 }; + uint32_t colors[HASH_SIZE]; + static const uint32_t kHashMul = 0x1e35a7bd; + const uint32_t* argb = pic->argb; + const int width = pic->width; + const int height = pic->height; + uint32_t last_pix = ~argb[0]; // so we're sure that last_pix != argb[0] + + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) { + int key; + if (argb[x] == last_pix) { + continue; + } + last_pix = argb[x]; + key = (kHashMul * last_pix) >> HASH_RIGHT_SHIFT; + while (1) { + if (!in_use[key]) { + colors[key] = last_pix; + in_use[key] = 1; + ++num_colors; + if (num_colors >= MAX_COLOR_COUNT) { + return MAX_COLOR_COUNT; // Exact count not needed. + } + break; + } else if (colors[key] == last_pix) { + break; // The color is already there. + } else { + // Some other color sits here, so do linear conflict resolution. + ++key; + key &= (HASH_SIZE - 1); // Key mask. + } + } + } + argb += pic->argb_stride; + } + return num_colors; +} + +#undef MAX_COLOR_COUNT +#undef HASH_SIZE +#undef HASH_RIGHT_SHIFT + +// For pixels in 'rect', replace those pixels in 'dst' that are same as 'src' by +// transparent pixels. +static void IncreaseTransparency(const WebPPicture* const src, + const FrameRect* const rect, + WebPPicture* const dst) { + int i, j; + assert(src != NULL && dst != NULL && rect != NULL); + assert(src->width == dst->width && src->height == dst->height); + for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) { + const uint32_t* const psrc = src->argb + j * src->argb_stride; + uint32_t* const pdst = dst->argb + j * dst->argb_stride; + for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) { + if (psrc[i] == pdst[i]) { + pdst[i] = TRANSPARENT_COLOR; + } + } + } +} + +#undef TRANSPARENT_COLOR + +// Replace similar blocks of pixels by a 'see-through' transparent block +// with uniform average color. +static void FlattenSimilarBlocks(const WebPPicture* const src, + const FrameRect* const rect, + WebPPicture* const dst) { + int i, j; + const int block_size = 8; + const int y_start = (rect->y_offset_ + block_size) & ~(block_size - 1); + const int y_end = (rect->y_offset_ + rect->height_) & ~(block_size - 1); + const int x_start = (rect->x_offset_ + block_size) & ~(block_size - 1); + const int x_end = (rect->x_offset_ + rect->width_) & ~(block_size - 1); + assert(src != NULL && dst != NULL && rect != NULL); + assert(src->width == dst->width && src->height == dst->height); + assert((block_size & (block_size - 1)) == 0); // must be a power of 2 + // Iterate over each block and count similar pixels. + for (j = y_start; j < y_end; j += block_size) { + for (i = x_start; i < x_end; i += block_size) { + int cnt = 0; + int avg_r = 0, avg_g = 0, avg_b = 0; + int x, y; + const uint32_t* const psrc = src->argb + j * src->argb_stride + i; + uint32_t* const pdst = dst->argb + j * dst->argb_stride + i; + for (y = 0; y < block_size; ++y) { + for (x = 0; x < block_size; ++x) { + const uint32_t src_pixel = psrc[x + y * src->argb_stride]; + const int alpha = src_pixel >> 24; + if (alpha == 0xff && + src_pixel == pdst[x + y * dst->argb_stride]) { + ++cnt; + avg_r += (src_pixel >> 16) & 0xff; + avg_g += (src_pixel >> 8) & 0xff; + avg_b += (src_pixel >> 0) & 0xff; + } + } + } + // If we have a fully similar block, we replace it with an + // average transparent block. This compresses better in lossy mode. + if (cnt == block_size * block_size) { + const uint32_t color = (0x00 << 24) | + ((avg_r / cnt) << 16) | + ((avg_g / cnt) << 8) | + ((avg_b / cnt) << 0); + for (y = 0; y < block_size; ++y) { + for (x = 0; x < block_size; ++x) { + pdst[x + y * dst->argb_stride] = color; + } + } + } + } + } +} + +static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic, + WebPMemoryWriter* const memory) { + pic->use_argb = 1; + pic->writer = WebPMemoryWrite; + pic->custom_ptr = memory; + if (!WebPEncode(config, pic)) { + return 0; + } + return 1; +} + +// Struct representing a candidate encoded frame including its metadata. +typedef struct { + WebPMemoryWriter mem_; + WebPMuxFrameInfo info_; + FrameRect rect_; + int evaluate_; // True if this candidate should be evaluated. +} Candidate; + +// Generates a candidate encoded frame given a picture and metadata. +static WebPEncodingError EncodeCandidate(WebPPicture* const sub_frame, + const FrameRect* const rect, + const WebPConfig* const config, + int use_blending, int duration, + Candidate* const candidate) { + WebPEncodingError error_code = VP8_ENC_OK; + assert(candidate != NULL); + memset(candidate, 0, sizeof(*candidate)); + + // Set frame rect and info. + candidate->rect_ = *rect; + candidate->info_.id = WEBP_CHUNK_ANMF; + candidate->info_.x_offset = rect->x_offset_; + candidate->info_.y_offset = rect->y_offset_; + candidate->info_.dispose_method = WEBP_MUX_DISPOSE_NONE; // Set later. + candidate->info_.blend_method = + use_blending ? WEBP_MUX_BLEND : WEBP_MUX_NO_BLEND; + candidate->info_.duration = duration; + + // Encode picture. + WebPMemoryWriterInit(&candidate->mem_); + + if (!EncodeFrame(config, sub_frame, &candidate->mem_)) { + error_code = sub_frame->error_code; + goto Err; + } + + candidate->evaluate_ = 1; + return error_code; + + Err: + WebPMemoryWriterClear(&candidate->mem_); + return error_code; +} + +enum { + LL_DISP_NONE = 0, + LL_DISP_BG, + LOSSY_DISP_NONE, + LOSSY_DISP_BG, + CANDIDATE_COUNT +}; + +// Generates candidates for a given dispose method given pre-filled 'rect' +// and 'sub_frame'. +static WebPEncodingError GenerateCandidates( + WebPAnimEncoder* const enc, Candidate candidates[CANDIDATE_COUNT], + WebPMuxAnimDispose dispose_method, int is_lossless, int is_key_frame, + const FrameRect* const rect, WebPPicture* sub_frame, int duration, + const WebPConfig* const config_ll, const WebPConfig* const config_lossy) { + WebPEncodingError error_code = VP8_ENC_OK; + const int is_dispose_none = (dispose_method == WEBP_MUX_DISPOSE_NONE); + Candidate* const candidate_ll = + is_dispose_none ? &candidates[LL_DISP_NONE] : &candidates[LL_DISP_BG]; + Candidate* const candidate_lossy = is_dispose_none + ? &candidates[LOSSY_DISP_NONE] + : &candidates[LOSSY_DISP_BG]; + const WebPPicture* const curr_canvas_orig = enc->curr_canvas_; + WebPPicture* const curr_canvas_mod = &enc->curr_canvas_mod_; + const WebPPicture* const prev_canvas = + is_dispose_none ? &enc->prev_canvas_ : &enc->prev_canvas_disposed_; + const int use_blending = + !is_key_frame && + IsBlendingPossible(prev_canvas, curr_canvas_orig, rect); + + // True if 'curr_canvas_mod' is different from 'curr_canvas_orig'. + int is_curr_canvas_modified = 1; + + // Pick candidates to be tried. + if (!enc->options_.allow_mixed) { + candidate_ll->evaluate_ = is_lossless; + candidate_lossy->evaluate_ = !is_lossless; + } else { // Use a heuristic for trying lossless and/or lossy compression. + const int num_colors = GetColorCount(sub_frame); + candidate_ll->evaluate_ = (num_colors < MAX_COLORS_LOSSLESS); + candidate_lossy->evaluate_ = (num_colors >= MIN_COLORS_LOSSY); + } + + // Generate candidates. + if (candidate_ll->evaluate_) { + if (is_curr_canvas_modified) { // Copy original. + CopyPixels(curr_canvas_orig, curr_canvas_mod); + is_curr_canvas_modified = 0; + } + if (use_blending) { + IncreaseTransparency(prev_canvas, rect, curr_canvas_mod); + is_curr_canvas_modified = 1; + } + error_code = EncodeCandidate(sub_frame, rect, config_ll, use_blending, + duration, candidate_ll); + if (error_code != VP8_ENC_OK) return error_code; + } + if (candidate_lossy->evaluate_) { + if (is_curr_canvas_modified) { // Copy original. + CopyPixels(curr_canvas_orig, curr_canvas_mod); + } + if (use_blending) { + FlattenSimilarBlocks(prev_canvas, rect, curr_canvas_mod); + } + error_code = EncodeCandidate(sub_frame, rect, config_lossy, use_blending, + duration, candidate_lossy); + if (error_code != VP8_ENC_OK) return error_code; + } + return error_code; +} + +#undef MIN_COLORS_LOSSY +#undef MAX_COLORS_LOSSLESS + +static void GetEncodedData(const WebPMemoryWriter* const memory, + WebPData* const encoded_data) { + encoded_data->bytes = memory->mem; + encoded_data->size = memory->size; +} + +// Sets dispose method of the previous frame to be 'dispose_method'. +static void SetPreviousDisposeMethod(WebPAnimEncoder* const enc, + WebPMuxAnimDispose dispose_method) { + const size_t position = enc->count_ - 2; + EncodedFrame* const prev_enc_frame = GetFrame(enc, position); + assert(enc->count_ >= 2); // As current and previous frames are in enc. + + if (enc->prev_candidate_undecided_) { + assert(dispose_method == WEBP_MUX_DISPOSE_NONE); + prev_enc_frame->sub_frame_.dispose_method = dispose_method; + prev_enc_frame->key_frame_.dispose_method = dispose_method; + } else { + WebPMuxFrameInfo* const prev_info = prev_enc_frame->is_key_frame_ + ? &prev_enc_frame->key_frame_ + : &prev_enc_frame->sub_frame_; + prev_info->dispose_method = dispose_method; + } +} + +// Pick the candidate encoded frame with smallest size and release other +// candidates. +// TODO(later): Perhaps a rough SSIM/PSNR produced by the encoder should +// also be a criteria, in addition to sizes. +static void PickBestCandidate(WebPAnimEncoder* const enc, + Candidate* const candidates, int is_key_frame, + EncodedFrame* const encoded_frame) { + int i; + int best_idx = -1; + size_t best_size = ~0; + for (i = 0; i < CANDIDATE_COUNT; ++i) { + if (candidates[i].evaluate_) { + const size_t candidate_size = candidates[i].mem_.size; + if (candidate_size < best_size) { + best_idx = i; + best_size = candidate_size; + } + } + } + assert(best_idx != -1); + for (i = 0; i < CANDIDATE_COUNT; ++i) { + if (candidates[i].evaluate_) { + if (i == best_idx) { + WebPMuxFrameInfo* const dst = is_key_frame + ? &encoded_frame->key_frame_ + : &encoded_frame->sub_frame_; + *dst = candidates[i].info_; + GetEncodedData(&candidates[i].mem_, &dst->bitstream); + if (!is_key_frame) { + // Note: Previous dispose method only matters for non-keyframes. + // Also, we don't want to modify previous dispose method that was + // selected when a non key-frame was assumed. + const WebPMuxAnimDispose prev_dispose_method = + (best_idx == LL_DISP_NONE || best_idx == LOSSY_DISP_NONE) + ? WEBP_MUX_DISPOSE_NONE + : WEBP_MUX_DISPOSE_BACKGROUND; + SetPreviousDisposeMethod(enc, prev_dispose_method); + } + enc->prev_webp_rect = candidates[i].rect_; // save for next frame. + } else { + WebPMemoryWriterClear(&candidates[i].mem_); + candidates[i].evaluate_ = 0; + } + } + } +} + +// Depending on the configuration, tries different compressions +// (lossy/lossless), dispose methods, blending methods etc to encode the current +// frame and outputs the best one in 'encoded_frame'. +static WebPEncodingError SetFrame( + WebPAnimEncoder* const enc, int duration, + const WebPAnimEncoderFrameOptions* const frame_options, int is_key_frame, + EncodedFrame* const encoded_frame) { + int i; + WebPEncodingError error_code = VP8_ENC_OK; + const WebPPicture* const curr_canvas = enc->curr_canvas_; + const WebPPicture* const prev_canvas = &enc->prev_canvas_; + Candidate candidates[CANDIDATE_COUNT]; + const int is_lossless = frame_options->config.lossless; + + int try_dispose_none = 1; // Default. + FrameRect rect_none; + WebPPicture sub_frame_none; + + // If current frame is a key-frame, dispose method of previous frame doesn't + // matter, so we don't try dispose to background. + // Also, if key-frame insertion is on, and previous frame could be picked as + // either a sub-frame or a key-frame, then we can't be sure about what frame + // rectangle would be disposed. In that case too, we don't try dispose to + // background. + const int dispose_bg_possible = + !is_key_frame && !enc->prev_candidate_undecided_; + int try_dispose_bg = 0; // Default. + FrameRect rect_bg; + WebPPicture sub_frame_bg; + + WebPConfig config_ll = frame_options->config; + WebPConfig config_lossy = frame_options->config; + config_ll.lossless = 1; + config_lossy.lossless = 0; + + if (!WebPPictureInit(&sub_frame_none) || !WebPPictureInit(&sub_frame_bg)) { + return VP8_ENC_ERROR_INVALID_CONFIGURATION; + } + + for (i = 0; i < CANDIDATE_COUNT; ++i) { + candidates[i].evaluate_ = 0; + } + + // Change-rectangle assuming previous frame was DISPOSE_NONE. + GetSubRect(prev_canvas, curr_canvas, is_key_frame, + &rect_none, &sub_frame_none); + + if (dispose_bg_possible) { + // Change-rectangle assuming previous frame was DISPOSE_BACKGROUND. + WebPPicture* const prev_canvas_disposed = &enc->prev_canvas_disposed_; + CopyPixels(prev_canvas, prev_canvas_disposed); + DisposeFrameRectangle(WEBP_MUX_DISPOSE_BACKGROUND, &enc->prev_webp_rect, + prev_canvas_disposed); + GetSubRect(prev_canvas_disposed, curr_canvas, is_key_frame, + &rect_bg, &sub_frame_bg); + + if (enc->options_.minimize_size) { // Try both dispose methods. + try_dispose_bg = 1; + try_dispose_none = 1; + } else if (RectArea(&rect_bg) < RectArea(&rect_none)) { + try_dispose_bg = 1; // Pick DISPOSE_BACKGROUND. + try_dispose_none = 0; + } + } + + if (try_dispose_none) { + error_code = GenerateCandidates( + enc, candidates, WEBP_MUX_DISPOSE_NONE, is_lossless, is_key_frame, + &rect_none, &sub_frame_none, duration, &config_ll, &config_lossy); + if (error_code != VP8_ENC_OK) goto Err; + } + + if (try_dispose_bg) { + assert(!enc->is_first_frame_); + assert(dispose_bg_possible); + error_code = + GenerateCandidates(enc, candidates, WEBP_MUX_DISPOSE_BACKGROUND, + is_lossless, is_key_frame, &rect_bg, &sub_frame_bg, + duration, &config_ll, &config_lossy); + if (error_code != VP8_ENC_OK) goto Err; + } + + PickBestCandidate(enc, candidates, is_key_frame, encoded_frame); + + goto End; + + Err: + for (i = 0; i < CANDIDATE_COUNT; ++i) { + if (candidates[i].evaluate_) { + WebPMemoryWriterClear(&candidates[i].mem_); + } + } + + End: + WebPPictureFree(&sub_frame_none); + WebPPictureFree(&sub_frame_bg); + return error_code; +} + +// Calculate the penalty incurred if we encode given frame as a key frame +// instead of a sub-frame. +static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) { + return ((int64_t)encoded_frame->key_frame_.bitstream.size - + encoded_frame->sub_frame_.bitstream.size); +} + +static int CacheFrame(WebPAnimEncoder* const enc, int duration, + const WebPAnimEncoderFrameOptions* const frame_options) { + int ok = 0; + WebPEncodingError error_code = VP8_ENC_OK; + const size_t position = enc->count_; + EncodedFrame* const encoded_frame = GetFrame(enc, position); + + ++enc->count_; + + if (enc->is_first_frame_) { // Add this as a key-frame. + error_code = + SetFrame(enc, duration, frame_options, 1, encoded_frame); + if (error_code != VP8_ENC_OK) { + goto End; + } + assert(position == 0 && enc->count_ == 1); + encoded_frame->is_key_frame_ = 1; + enc->flush_count_ = 0; + enc->count_since_key_frame_ = 0; + enc->prev_candidate_undecided_ = 0; + } else { + ++enc->count_since_key_frame_; + if (enc->count_since_key_frame_ <= enc->options_.kmin) { + // Add this as a frame rectangle. + error_code = SetFrame(enc, duration, frame_options, 0, encoded_frame); + if (error_code != VP8_ENC_OK) { + goto End; + } + encoded_frame->is_key_frame_ = 0; + enc->flush_count_ = enc->count_ - 1; + enc->prev_candidate_undecided_ = 0; + } else { + int64_t curr_delta; + + // Add this as a frame rectangle to enc. + error_code = SetFrame(enc, duration, frame_options, 0, encoded_frame); + if (error_code != VP8_ENC_OK) goto End; + + // Add this as a key-frame to enc, too. + error_code = SetFrame(enc, duration, frame_options, 1, encoded_frame); + if (error_code != VP8_ENC_OK) goto End; + + // Analyze size difference of the two variants. + curr_delta = KeyFramePenalty(encoded_frame); + if (curr_delta <= enc->best_delta_) { // Pick this as the key-frame. + if (enc->keyframe_ != KEYFRAME_NONE) { + EncodedFrame* const old_keyframe = GetFrame(enc, enc->keyframe_); + assert(old_keyframe->is_key_frame_); + old_keyframe->is_key_frame_ = 0; + } + encoded_frame->is_key_frame_ = 1; + enc->keyframe_ = position; + enc->best_delta_ = curr_delta; + enc->flush_count_ = enc->count_ - 1; // We can flush previous frames. + } else { + encoded_frame->is_key_frame_ = 0; + } + if (enc->count_since_key_frame_ == enc->options_.kmax) { + enc->flush_count_ = enc->count_ - 1; + enc->count_since_key_frame_ = 0; + } + enc->prev_candidate_undecided_ = 1; + } + } + + // Update previous to previous and previous canvases for next call. + CopyPixels(&enc->prev_canvas_, &enc->prev_to_prev_canvas_); + CopyPixels(enc->curr_canvas_, &enc->prev_canvas_); + enc->is_first_frame_ = 0; + ok = 1; + + End: + if (!ok) { + FrameRelease(encoded_frame); + --enc->count_; // We reset the count, as the frame addition failed. + if (enc->options_.verbose) { + fprintf(stderr, "ERROR adding frame. WebPEncodingError: %d.\n", + error_code); + } + } + enc->curr_canvas_->error_code = error_code; // report error_code + assert(ok || error_code != VP8_ENC_OK); + return ok; +} + +static int FlushFrames(WebPAnimEncoder* const enc) { + while (enc->flush_count_ > 0) { + WebPMuxFrameInfo* info; + WebPMuxError err; + EncodedFrame* const curr = GetFrame(enc, 0); + // Pick frame or full canvas. + if (curr->is_key_frame_) { + info = &curr->key_frame_; + if (enc->keyframe_ == 0) { + enc->keyframe_ = KEYFRAME_NONE; + enc->best_delta_ = DELTA_INFINITY; + } + } else { + info = &curr->sub_frame_; + } + // Add to mux. + assert(enc->mux_ != NULL); + err = WebPMuxPushFrame(enc->mux_, info, 1); + if (err != WEBP_MUX_OK) { + if (enc->options_.verbose) { + fprintf(stderr, "ERROR adding frame. WebPMuxError: %d.\n", err); + } + return 0; + } + if (enc->options_.verbose) { + fprintf(stderr, + "Added frame. offset:%d,%d duration:%d dispose:%d blend:%d\n", + info->x_offset, info->y_offset, info->duration, + info->dispose_method, info->blend_method); + } + ++enc->frame_count_; + FrameRelease(curr); + ++enc->start_; + --enc->flush_count_; + --enc->count_; + if (enc->keyframe_ != KEYFRAME_NONE) --enc->keyframe_; + } + + if (enc->count_ == 1 && enc->start_ != 0) { + // Move enc->start to index 0. + const int enc_start_tmp = (int)enc->start_; + EncodedFrame temp = enc->encoded_frames_[0]; + enc->encoded_frames_[0] = enc->encoded_frames_[enc_start_tmp]; + enc->encoded_frames_[enc_start_tmp] = temp; + FrameRelease(&enc->encoded_frames_[enc_start_tmp]); + enc->start_ = 0; + } + return 1; +} + +#undef DELTA_INFINITY +#undef KEYFRAME_NONE + +int WebPAnimEncoderAdd(WebPAnimEncoder* enc, WebPPicture* frame, int duration, + const WebPAnimEncoderFrameOptions* frame_options) { + WebPAnimEncoderFrameOptions options; + if (enc == NULL || frame == NULL) { + return 0; + } + if (frame->width != enc->canvas_width_ || + frame->height != enc->canvas_height_ || !frame->use_argb || + duration < 0) { + frame->error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION; + if (enc->options_.verbose) { + fprintf(stderr, "ERROR adding frame: Invalid input.\n"); + } + return 0; + } + if (frame_options != NULL) { + options = *frame_options; + } else { + DefaultFrameOptions(&options); + } + assert(enc->curr_canvas_ == NULL); + enc->curr_canvas_ = frame; // Store reference. + + if (!CacheFrame(enc, duration, &options)) { + return 0; + } + if (!FlushFrames(enc)) { + return 0; + } + enc->curr_canvas_ = NULL; + return 1; +} + +// ----------------------------------------------------------------------------- +// Bitstream assembly. + +static WebPMuxError ConvertSingleFrameToFullFrame(WebPMux* const mux) { + // TODO(urvang): If only one frame, re-encode it as a full frame. + (void)mux; + return WEBP_MUX_OK; +} + +int WebPAnimEncoderAssemble(WebPAnimEncoder* enc, WebPData* webp_data) { + WebPMux* mux; + WebPMuxError err; + + if (enc == NULL) { + return 0; + } + if (webp_data == NULL) { + if (enc->options_.verbose) { + fprintf(stderr, "ERROR assembling: NULL input\n"); + } + return 0; + } + + // Flush any remaining frames. + enc->flush_count_ = enc->count_; + if (!FlushFrames(enc)) { + return 0; + } + + // Set definitive canvas size. + mux = enc->mux_; + err = WebPMuxSetCanvasSize(mux, enc->canvas_width_, enc->canvas_height_); + if (err != WEBP_MUX_OK) goto Err; + + if (enc->frame_count_ == 1) { + err = ConvertSingleFrameToFullFrame(mux); + if (err != WEBP_MUX_OK) goto Err; + } else { + err = WebPMuxSetAnimationParams(mux, &enc->options_.anim_params); + if (err != WEBP_MUX_OK) goto Err; + } + + // Assemble into a WebP bitstream. + err = WebPMuxAssemble(mux, webp_data); + if (err != WEBP_MUX_OK) goto Err; + + return 1; + + Err: + if (enc->options_.verbose) { + fprintf(stderr, "ERROR assembling WebP: %d\n", err); + } + return 0; +} + +// ----------------------------------------------------------------------------- diff --git a/src/webp/mux.h b/src/webp/mux.h index 578d9e02..047a4020 100644 --- a/src/webp/mux.h +++ b/src/webp/mux.h @@ -7,11 +7,26 @@ // be found in the AUTHORS file in the root of the source tree. // ----------------------------------------------------------------------------- // -// RIFF container manipulation for WebP images. +// RIFF container manipulation and encoding for WebP images. // // Authors: Urvang (urvang@google.com) // Vikas (vikasa@google.com) +#ifndef WEBP_WEBP_MUX_H_ +#define WEBP_WEBP_MUX_H_ + +#include "./encode.h" +#include "./mux_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define WEBP_MUX_ABI_VERSION 0x0103 // MAJOR(8b) + MINOR(8b) + +//------------------------------------------------------------------------------ +// Mux API +// // This API allows manipulation of WebP container images containing features // like color profile, metadata, animation and fragmented images. // @@ -46,17 +61,6 @@ free(data); */ -#ifndef WEBP_WEBP_MUX_H_ -#define WEBP_WEBP_MUX_H_ - -#include "./mux_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define WEBP_MUX_ABI_VERSION 0x0102 // MAJOR(8b) + MINOR(8b) - // Note: forward declaring enumerations is not allowed in (strict) C and C++, // the types are left here for reference. // typedef enum WebPMuxError WebPMuxError; @@ -388,6 +392,101 @@ WEBP_EXTERN(WebPMuxError) WebPMuxNumChunks(const WebPMux* mux, WEBP_EXTERN(WebPMuxError) WebPMuxAssemble(WebPMux* mux, WebPData* assembled_data); +//------------------------------------------------------------------------------ +// WebPAnimEncoder API +// +// This API allows encoding (possibly) animated WebP images. +// +// Code Example: +/* + WebPAnimEncoder* enc = WebPAnimEncoderNew(width, height, enc_options); + while() { + WebPAnimEncoderAdd(enc, frame, duration, frame_options); + } + WebPAnimEncoderAssemble(enc, webp_data); + WebPAnimEncoderDelete(enc); + // Write the 'webp_data' to a file, or re-mux it further. +*/ + +typedef struct WebPAnimEncoder WebPAnimEncoder; // Main opaque object. + +// Global options. +typedef struct { + WebPMuxAnimParams anim_params; // Animation parameters. + int minimize_size; // If true, minimize the output size (slow). Implicitly + // disables key-frame insertion. + size_t kmin; + size_t kmax; // Minimum and maximum distance between consecutive key + // frames in the output. The library may insert some key + // frames as needed to satisfy this criteria. + // Note that these conditions should hold: kmax > kmin + // and kmin >= kmax / 2 + 1. Also, if kmin == 0, then + // key-frame insertion is disabled. + int allow_mixed; // If true, use mixed compression mode; may choose + // either lossy and lossless for each frame. + + // TODO(urvang): Instead of printing errors to STDERR, we should have an error + // string attached to the encoder. + int verbose; // If true, print encoding info. + uint32_t padding[4]; // Padding for later use. +} WebPAnimEncoderOptions; + +// Per-frame options. +typedef struct { + WebPConfig config; // Core encoding parameters. + // TODO(urvang): Add rectangle, dispose and blend method options. After adding + // them, we might need to add 'prev_frame_disposed' parameter to + // WebPAnimEncoderAdd as well. + uint32_t padding[8]; // Padding for later use. +} WebPAnimEncoderFrameOptions; + +// Internal, version-checked, entry point. +WEBP_EXTERN(WebPAnimEncoder*) WebPAnimEncoderNewInternal( + int, int, const WebPAnimEncoderOptions*, int); + +// Creates and initializes a WebPAnimEncoder object. +// Parameters: +// width/height - (in) canvas width and height of the animation. +// encoder_options - (in) encoding options; can be passed NULL to pick +// reasonable defaults. +// Returns: +// A pointer to the newly created WebPAnimEncoder object. +// Or NULL in case of memory error. +static WEBP_INLINE WebPAnimEncoder* WebPAnimEncoderNew( + int width, int height, const WebPAnimEncoderOptions* enc_options) { + return WebPAnimEncoderNewInternal(width, height, enc_options, + WEBP_MUX_ABI_VERSION); +} + +// Optimize the given frame for WebP, encode it and add it to the +// WebPAnimEncoder object. +// Parameters: +// enc - (in/out) object to which the frame is to be added. +// frame - (in/out) frame data in ARGB or YUVA format. +// duration - (in) frame duration +// frame_options - (in) frame options; can be passed NULL to pick +// reasonable defaults. +// Returns: +// On error, returns false and frame->error_code is set appropriately. +// Otherwise, returns true. +WEBP_EXTERN(int) WebPAnimEncoderAdd( + WebPAnimEncoder* enc, WebPPicture* frame, int duration, + const WebPAnimEncoderFrameOptions* frame_options); + +// Assemble all frames added so far into a WebP bitstream. +// Parameters: +// enc - (in/out) object from which the frames are to be assembled. +// webp_data - (out) generated WebP bitstream. +// Returns: +// True on success. +WEBP_EXTERN(int) WebPAnimEncoderAssemble(WebPAnimEncoder* enc, + WebPData* webp_data); + +// Deletes the WebPAnimEncoder object. +// Parameters: +// anim_enc - (in/out) object to be deleted +WEBP_EXTERN(void) WebPAnimEncoderDelete(WebPAnimEncoder* anim_enc); + //------------------------------------------------------------------------------ #ifdef __cplusplus