Path: blob/master/thirdparty/libwebp/src/mux/anim_encode.c
9912 views
// Copyright 2014 Google Inc. All Rights Reserved.1//2// Use of this source code is governed by a BSD-style license3// that can be found in the COPYING file in the root of the source4// tree. An additional intellectual property rights grant can be found5// in the file PATENTS. All contributing project authors may6// be found in the AUTHORS file in the root of the source tree.7// -----------------------------------------------------------------------------8//9// AnimEncoder implementation.10//1112#include <assert.h>13#include <limits.h>14#include <math.h> // for pow()15#include <stdio.h>16#include <stdlib.h> // for abs()1718#include "src/mux/animi.h"19#include "src/utils/utils.h"20#include "src/webp/decode.h"21#include "src/webp/encode.h"22#include "src/webp/format_constants.h"23#include "src/webp/mux.h"24#include "src/webp/types.h"2526#if defined(_MSC_VER) && _MSC_VER < 190027#define snprintf _snprintf28#endif2930#define ERROR_STR_MAX_LENGTH 1003132//------------------------------------------------------------------------------33// Internal structs.3435// Stores frame rectangle dimensions.36typedef struct {37int x_offset_, y_offset_, width_, height_;38} FrameRectangle;3940// Used to store two candidates of encoded data for an animation frame. One of41// the two will be chosen later.42typedef struct {43WebPMuxFrameInfo sub_frame_; // Encoded frame rectangle.44WebPMuxFrameInfo key_frame_; // Encoded frame if it is a key-frame.45int is_key_frame_; // True if 'key_frame' has been chosen.46} EncodedFrame;4748struct WebPAnimEncoder {49const int canvas_width_; // Canvas width.50const int canvas_height_; // Canvas height.51const WebPAnimEncoderOptions options_; // Global encoding options.5253FrameRectangle prev_rect_; // Previous WebP frame rectangle.54WebPConfig last_config_; // Cached in case a re-encode is needed.55WebPConfig last_config_reversed_; // If 'last_config_' uses lossless, then56// this config uses lossy and vice versa;57// only valid if 'options_.allow_mixed'58// is true.5960WebPPicture* curr_canvas_; // Only pointer; we don't own memory.6162// Canvas buffers.63WebPPicture curr_canvas_copy_; // Possibly modified current canvas.64int curr_canvas_copy_modified_; // True if pixels in 'curr_canvas_copy_'65// differ from those in 'curr_canvas_'.6667WebPPicture prev_canvas_; // Previous canvas.68WebPPicture prev_canvas_disposed_; // Previous canvas disposed to background.6970// Encoded data.71EncodedFrame* encoded_frames_; // Array of encoded frames.72size_t size_; // Number of allocated frames.73size_t start_; // Frame start index.74size_t count_; // Number of valid frames.75size_t flush_count_; // If >0, 'flush_count' frames starting from76// 'start' are ready to be added to mux.7778// key-frame related.79int64_t best_delta_; // min(canvas size - frame size) over the frames.80// Can be negative in certain cases due to81// transparent pixels in a frame.82int keyframe_; // Index of selected key-frame relative to 'start_'.83int count_since_key_frame_; // Frames seen since the last key-frame.8485int first_timestamp_; // Timestamp of the first frame.86int prev_timestamp_; // Timestamp of the last added frame.87int prev_candidate_undecided_; // True if it's not yet decided if previous88// frame would be a sub-frame or a key-frame.8990// Misc.91int is_first_frame_; // True if first frame is yet to be added/being added.92int got_null_frame_; // True if WebPAnimEncoderAdd() has already been called93// with a NULL frame.9495size_t in_frame_count_; // Number of input frames processed so far.96size_t out_frame_count_; // Number of frames added to mux so far. This may be97// different from 'in_frame_count_' due to merging.9899WebPMux* mux_; // Muxer to assemble the WebP bitstream.100char error_str_[ERROR_STR_MAX_LENGTH]; // Error string. Empty if no error.101};102103// -----------------------------------------------------------------------------104// Life of WebPAnimEncoder object.105106#define DELTA_INFINITY (1ULL << 32)107#define KEYFRAME_NONE (-1)108109// Reset the counters in the WebPAnimEncoder.110static void ResetCounters(WebPAnimEncoder* const enc) {111enc->start_ = 0;112enc->count_ = 0;113enc->flush_count_ = 0;114enc->best_delta_ = DELTA_INFINITY;115enc->keyframe_ = KEYFRAME_NONE;116}117118static void DisableKeyframes(WebPAnimEncoderOptions* const enc_options) {119enc_options->kmax = INT_MAX;120enc_options->kmin = enc_options->kmax - 1;121}122123#define MAX_CACHED_FRAMES 30124125static void SanitizeEncoderOptions(WebPAnimEncoderOptions* const enc_options) {126int print_warning = enc_options->verbose;127128if (enc_options->minimize_size) {129DisableKeyframes(enc_options);130}131132if (enc_options->kmax == 1) { // All frames will be key-frames.133enc_options->kmin = 0;134enc_options->kmax = 0;135return;136} else if (enc_options->kmax <= 0) {137DisableKeyframes(enc_options);138print_warning = 0;139}140141if (enc_options->kmin >= enc_options->kmax) {142enc_options->kmin = enc_options->kmax - 1;143if (print_warning) {144fprintf(stderr, "WARNING: Setting kmin = %d, so that kmin < kmax.\n",145enc_options->kmin);146}147} else {148const int kmin_limit = enc_options->kmax / 2 + 1;149if (enc_options->kmin < kmin_limit && kmin_limit < enc_options->kmax) {150// This ensures that enc.keyframe + kmin >= kmax is always true. So, we151// can flush all the frames in the 'count_since_key_frame == kmax' case.152enc_options->kmin = kmin_limit;153if (print_warning) {154fprintf(stderr,155"WARNING: Setting kmin = %d, so that kmin >= kmax / 2 + 1.\n",156enc_options->kmin);157}158}159}160// Limit the max number of frames that are allocated.161if (enc_options->kmax - enc_options->kmin > MAX_CACHED_FRAMES) {162enc_options->kmin = enc_options->kmax - MAX_CACHED_FRAMES;163if (print_warning) {164fprintf(stderr,165"WARNING: Setting kmin = %d, so that kmax - kmin <= %d.\n",166enc_options->kmin, MAX_CACHED_FRAMES);167}168}169assert(enc_options->kmin < enc_options->kmax);170}171172#undef MAX_CACHED_FRAMES173174static void DefaultEncoderOptions(WebPAnimEncoderOptions* const enc_options) {175enc_options->anim_params.loop_count = 0;176enc_options->anim_params.bgcolor = 0xffffffff; // White.177enc_options->minimize_size = 0;178DisableKeyframes(enc_options);179enc_options->allow_mixed = 0;180enc_options->verbose = 0;181}182183int WebPAnimEncoderOptionsInitInternal(WebPAnimEncoderOptions* enc_options,184int abi_version) {185if (enc_options == NULL ||186WEBP_ABI_IS_INCOMPATIBLE(abi_version, WEBP_MUX_ABI_VERSION)) {187return 0;188}189DefaultEncoderOptions(enc_options);190return 1;191}192193// This value is used to match a later call to WebPReplaceTransparentPixels(),194// making it a no-op for lossless (see WebPEncode()).195#define TRANSPARENT_COLOR 0x00000000196197static void ClearRectangle(WebPPicture* const picture,198int left, int top, int width, int height) {199int j;200for (j = top; j < top + height; ++j) {201uint32_t* const dst = picture->argb + j * picture->argb_stride;202int i;203for (i = left; i < left + width; ++i) {204dst[i] = TRANSPARENT_COLOR;205}206}207}208209static void WebPUtilClearPic(WebPPicture* const picture,210const FrameRectangle* const rect) {211if (rect != NULL) {212ClearRectangle(picture, rect->x_offset_, rect->y_offset_,213rect->width_, rect->height_);214} else {215ClearRectangle(picture, 0, 0, picture->width, picture->height);216}217}218219static void MarkNoError(WebPAnimEncoder* const enc) {220enc->error_str_[0] = '\0'; // Empty string.221}222223static void MarkError(WebPAnimEncoder* const enc, const char* str) {224if (snprintf(enc->error_str_, ERROR_STR_MAX_LENGTH, "%s.", str) < 0) {225assert(0); // FIX ME!226}227}228229static void MarkError2(WebPAnimEncoder* const enc,230const char* str, int error_code) {231if (snprintf(enc->error_str_, ERROR_STR_MAX_LENGTH, "%s: %d.", str,232error_code) < 0) {233assert(0); // FIX ME!234}235}236237WebPAnimEncoder* WebPAnimEncoderNewInternal(238int width, int height, const WebPAnimEncoderOptions* enc_options,239int abi_version) {240WebPAnimEncoder* enc;241242if (WEBP_ABI_IS_INCOMPATIBLE(abi_version, WEBP_MUX_ABI_VERSION)) {243return NULL;244}245if (width <= 0 || height <= 0 ||246(width * (uint64_t)height) >= MAX_IMAGE_AREA) {247return NULL;248}249250enc = (WebPAnimEncoder*)WebPSafeCalloc(1, sizeof(*enc));251if (enc == NULL) return NULL;252MarkNoError(enc);253254// Dimensions and options.255*(int*)&enc->canvas_width_ = width;256*(int*)&enc->canvas_height_ = height;257if (enc_options != NULL) {258*(WebPAnimEncoderOptions*)&enc->options_ = *enc_options;259SanitizeEncoderOptions((WebPAnimEncoderOptions*)&enc->options_);260} else {261DefaultEncoderOptions((WebPAnimEncoderOptions*)&enc->options_);262}263264// Canvas buffers.265if (!WebPPictureInit(&enc->curr_canvas_copy_) ||266!WebPPictureInit(&enc->prev_canvas_) ||267!WebPPictureInit(&enc->prev_canvas_disposed_)) {268goto Err;269}270enc->curr_canvas_copy_.width = width;271enc->curr_canvas_copy_.height = height;272enc->curr_canvas_copy_.use_argb = 1;273if (!WebPPictureAlloc(&enc->curr_canvas_copy_) ||274!WebPPictureCopy(&enc->curr_canvas_copy_, &enc->prev_canvas_) ||275!WebPPictureCopy(&enc->curr_canvas_copy_, &enc->prev_canvas_disposed_)) {276goto Err;277}278WebPUtilClearPic(&enc->prev_canvas_, NULL);279enc->curr_canvas_copy_modified_ = 1;280281// Encoded frames.282ResetCounters(enc);283// Note: one extra storage is for the previous frame.284enc->size_ = enc->options_.kmax - enc->options_.kmin + 1;285// We need space for at least 2 frames. But when kmin, kmax are both zero,286// enc->size_ will be 1. So we handle that special case below.287if (enc->size_ < 2) enc->size_ = 2;288enc->encoded_frames_ =289(EncodedFrame*)WebPSafeCalloc(enc->size_, sizeof(*enc->encoded_frames_));290if (enc->encoded_frames_ == NULL) goto Err;291292enc->mux_ = WebPMuxNew();293if (enc->mux_ == NULL) goto Err;294295enc->count_since_key_frame_ = 0;296enc->first_timestamp_ = 0;297enc->prev_timestamp_ = 0;298enc->prev_candidate_undecided_ = 0;299enc->is_first_frame_ = 1;300enc->got_null_frame_ = 0;301302return enc; // All OK.303304Err:305WebPAnimEncoderDelete(enc);306return NULL;307}308309// Release the data contained by 'encoded_frame'.310static void FrameRelease(EncodedFrame* const encoded_frame) {311if (encoded_frame != NULL) {312WebPDataClear(&encoded_frame->sub_frame_.bitstream);313WebPDataClear(&encoded_frame->key_frame_.bitstream);314memset(encoded_frame, 0, sizeof(*encoded_frame));315}316}317318void WebPAnimEncoderDelete(WebPAnimEncoder* enc) {319if (enc != NULL) {320WebPPictureFree(&enc->curr_canvas_copy_);321WebPPictureFree(&enc->prev_canvas_);322WebPPictureFree(&enc->prev_canvas_disposed_);323if (enc->encoded_frames_ != NULL) {324size_t i;325for (i = 0; i < enc->size_; ++i) {326FrameRelease(&enc->encoded_frames_[i]);327}328WebPSafeFree(enc->encoded_frames_);329}330WebPMuxDelete(enc->mux_);331WebPSafeFree(enc);332}333}334335// -----------------------------------------------------------------------------336// Frame addition.337338// Returns cached frame at the given 'position'.339static EncodedFrame* GetFrame(const WebPAnimEncoder* const enc,340size_t position) {341assert(enc->start_ + position < enc->size_);342return &enc->encoded_frames_[enc->start_ + position];343}344345typedef int (*ComparePixelsFunc)(const uint32_t*, int, const uint32_t*, int,346int, int);347348// Returns true if 'length' number of pixels in 'src' and 'dst' are equal,349// assuming the given step sizes between pixels.350// 'max_allowed_diff' is unused and only there to allow function pointer use.351static WEBP_INLINE int ComparePixelsLossless(const uint32_t* src, int src_step,352const uint32_t* dst, int dst_step,353int length, int max_allowed_diff) {354(void)max_allowed_diff;355assert(length > 0);356while (length-- > 0) {357if (*src != *dst) {358return 0;359}360src += src_step;361dst += dst_step;362}363return 1;364}365366// Helper to check if each channel in 'src' and 'dst' is at most off by367// 'max_allowed_diff'.368static WEBP_INLINE int PixelsAreSimilar(uint32_t src, uint32_t dst,369int max_allowed_diff) {370const int src_a = (src >> 24) & 0xff;371const int src_r = (src >> 16) & 0xff;372const int src_g = (src >> 8) & 0xff;373const int src_b = (src >> 0) & 0xff;374const int dst_a = (dst >> 24) & 0xff;375const int dst_r = (dst >> 16) & 0xff;376const int dst_g = (dst >> 8) & 0xff;377const int dst_b = (dst >> 0) & 0xff;378379return (src_a == dst_a) &&380(abs(src_r - dst_r) * dst_a <= (max_allowed_diff * 255)) &&381(abs(src_g - dst_g) * dst_a <= (max_allowed_diff * 255)) &&382(abs(src_b - dst_b) * dst_a <= (max_allowed_diff * 255));383}384385// Returns true if 'length' number of pixels in 'src' and 'dst' are within an386// error bound, assuming the given step sizes between pixels.387static WEBP_INLINE int ComparePixelsLossy(const uint32_t* src, int src_step,388const uint32_t* dst, int dst_step,389int length, int max_allowed_diff) {390assert(length > 0);391while (length-- > 0) {392if (!PixelsAreSimilar(*src, *dst, max_allowed_diff)) {393return 0;394}395src += src_step;396dst += dst_step;397}398return 1;399}400401static int IsEmptyRect(const FrameRectangle* const rect) {402return (rect->width_ == 0) || (rect->height_ == 0);403}404405static int QualityToMaxDiff(float quality) {406const double val = pow(quality / 100., 0.5);407const double max_diff = 31 * (1 - val) + 1 * val;408return (int)(max_diff + 0.5);409}410411// Assumes that an initial valid guess of change rectangle 'rect' is passed.412static void MinimizeChangeRectangle(const WebPPicture* const src,413const WebPPicture* const dst,414FrameRectangle* const rect,415int is_lossless, float quality) {416int i, j;417const ComparePixelsFunc compare_pixels =418is_lossless ? ComparePixelsLossless : ComparePixelsLossy;419const int max_allowed_diff_lossy = QualityToMaxDiff(quality);420const int max_allowed_diff = is_lossless ? 0 : max_allowed_diff_lossy;421422// Assumption/correctness checks.423assert(src->width == dst->width && src->height == dst->height);424assert(rect->x_offset_ + rect->width_ <= dst->width);425assert(rect->y_offset_ + rect->height_ <= dst->height);426427// Left boundary.428for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {429const uint32_t* const src_argb =430&src->argb[rect->y_offset_ * src->argb_stride + i];431const uint32_t* const dst_argb =432&dst->argb[rect->y_offset_ * dst->argb_stride + i];433if (compare_pixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride,434rect->height_, max_allowed_diff)) {435--rect->width_; // Redundant column.436++rect->x_offset_;437} else {438break;439}440}441if (rect->width_ == 0) goto NoChange;442443// Right boundary.444for (i = rect->x_offset_ + rect->width_ - 1; i >= rect->x_offset_; --i) {445const uint32_t* const src_argb =446&src->argb[rect->y_offset_ * src->argb_stride + i];447const uint32_t* const dst_argb =448&dst->argb[rect->y_offset_ * dst->argb_stride + i];449if (compare_pixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride,450rect->height_, max_allowed_diff)) {451--rect->width_; // Redundant column.452} else {453break;454}455}456if (rect->width_ == 0) goto NoChange;457458// Top boundary.459for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {460const uint32_t* const src_argb =461&src->argb[j * src->argb_stride + rect->x_offset_];462const uint32_t* const dst_argb =463&dst->argb[j * dst->argb_stride + rect->x_offset_];464if (compare_pixels(src_argb, 1, dst_argb, 1, rect->width_,465max_allowed_diff)) {466--rect->height_; // Redundant row.467++rect->y_offset_;468} else {469break;470}471}472if (rect->height_ == 0) goto NoChange;473474// Bottom boundary.475for (j = rect->y_offset_ + rect->height_ - 1; j >= rect->y_offset_; --j) {476const uint32_t* const src_argb =477&src->argb[j * src->argb_stride + rect->x_offset_];478const uint32_t* const dst_argb =479&dst->argb[j * dst->argb_stride + rect->x_offset_];480if (compare_pixels(src_argb, 1, dst_argb, 1, rect->width_,481max_allowed_diff)) {482--rect->height_; // Redundant row.483} else {484break;485}486}487if (rect->height_ == 0) goto NoChange;488489if (IsEmptyRect(rect)) {490NoChange:491rect->x_offset_ = 0;492rect->y_offset_ = 0;493rect->width_ = 0;494rect->height_ = 0;495}496}497498// Snap rectangle to even offsets (and adjust dimensions if needed).499static WEBP_INLINE void SnapToEvenOffsets(FrameRectangle* const rect) {500rect->width_ += (rect->x_offset_ & 1);501rect->height_ += (rect->y_offset_ & 1);502rect->x_offset_ &= ~1;503rect->y_offset_ &= ~1;504}505506typedef struct {507int should_try_; // Should try this set of parameters.508int empty_rect_allowed_; // Frame with empty rectangle can be skipped.509FrameRectangle rect_ll_; // Frame rectangle for lossless compression.510WebPPicture sub_frame_ll_; // Sub-frame pic for lossless compression.511FrameRectangle rect_lossy_; // Frame rectangle for lossy compression.512// Could be smaller than rect_ll_ as pixels513// with small diffs can be ignored.514WebPPicture sub_frame_lossy_; // Sub-frame pic for lossless compression.515} SubFrameParams;516517static int SubFrameParamsInit(SubFrameParams* const params,518int should_try, int empty_rect_allowed) {519params->should_try_ = should_try;520params->empty_rect_allowed_ = empty_rect_allowed;521if (!WebPPictureInit(¶ms->sub_frame_ll_) ||522!WebPPictureInit(¶ms->sub_frame_lossy_)) {523return 0;524}525return 1;526}527528static void SubFrameParamsFree(SubFrameParams* const params) {529WebPPictureFree(¶ms->sub_frame_ll_);530WebPPictureFree(¶ms->sub_frame_lossy_);531}532533// Given previous and current canvas, picks the optimal rectangle for the534// current frame based on 'is_lossless' and other parameters. Assumes that the535// initial guess 'rect' is valid.536static int GetSubRect(const WebPPicture* const prev_canvas,537const WebPPicture* const curr_canvas, int is_key_frame,538int is_first_frame, int empty_rect_allowed,539int is_lossless, float quality,540FrameRectangle* const rect,541WebPPicture* const sub_frame) {542if (!is_key_frame || is_first_frame) { // Optimize frame rectangle.543// Note: This behaves as expected for first frame, as 'prev_canvas' is544// initialized to a fully transparent canvas in the beginning.545MinimizeChangeRectangle(prev_canvas, curr_canvas, rect,546is_lossless, quality);547}548549if (IsEmptyRect(rect)) {550if (empty_rect_allowed) { // No need to get 'sub_frame'.551return 1;552} else { // Force a 1x1 rectangle.553rect->width_ = 1;554rect->height_ = 1;555assert(rect->x_offset_ == 0);556assert(rect->y_offset_ == 0);557}558}559560SnapToEvenOffsets(rect);561return WebPPictureView(curr_canvas, rect->x_offset_, rect->y_offset_,562rect->width_, rect->height_, sub_frame);563}564565// Picks optimal frame rectangle for both lossless and lossy compression. The566// initial guess for frame rectangles will be the full canvas.567static int GetSubRects(const WebPPicture* const prev_canvas,568const WebPPicture* const curr_canvas, int is_key_frame,569int is_first_frame, float quality,570SubFrameParams* const params) {571// Lossless frame rectangle.572params->rect_ll_.x_offset_ = 0;573params->rect_ll_.y_offset_ = 0;574params->rect_ll_.width_ = curr_canvas->width;575params->rect_ll_.height_ = curr_canvas->height;576if (!GetSubRect(prev_canvas, curr_canvas, is_key_frame, is_first_frame,577params->empty_rect_allowed_, 1, quality,578¶ms->rect_ll_, ¶ms->sub_frame_ll_)) {579return 0;580}581// Lossy frame rectangle.582params->rect_lossy_ = params->rect_ll_; // seed with lossless rect.583return GetSubRect(prev_canvas, curr_canvas, is_key_frame, is_first_frame,584params->empty_rect_allowed_, 0, quality,585¶ms->rect_lossy_, ¶ms->sub_frame_lossy_);586}587588static WEBP_INLINE int clip(int v, int min_v, int max_v) {589return (v < min_v) ? min_v : (v > max_v) ? max_v : v;590}591592int WebPAnimEncoderRefineRect(593const WebPPicture* const prev_canvas, const WebPPicture* const curr_canvas,594int is_lossless, float quality, int* const x_offset, int* const y_offset,595int* const width, int* const height) {596FrameRectangle rect;597int right, left, bottom, top;598if (prev_canvas == NULL || curr_canvas == NULL ||599prev_canvas->width != curr_canvas->width ||600prev_canvas->height != curr_canvas->height ||601!prev_canvas->use_argb || !curr_canvas->use_argb) {602return 0;603}604right = clip(*x_offset + *width, 0, curr_canvas->width);605left = clip(*x_offset, 0, curr_canvas->width - 1);606bottom = clip(*y_offset + *height, 0, curr_canvas->height);607top = clip(*y_offset, 0, curr_canvas->height - 1);608rect.x_offset_ = left;609rect.y_offset_ = top;610rect.width_ = clip(right - left, 0, curr_canvas->width - rect.x_offset_);611rect.height_ = clip(bottom - top, 0, curr_canvas->height - rect.y_offset_);612MinimizeChangeRectangle(prev_canvas, curr_canvas, &rect, is_lossless,613quality);614SnapToEvenOffsets(&rect);615*x_offset = rect.x_offset_;616*y_offset = rect.y_offset_;617*width = rect.width_;618*height = rect.height_;619return 1;620}621622static void DisposeFrameRectangle(int dispose_method,623const FrameRectangle* const rect,624WebPPicture* const curr_canvas) {625assert(rect != NULL);626if (dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {627WebPUtilClearPic(curr_canvas, rect);628}629}630631static uint32_t RectArea(const FrameRectangle* const rect) {632return (uint32_t)rect->width_ * rect->height_;633}634635static int IsLosslessBlendingPossible(const WebPPicture* const src,636const WebPPicture* const dst,637const FrameRectangle* const rect) {638int i, j;639assert(src->width == dst->width && src->height == dst->height);640assert(rect->x_offset_ + rect->width_ <= dst->width);641assert(rect->y_offset_ + rect->height_ <= dst->height);642for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {643for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {644const uint32_t src_pixel = src->argb[j * src->argb_stride + i];645const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i];646const uint32_t dst_alpha = dst_pixel >> 24;647if (dst_alpha != 0xff && src_pixel != dst_pixel) {648// In this case, if we use blending, we can't attain the desired649// 'dst_pixel' value for this pixel. So, blending is not possible.650return 0;651}652}653}654return 1;655}656657static int IsLossyBlendingPossible(const WebPPicture* const src,658const WebPPicture* const dst,659const FrameRectangle* const rect,660float quality) {661const int max_allowed_diff_lossy = QualityToMaxDiff(quality);662int i, j;663assert(src->width == dst->width && src->height == dst->height);664assert(rect->x_offset_ + rect->width_ <= dst->width);665assert(rect->y_offset_ + rect->height_ <= dst->height);666for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {667for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {668const uint32_t src_pixel = src->argb[j * src->argb_stride + i];669const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i];670const uint32_t dst_alpha = dst_pixel >> 24;671if (dst_alpha != 0xff &&672!PixelsAreSimilar(src_pixel, dst_pixel, max_allowed_diff_lossy)) {673// In this case, if we use blending, we can't attain the desired674// 'dst_pixel' value for this pixel. So, blending is not possible.675return 0;676}677}678}679return 1;680}681682// For pixels in 'rect', replace those pixels in 'dst' that are same as 'src' by683// transparent pixels.684// Returns true if at least one pixel gets modified.685static int IncreaseTransparency(const WebPPicture* const src,686const FrameRectangle* const rect,687WebPPicture* const dst) {688int i, j;689int modified = 0;690assert(src != NULL && dst != NULL && rect != NULL);691assert(src->width == dst->width && src->height == dst->height);692for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {693const uint32_t* const psrc = src->argb + j * src->argb_stride;694uint32_t* const pdst = dst->argb + j * dst->argb_stride;695for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {696if (psrc[i] == pdst[i] && pdst[i] != TRANSPARENT_COLOR) {697pdst[i] = TRANSPARENT_COLOR;698modified = 1;699}700}701}702return modified;703}704705#undef TRANSPARENT_COLOR706707// Replace similar blocks of pixels by a 'see-through' transparent block708// with uniform average color.709// Assumes lossy compression is being used.710// Returns true if at least one pixel gets modified.711static int FlattenSimilarBlocks(const WebPPicture* const src,712const FrameRectangle* const rect,713WebPPicture* const dst, float quality) {714const int max_allowed_diff_lossy = QualityToMaxDiff(quality);715int i, j;716int modified = 0;717const int block_size = 8;718const int y_start = (rect->y_offset_ + block_size) & ~(block_size - 1);719const int y_end = (rect->y_offset_ + rect->height_) & ~(block_size - 1);720const int x_start = (rect->x_offset_ + block_size) & ~(block_size - 1);721const int x_end = (rect->x_offset_ + rect->width_) & ~(block_size - 1);722assert(src != NULL && dst != NULL && rect != NULL);723assert(src->width == dst->width && src->height == dst->height);724assert((block_size & (block_size - 1)) == 0); // must be a power of 2725// Iterate over each block and count similar pixels.726for (j = y_start; j < y_end; j += block_size) {727for (i = x_start; i < x_end; i += block_size) {728int cnt = 0;729int avg_r = 0, avg_g = 0, avg_b = 0;730int x, y;731const uint32_t* const psrc = src->argb + j * src->argb_stride + i;732uint32_t* const pdst = dst->argb + j * dst->argb_stride + i;733for (y = 0; y < block_size; ++y) {734for (x = 0; x < block_size; ++x) {735const uint32_t src_pixel = psrc[x + y * src->argb_stride];736const int alpha = src_pixel >> 24;737if (alpha == 0xff &&738PixelsAreSimilar(src_pixel, pdst[x + y * dst->argb_stride],739max_allowed_diff_lossy)) {740++cnt;741avg_r += (src_pixel >> 16) & 0xff;742avg_g += (src_pixel >> 8) & 0xff;743avg_b += (src_pixel >> 0) & 0xff;744}745}746}747// If we have a fully similar block, we replace it with an748// average transparent block. This compresses better in lossy mode.749if (cnt == block_size * block_size) {750const uint32_t color = (0x00 << 24) |751((avg_r / cnt) << 16) |752((avg_g / cnt) << 8) |753((avg_b / cnt) << 0);754for (y = 0; y < block_size; ++y) {755for (x = 0; x < block_size; ++x) {756pdst[x + y * dst->argb_stride] = color;757}758}759modified = 1;760}761}762}763return modified;764}765766static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic,767WebPMemoryWriter* const memory) {768pic->use_argb = 1;769pic->writer = WebPMemoryWrite;770pic->custom_ptr = memory;771if (!WebPEncode(config, pic)) {772return 0;773}774return 1;775}776777// Struct representing a candidate encoded frame including its metadata.778typedef struct {779WebPMemoryWriter mem_;780WebPMuxFrameInfo info_;781FrameRectangle rect_;782int evaluate_; // True if this candidate should be evaluated.783} Candidate;784785// Generates a candidate encoded frame given a picture and metadata.786static WebPEncodingError EncodeCandidate(WebPPicture* const sub_frame,787const FrameRectangle* const rect,788const WebPConfig* const encoder_config,789int use_blending,790Candidate* const candidate) {791WebPConfig config = *encoder_config;792WebPEncodingError error_code = VP8_ENC_OK;793assert(candidate != NULL);794memset(candidate, 0, sizeof(*candidate));795796// Set frame rect and info.797candidate->rect_ = *rect;798candidate->info_.id = WEBP_CHUNK_ANMF;799candidate->info_.x_offset = rect->x_offset_;800candidate->info_.y_offset = rect->y_offset_;801candidate->info_.dispose_method = WEBP_MUX_DISPOSE_NONE; // Set later.802candidate->info_.blend_method =803use_blending ? WEBP_MUX_BLEND : WEBP_MUX_NO_BLEND;804candidate->info_.duration = 0; // Set in next call to WebPAnimEncoderAdd().805806// Encode picture.807WebPMemoryWriterInit(&candidate->mem_);808809if (!config.lossless && use_blending) {810// Disable filtering to avoid blockiness in reconstructed frames at the811// time of decoding.812config.autofilter = 0;813config.filter_strength = 0;814}815if (!EncodeFrame(&config, sub_frame, &candidate->mem_)) {816error_code = sub_frame->error_code;817goto Err;818}819820candidate->evaluate_ = 1;821return error_code;822823Err:824WebPMemoryWriterClear(&candidate->mem_);825return error_code;826}827828static void CopyCurrentCanvas(WebPAnimEncoder* const enc) {829if (enc->curr_canvas_copy_modified_) {830WebPCopyPixels(enc->curr_canvas_, &enc->curr_canvas_copy_);831enc->curr_canvas_copy_.progress_hook = enc->curr_canvas_->progress_hook;832enc->curr_canvas_copy_.user_data = enc->curr_canvas_->user_data;833enc->curr_canvas_copy_modified_ = 0;834}835}836837enum {838LL_DISP_NONE = 0,839LL_DISP_BG,840LOSSY_DISP_NONE,841LOSSY_DISP_BG,842CANDIDATE_COUNT843};844845#define MIN_COLORS_LOSSY 31 // Don't try lossy below this threshold.846#define MAX_COLORS_LOSSLESS 194 // Don't try lossless above this threshold.847848// Generates candidates for a given dispose method given pre-filled sub-frame849// 'params'.850static WebPEncodingError GenerateCandidates(851WebPAnimEncoder* const enc, Candidate candidates[CANDIDATE_COUNT],852WebPMuxAnimDispose dispose_method, int is_lossless, int is_key_frame,853SubFrameParams* const params,854const WebPConfig* const config_ll, const WebPConfig* const config_lossy) {855WebPEncodingError error_code = VP8_ENC_OK;856const int is_dispose_none = (dispose_method == WEBP_MUX_DISPOSE_NONE);857Candidate* const candidate_ll =858is_dispose_none ? &candidates[LL_DISP_NONE] : &candidates[LL_DISP_BG];859Candidate* const candidate_lossy = is_dispose_none860? &candidates[LOSSY_DISP_NONE]861: &candidates[LOSSY_DISP_BG];862WebPPicture* const curr_canvas = &enc->curr_canvas_copy_;863const WebPPicture* const prev_canvas =864is_dispose_none ? &enc->prev_canvas_ : &enc->prev_canvas_disposed_;865int use_blending_ll, use_blending_lossy;866int evaluate_ll, evaluate_lossy;867868CopyCurrentCanvas(enc);869use_blending_ll =870!is_key_frame &&871IsLosslessBlendingPossible(prev_canvas, curr_canvas, ¶ms->rect_ll_);872use_blending_lossy =873!is_key_frame &&874IsLossyBlendingPossible(prev_canvas, curr_canvas, ¶ms->rect_lossy_,875config_lossy->quality);876877// Pick candidates to be tried.878if (!enc->options_.allow_mixed) {879evaluate_ll = is_lossless;880evaluate_lossy = !is_lossless;881} else if (enc->options_.minimize_size) {882evaluate_ll = 1;883evaluate_lossy = 1;884} else { // Use a heuristic for trying lossless and/or lossy compression.885const int num_colors = WebPGetColorPalette(¶ms->sub_frame_ll_, NULL);886evaluate_ll = (num_colors < MAX_COLORS_LOSSLESS);887evaluate_lossy = (num_colors >= MIN_COLORS_LOSSY);888}889890// Generate candidates.891if (evaluate_ll) {892CopyCurrentCanvas(enc);893if (use_blending_ll) {894enc->curr_canvas_copy_modified_ =895IncreaseTransparency(prev_canvas, ¶ms->rect_ll_, curr_canvas);896}897error_code = EncodeCandidate(¶ms->sub_frame_ll_, ¶ms->rect_ll_,898config_ll, use_blending_ll, candidate_ll);899if (error_code != VP8_ENC_OK) return error_code;900}901if (evaluate_lossy) {902CopyCurrentCanvas(enc);903if (use_blending_lossy) {904enc->curr_canvas_copy_modified_ =905FlattenSimilarBlocks(prev_canvas, ¶ms->rect_lossy_, curr_canvas,906config_lossy->quality);907}908error_code =909EncodeCandidate(¶ms->sub_frame_lossy_, ¶ms->rect_lossy_,910config_lossy, use_blending_lossy, candidate_lossy);911if (error_code != VP8_ENC_OK) return error_code;912enc->curr_canvas_copy_modified_ = 1;913}914return error_code;915}916917#undef MIN_COLORS_LOSSY918#undef MAX_COLORS_LOSSLESS919920static void GetEncodedData(const WebPMemoryWriter* const memory,921WebPData* const encoded_data) {922encoded_data->bytes = memory->mem;923encoded_data->size = memory->size;924}925926// Sets dispose method of the previous frame to be 'dispose_method'.927static void SetPreviousDisposeMethod(WebPAnimEncoder* const enc,928WebPMuxAnimDispose dispose_method) {929const size_t position = enc->count_ - 2;930EncodedFrame* const prev_enc_frame = GetFrame(enc, position);931assert(enc->count_ >= 2); // As current and previous frames are in enc.932933if (enc->prev_candidate_undecided_) {934assert(dispose_method == WEBP_MUX_DISPOSE_NONE);935prev_enc_frame->sub_frame_.dispose_method = dispose_method;936prev_enc_frame->key_frame_.dispose_method = dispose_method;937} else {938WebPMuxFrameInfo* const prev_info = prev_enc_frame->is_key_frame_939? &prev_enc_frame->key_frame_940: &prev_enc_frame->sub_frame_;941prev_info->dispose_method = dispose_method;942}943}944945static int IncreasePreviousDuration(WebPAnimEncoder* const enc, int duration) {946const size_t position = enc->count_ - 1;947EncodedFrame* const prev_enc_frame = GetFrame(enc, position);948int new_duration;949950assert(enc->count_ >= 1);951assert(!prev_enc_frame->is_key_frame_ ||952prev_enc_frame->sub_frame_.duration ==953prev_enc_frame->key_frame_.duration);954assert(prev_enc_frame->sub_frame_.duration ==955(prev_enc_frame->sub_frame_.duration & (MAX_DURATION - 1)));956assert(duration == (duration & (MAX_DURATION - 1)));957958new_duration = prev_enc_frame->sub_frame_.duration + duration;959if (new_duration >= MAX_DURATION) { // Special case.960// Separate out previous frame from earlier merged frames to avoid overflow.961// We add a 1x1 transparent frame for the previous frame, with blending on.962const FrameRectangle rect = { 0, 0, 1, 1 };963const uint8_t lossless_1x1_bytes[] = {9640x52, 0x49, 0x46, 0x46, 0x14, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,9650x56, 0x50, 0x38, 0x4c, 0x08, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00,9660x10, 0x88, 0x88, 0x08967};968const WebPData lossless_1x1 = {969lossless_1x1_bytes, sizeof(lossless_1x1_bytes)970};971const uint8_t lossy_1x1_bytes[] = {9720x52, 0x49, 0x46, 0x46, 0x40, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,9730x56, 0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,9740x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x4c, 0x50, 0x48, 0x02, 0x00,9750x00, 0x00, 0x00, 0x00, 0x56, 0x50, 0x38, 0x20, 0x18, 0x00, 0x00, 0x00,9760x30, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00,9770x34, 0x25, 0xa4, 0x00, 0x03, 0x70, 0x00, 0xfe, 0xfb, 0xfd, 0x50, 0x00978};979const WebPData lossy_1x1 = { lossy_1x1_bytes, sizeof(lossy_1x1_bytes) };980const int can_use_lossless =981(enc->last_config_.lossless || enc->options_.allow_mixed);982EncodedFrame* const curr_enc_frame = GetFrame(enc, enc->count_);983curr_enc_frame->is_key_frame_ = 0;984curr_enc_frame->sub_frame_.id = WEBP_CHUNK_ANMF;985curr_enc_frame->sub_frame_.x_offset = 0;986curr_enc_frame->sub_frame_.y_offset = 0;987curr_enc_frame->sub_frame_.dispose_method = WEBP_MUX_DISPOSE_NONE;988curr_enc_frame->sub_frame_.blend_method = WEBP_MUX_BLEND;989curr_enc_frame->sub_frame_.duration = duration;990if (!WebPDataCopy(can_use_lossless ? &lossless_1x1 : &lossy_1x1,991&curr_enc_frame->sub_frame_.bitstream)) {992return 0;993}994++enc->count_;995++enc->count_since_key_frame_;996enc->flush_count_ = enc->count_ - 1;997enc->prev_candidate_undecided_ = 0;998enc->prev_rect_ = rect;999} else { // Regular case.1000// Increase duration of the previous frame by 'duration'.1001prev_enc_frame->sub_frame_.duration = new_duration;1002prev_enc_frame->key_frame_.duration = new_duration;1003}1004return 1;1005}10061007// Pick the candidate encoded frame with smallest size and release other1008// candidates.1009// TODO(later): Perhaps a rough SSIM/PSNR produced by the encoder should1010// also be a criteria, in addition to sizes.1011static void PickBestCandidate(WebPAnimEncoder* const enc,1012Candidate* const candidates, int is_key_frame,1013EncodedFrame* const encoded_frame) {1014int i;1015int best_idx = -1;1016size_t best_size = ~0;1017for (i = 0; i < CANDIDATE_COUNT; ++i) {1018if (candidates[i].evaluate_) {1019const size_t candidate_size = candidates[i].mem_.size;1020if (candidate_size < best_size) {1021best_idx = i;1022best_size = candidate_size;1023}1024}1025}1026assert(best_idx != -1);1027for (i = 0; i < CANDIDATE_COUNT; ++i) {1028if (candidates[i].evaluate_) {1029if (i == best_idx) {1030WebPMuxFrameInfo* const dst = is_key_frame1031? &encoded_frame->key_frame_1032: &encoded_frame->sub_frame_;1033*dst = candidates[i].info_;1034GetEncodedData(&candidates[i].mem_, &dst->bitstream);1035if (!is_key_frame) {1036// Note: Previous dispose method only matters for non-keyframes.1037// Also, we don't want to modify previous dispose method that was1038// selected when a non key-frame was assumed.1039const WebPMuxAnimDispose prev_dispose_method =1040(best_idx == LL_DISP_NONE || best_idx == LOSSY_DISP_NONE)1041? WEBP_MUX_DISPOSE_NONE1042: WEBP_MUX_DISPOSE_BACKGROUND;1043SetPreviousDisposeMethod(enc, prev_dispose_method);1044}1045enc->prev_rect_ = candidates[i].rect_; // save for next frame.1046} else {1047WebPMemoryWriterClear(&candidates[i].mem_);1048candidates[i].evaluate_ = 0;1049}1050}1051}1052}10531054// Depending on the configuration, tries different compressions1055// (lossy/lossless), dispose methods, blending methods etc to encode the current1056// frame and outputs the best one in 'encoded_frame'.1057// 'frame_skipped' will be set to true if this frame should actually be skipped.1058static WebPEncodingError SetFrame(WebPAnimEncoder* const enc,1059const WebPConfig* const config,1060int is_key_frame,1061EncodedFrame* const encoded_frame,1062int* const frame_skipped) {1063int i;1064WebPEncodingError error_code = VP8_ENC_OK;1065const WebPPicture* const curr_canvas = &enc->curr_canvas_copy_;1066const WebPPicture* const prev_canvas = &enc->prev_canvas_;1067Candidate candidates[CANDIDATE_COUNT];1068const int is_lossless = config->lossless;1069const int consider_lossless = is_lossless || enc->options_.allow_mixed;1070const int consider_lossy = !is_lossless || enc->options_.allow_mixed;1071const int is_first_frame = enc->is_first_frame_;10721073// First frame cannot be skipped as there is no 'previous frame' to merge it1074// to. So, empty rectangle is not allowed for the first frame.1075const int empty_rect_allowed_none = !is_first_frame;10761077// Even if there is exact pixel match between 'disposed previous canvas' and1078// 'current canvas', we can't skip current frame, as there may not be exact1079// pixel match between 'previous canvas' and 'current canvas'. So, we don't1080// allow empty rectangle in this case.1081const int empty_rect_allowed_bg = 0;10821083// If current frame is a key-frame, dispose method of previous frame doesn't1084// matter, so we don't try dispose to background.1085// Also, if key-frame insertion is on, and previous frame could be picked as1086// either a sub-frame or a key-frame, then we can't be sure about what frame1087// rectangle would be disposed. In that case too, we don't try dispose to1088// background.1089const int dispose_bg_possible =1090!is_key_frame && !enc->prev_candidate_undecided_;10911092SubFrameParams dispose_none_params;1093SubFrameParams dispose_bg_params;10941095WebPConfig config_ll = *config;1096WebPConfig config_lossy = *config;1097config_ll.lossless = 1;1098config_lossy.lossless = 0;1099enc->last_config_ = *config;1100enc->last_config_reversed_ = config->lossless ? config_lossy : config_ll;1101*frame_skipped = 0;11021103if (!SubFrameParamsInit(&dispose_none_params, 1, empty_rect_allowed_none) ||1104!SubFrameParamsInit(&dispose_bg_params, 0, empty_rect_allowed_bg)) {1105return VP8_ENC_ERROR_INVALID_CONFIGURATION;1106}11071108memset(candidates, 0, sizeof(candidates));11091110// Change-rectangle assuming previous frame was DISPOSE_NONE.1111if (!GetSubRects(prev_canvas, curr_canvas, is_key_frame, is_first_frame,1112config_lossy.quality, &dispose_none_params)) {1113error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;1114goto Err;1115}11161117if ((consider_lossless && IsEmptyRect(&dispose_none_params.rect_ll_)) ||1118(consider_lossy && IsEmptyRect(&dispose_none_params.rect_lossy_))) {1119// Don't encode the frame at all. Instead, the duration of the previous1120// frame will be increased later.1121assert(empty_rect_allowed_none);1122*frame_skipped = 1;1123goto End;1124}11251126if (dispose_bg_possible) {1127// Change-rectangle assuming previous frame was DISPOSE_BACKGROUND.1128WebPPicture* const prev_canvas_disposed = &enc->prev_canvas_disposed_;1129WebPCopyPixels(prev_canvas, prev_canvas_disposed);1130DisposeFrameRectangle(WEBP_MUX_DISPOSE_BACKGROUND, &enc->prev_rect_,1131prev_canvas_disposed);11321133if (!GetSubRects(prev_canvas_disposed, curr_canvas, is_key_frame,1134is_first_frame, config_lossy.quality,1135&dispose_bg_params)) {1136error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;1137goto Err;1138}1139assert(!IsEmptyRect(&dispose_bg_params.rect_ll_));1140assert(!IsEmptyRect(&dispose_bg_params.rect_lossy_));11411142if (enc->options_.minimize_size) { // Try both dispose methods.1143dispose_bg_params.should_try_ = 1;1144dispose_none_params.should_try_ = 1;1145} else if ((is_lossless &&1146RectArea(&dispose_bg_params.rect_ll_) <1147RectArea(&dispose_none_params.rect_ll_)) ||1148(!is_lossless &&1149RectArea(&dispose_bg_params.rect_lossy_) <1150RectArea(&dispose_none_params.rect_lossy_))) {1151dispose_bg_params.should_try_ = 1; // Pick DISPOSE_BACKGROUND.1152dispose_none_params.should_try_ = 0;1153}1154}11551156if (dispose_none_params.should_try_) {1157error_code = GenerateCandidates(1158enc, candidates, WEBP_MUX_DISPOSE_NONE, is_lossless, is_key_frame,1159&dispose_none_params, &config_ll, &config_lossy);1160if (error_code != VP8_ENC_OK) goto Err;1161}11621163if (dispose_bg_params.should_try_) {1164assert(!enc->is_first_frame_);1165assert(dispose_bg_possible);1166error_code = GenerateCandidates(1167enc, candidates, WEBP_MUX_DISPOSE_BACKGROUND, is_lossless, is_key_frame,1168&dispose_bg_params, &config_ll, &config_lossy);1169if (error_code != VP8_ENC_OK) goto Err;1170}11711172PickBestCandidate(enc, candidates, is_key_frame, encoded_frame);11731174goto End;11751176Err:1177for (i = 0; i < CANDIDATE_COUNT; ++i) {1178if (candidates[i].evaluate_) {1179WebPMemoryWriterClear(&candidates[i].mem_);1180}1181}11821183End:1184SubFrameParamsFree(&dispose_none_params);1185SubFrameParamsFree(&dispose_bg_params);1186return error_code;1187}11881189// Calculate the penalty incurred if we encode given frame as a key frame1190// instead of a sub-frame.1191static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) {1192return ((int64_t)encoded_frame->key_frame_.bitstream.size -1193encoded_frame->sub_frame_.bitstream.size);1194}11951196static int CacheFrame(WebPAnimEncoder* const enc,1197const WebPConfig* const config) {1198int ok = 0;1199int frame_skipped = 0;1200WebPEncodingError error_code = VP8_ENC_OK;1201const size_t position = enc->count_;1202EncodedFrame* const encoded_frame = GetFrame(enc, position);12031204++enc->count_;12051206if (enc->is_first_frame_) { // Add this as a key-frame.1207error_code = SetFrame(enc, config, 1, encoded_frame, &frame_skipped);1208if (error_code != VP8_ENC_OK) goto End;1209assert(frame_skipped == 0); // First frame can't be skipped, even if empty.1210assert(position == 0 && enc->count_ == 1);1211encoded_frame->is_key_frame_ = 1;1212enc->flush_count_ = 0;1213enc->count_since_key_frame_ = 0;1214enc->prev_candidate_undecided_ = 0;1215} else {1216++enc->count_since_key_frame_;1217if (enc->count_since_key_frame_ <= enc->options_.kmin) {1218// Add this as a frame rectangle.1219error_code = SetFrame(enc, config, 0, encoded_frame, &frame_skipped);1220if (error_code != VP8_ENC_OK) goto End;1221if (frame_skipped) goto Skip;1222encoded_frame->is_key_frame_ = 0;1223enc->flush_count_ = enc->count_ - 1;1224enc->prev_candidate_undecided_ = 0;1225} else {1226int64_t curr_delta;1227FrameRectangle prev_rect_key, prev_rect_sub;12281229// Add this as a frame rectangle to enc.1230error_code = SetFrame(enc, config, 0, encoded_frame, &frame_skipped);1231if (error_code != VP8_ENC_OK) goto End;1232if (frame_skipped) goto Skip;1233prev_rect_sub = enc->prev_rect_;123412351236// Add this as a key-frame to enc, too.1237error_code = SetFrame(enc, config, 1, encoded_frame, &frame_skipped);1238if (error_code != VP8_ENC_OK) goto End;1239assert(frame_skipped == 0); // Key-frame cannot be an empty rectangle.1240prev_rect_key = enc->prev_rect_;12411242// Analyze size difference of the two variants.1243curr_delta = KeyFramePenalty(encoded_frame);1244if (curr_delta <= enc->best_delta_) { // Pick this as the key-frame.1245if (enc->keyframe_ != KEYFRAME_NONE) {1246EncodedFrame* const old_keyframe = GetFrame(enc, enc->keyframe_);1247assert(old_keyframe->is_key_frame_);1248old_keyframe->is_key_frame_ = 0;1249}1250encoded_frame->is_key_frame_ = 1;1251enc->prev_candidate_undecided_ = 1;1252enc->keyframe_ = (int)position;1253enc->best_delta_ = curr_delta;1254enc->flush_count_ = enc->count_ - 1; // We can flush previous frames.1255} else {1256encoded_frame->is_key_frame_ = 0;1257enc->prev_candidate_undecided_ = 0;1258}1259// Note: We need '>=' below because when kmin and kmax are both zero,1260// count_since_key_frame will always be > kmax.1261if (enc->count_since_key_frame_ >= enc->options_.kmax) {1262enc->flush_count_ = enc->count_ - 1;1263enc->count_since_key_frame_ = 0;1264enc->keyframe_ = KEYFRAME_NONE;1265enc->best_delta_ = DELTA_INFINITY;1266}1267if (!enc->prev_candidate_undecided_) {1268enc->prev_rect_ =1269encoded_frame->is_key_frame_ ? prev_rect_key : prev_rect_sub;1270}1271}1272}12731274// Update previous to previous and previous canvases for next call.1275WebPCopyPixels(enc->curr_canvas_, &enc->prev_canvas_);1276enc->is_first_frame_ = 0;12771278Skip:1279ok = 1;1280++enc->in_frame_count_;12811282End:1283if (!ok || frame_skipped) {1284FrameRelease(encoded_frame);1285// We reset some counters, as the frame addition failed/was skipped.1286--enc->count_;1287if (!enc->is_first_frame_) --enc->count_since_key_frame_;1288if (!ok) {1289MarkError2(enc, "ERROR adding frame. WebPEncodingError", error_code);1290}1291}1292enc->curr_canvas_->error_code = error_code; // report error_code1293assert(ok || error_code != VP8_ENC_OK);1294return ok;1295}12961297static int FlushFrames(WebPAnimEncoder* const enc) {1298while (enc->flush_count_ > 0) {1299WebPMuxError err;1300EncodedFrame* const curr = GetFrame(enc, 0);1301const WebPMuxFrameInfo* const info =1302curr->is_key_frame_ ? &curr->key_frame_ : &curr->sub_frame_;1303assert(enc->mux_ != NULL);1304err = WebPMuxPushFrame(enc->mux_, info, 1);1305if (err != WEBP_MUX_OK) {1306MarkError2(enc, "ERROR adding frame. WebPMuxError", err);1307return 0;1308}1309if (enc->options_.verbose) {1310fprintf(stderr, "INFO: Added frame. offset:%d,%d dispose:%d blend:%d\n",1311info->x_offset, info->y_offset, info->dispose_method,1312info->blend_method);1313}1314++enc->out_frame_count_;1315FrameRelease(curr);1316++enc->start_;1317--enc->flush_count_;1318--enc->count_;1319if (enc->keyframe_ != KEYFRAME_NONE) --enc->keyframe_;1320}13211322if (enc->count_ == 1 && enc->start_ != 0) {1323// Move enc->start to index 0.1324const int enc_start_tmp = (int)enc->start_;1325EncodedFrame temp = enc->encoded_frames_[0];1326enc->encoded_frames_[0] = enc->encoded_frames_[enc_start_tmp];1327enc->encoded_frames_[enc_start_tmp] = temp;1328FrameRelease(&enc->encoded_frames_[enc_start_tmp]);1329enc->start_ = 0;1330}1331return 1;1332}13331334#undef DELTA_INFINITY1335#undef KEYFRAME_NONE13361337int WebPAnimEncoderAdd(WebPAnimEncoder* enc, WebPPicture* frame, int timestamp,1338const WebPConfig* encoder_config) {1339WebPConfig config;1340int ok;13411342if (enc == NULL) {1343return 0;1344}1345MarkNoError(enc);13461347if (!enc->is_first_frame_) {1348// Make sure timestamps are non-decreasing (integer wrap-around is OK).1349const uint32_t prev_frame_duration =1350(uint32_t)timestamp - enc->prev_timestamp_;1351if (prev_frame_duration >= MAX_DURATION) {1352if (frame != NULL) {1353frame->error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;1354}1355MarkError(enc, "ERROR adding frame: timestamps must be non-decreasing");1356return 0;1357}1358if (!IncreasePreviousDuration(enc, (int)prev_frame_duration)) {1359return 0;1360}1361// IncreasePreviousDuration() may add a frame to avoid exceeding1362// MAX_DURATION which could cause CacheFrame() to over read encoded_frames_1363// before the next flush.1364if (enc->count_ == enc->size_ && !FlushFrames(enc)) {1365return 0;1366}1367} else {1368enc->first_timestamp_ = timestamp;1369}13701371if (frame == NULL) { // Special: last call.1372enc->got_null_frame_ = 1;1373enc->prev_timestamp_ = timestamp;1374return 1;1375}13761377if (frame->width != enc->canvas_width_ ||1378frame->height != enc->canvas_height_) {1379frame->error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;1380MarkError(enc, "ERROR adding frame: Invalid frame dimensions");1381return 0;1382}13831384if (!frame->use_argb) { // Convert frame from YUV(A) to ARGB.1385if (enc->options_.verbose) {1386fprintf(stderr, "WARNING: Converting frame from YUV(A) to ARGB format; "1387"this incurs a small loss.\n");1388}1389if (!WebPPictureYUVAToARGB(frame)) {1390MarkError(enc, "ERROR converting frame from YUV(A) to ARGB");1391return 0;1392}1393}13941395if (encoder_config != NULL) {1396if (!WebPValidateConfig(encoder_config)) {1397MarkError(enc, "ERROR adding frame: Invalid WebPConfig");1398return 0;1399}1400config = *encoder_config;1401} else {1402if (!WebPConfigInit(&config)) {1403MarkError(enc, "Cannot Init config");1404return 0;1405}1406config.lossless = 1;1407}1408assert(enc->curr_canvas_ == NULL);1409enc->curr_canvas_ = frame; // Store reference.1410assert(enc->curr_canvas_copy_modified_ == 1);1411CopyCurrentCanvas(enc);14121413ok = CacheFrame(enc, &config) && FlushFrames(enc);14141415enc->curr_canvas_ = NULL;1416enc->curr_canvas_copy_modified_ = 1;1417if (ok) {1418enc->prev_timestamp_ = timestamp;1419}1420return ok;1421}14221423// -----------------------------------------------------------------------------1424// Bitstream assembly.14251426WEBP_NODISCARD static int DecodeFrameOntoCanvas(1427const WebPMuxFrameInfo* const frame, WebPPicture* const canvas) {1428const WebPData* const image = &frame->bitstream;1429WebPPicture sub_image;1430WebPDecoderConfig config;1431if (!WebPInitDecoderConfig(&config)) {1432return 0;1433}1434WebPUtilClearPic(canvas, NULL);1435if (WebPGetFeatures(image->bytes, image->size, &config.input) !=1436VP8_STATUS_OK) {1437return 0;1438}1439if (!WebPPictureView(canvas, frame->x_offset, frame->y_offset,1440config.input.width, config.input.height, &sub_image)) {1441return 0;1442}1443config.output.is_external_memory = 1;1444config.output.colorspace = MODE_BGRA;1445config.output.u.RGBA.rgba = (uint8_t*)sub_image.argb;1446config.output.u.RGBA.stride = sub_image.argb_stride * 4;1447config.output.u.RGBA.size = config.output.u.RGBA.stride * sub_image.height;14481449if (WebPDecode(image->bytes, image->size, &config) != VP8_STATUS_OK) {1450return 0;1451}1452return 1;1453}14541455static int FrameToFullCanvas(WebPAnimEncoder* const enc,1456const WebPMuxFrameInfo* const frame,1457WebPData* const full_image) {1458WebPPicture* const canvas_buf = &enc->curr_canvas_copy_;1459WebPMemoryWriter mem1, mem2;1460WebPMemoryWriterInit(&mem1);1461WebPMemoryWriterInit(&mem2);14621463if (!DecodeFrameOntoCanvas(frame, canvas_buf)) goto Err;1464if (!EncodeFrame(&enc->last_config_, canvas_buf, &mem1)) goto Err;1465GetEncodedData(&mem1, full_image);14661467if (enc->options_.allow_mixed) {1468if (!EncodeFrame(&enc->last_config_reversed_, canvas_buf, &mem2)) goto Err;1469if (mem2.size < mem1.size) {1470GetEncodedData(&mem2, full_image);1471WebPMemoryWriterClear(&mem1);1472} else {1473WebPMemoryWriterClear(&mem2);1474}1475}1476return 1;14771478Err:1479WebPMemoryWriterClear(&mem1);1480WebPMemoryWriterClear(&mem2);1481return 0;1482}14831484// Convert a single-frame animation to a non-animated image if appropriate.1485// TODO(urvang): Can we pick one of the two heuristically (based on frame1486// rectangle and/or presence of alpha)?1487static WebPMuxError OptimizeSingleFrame(WebPAnimEncoder* const enc,1488WebPData* const webp_data) {1489WebPMuxError err = WEBP_MUX_OK;1490int canvas_width, canvas_height;1491WebPMuxFrameInfo frame;1492WebPData full_image;1493WebPData webp_data2;1494WebPMux* const mux = WebPMuxCreate(webp_data, 0);1495if (mux == NULL) return WEBP_MUX_BAD_DATA;1496assert(enc->out_frame_count_ == 1);1497WebPDataInit(&frame.bitstream);1498WebPDataInit(&full_image);1499WebPDataInit(&webp_data2);15001501err = WebPMuxGetFrame(mux, 1, &frame);1502if (err != WEBP_MUX_OK) goto End;1503if (frame.id != WEBP_CHUNK_ANMF) goto End; // Non-animation: nothing to do.1504err = WebPMuxGetCanvasSize(mux, &canvas_width, &canvas_height);1505if (err != WEBP_MUX_OK) goto End;1506if (!FrameToFullCanvas(enc, &frame, &full_image)) {1507err = WEBP_MUX_BAD_DATA;1508goto End;1509}1510err = WebPMuxSetImage(mux, &full_image, 1);1511if (err != WEBP_MUX_OK) goto End;1512err = WebPMuxAssemble(mux, &webp_data2);1513if (err != WEBP_MUX_OK) goto End;15141515if (webp_data2.size < webp_data->size) { // Pick 'webp_data2' if smaller.1516WebPDataClear(webp_data);1517*webp_data = webp_data2;1518WebPDataInit(&webp_data2);1519}15201521End:1522WebPDataClear(&frame.bitstream);1523WebPDataClear(&full_image);1524WebPMuxDelete(mux);1525WebPDataClear(&webp_data2);1526return err;1527}15281529int WebPAnimEncoderAssemble(WebPAnimEncoder* enc, WebPData* webp_data) {1530WebPMux* mux;1531WebPMuxError err;15321533if (enc == NULL) {1534return 0;1535}1536MarkNoError(enc);15371538if (webp_data == NULL) {1539MarkError(enc, "ERROR assembling: NULL input");1540return 0;1541}15421543if (enc->in_frame_count_ == 0) {1544MarkError(enc, "ERROR: No frames to assemble");1545return 0;1546}15471548if (!enc->got_null_frame_ && enc->in_frame_count_ > 1 && enc->count_ > 0) {1549// set duration of the last frame to be avg of durations of previous frames.1550const double delta_time =1551(uint32_t)enc->prev_timestamp_ - enc->first_timestamp_;1552const int average_duration = (int)(delta_time / (enc->in_frame_count_ - 1));1553if (!IncreasePreviousDuration(enc, average_duration)) {1554return 0;1555}1556}15571558// Flush any remaining frames.1559enc->flush_count_ = enc->count_;1560if (!FlushFrames(enc)) {1561return 0;1562}15631564// Set definitive canvas size.1565mux = enc->mux_;1566err = WebPMuxSetCanvasSize(mux, enc->canvas_width_, enc->canvas_height_);1567if (err != WEBP_MUX_OK) goto Err;15681569err = WebPMuxSetAnimationParams(mux, &enc->options_.anim_params);1570if (err != WEBP_MUX_OK) goto Err;15711572// Assemble into a WebP bitstream.1573err = WebPMuxAssemble(mux, webp_data);1574if (err != WEBP_MUX_OK) goto Err;15751576if (enc->out_frame_count_ == 1) {1577err = OptimizeSingleFrame(enc, webp_data);1578if (err != WEBP_MUX_OK) goto Err;1579}1580return 1;15811582Err:1583MarkError2(enc, "ERROR assembling WebP", err);1584return 0;1585}15861587const char* WebPAnimEncoderGetError(WebPAnimEncoder* enc) {1588if (enc == NULL) return NULL;1589return enc->error_str_;1590}15911592WebPMuxError WebPAnimEncoderSetChunk(1593WebPAnimEncoder* enc, const char fourcc[4], const WebPData* chunk_data,1594int copy_data) {1595if (enc == NULL) return WEBP_MUX_INVALID_ARGUMENT;1596return WebPMuxSetChunk(enc->mux_, fourcc, chunk_data, copy_data);1597}15981599WebPMuxError WebPAnimEncoderGetChunk(1600const WebPAnimEncoder* enc, const char fourcc[4], WebPData* chunk_data) {1601if (enc == NULL) return WEBP_MUX_INVALID_ARGUMENT;1602return WebPMuxGetChunk(enc->mux_, fourcc, chunk_data);1603}16041605WebPMuxError WebPAnimEncoderDeleteChunk(1606WebPAnimEncoder* enc, const char fourcc[4]) {1607if (enc == NULL) return WEBP_MUX_INVALID_ARGUMENT;1608return WebPMuxDeleteChunk(enc->mux_, fourcc);1609}16101611// -----------------------------------------------------------------------------161216131614