libwebp/src/mux/anim_encode.c

1124 lines
38 KiB
C
Raw Normal View History

// 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 <assert.h>
#include <stdio.h>
#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.
// 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_)) {
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_)) {
goto Err;
}
WebPUtilClearPic(&enc->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_);
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->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 OptimizeSingleFrame(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;
err = WebPMuxSetAnimationParams(mux, &enc->options_.anim_params);
if (err != WEBP_MUX_OK) goto Err;
if (enc->frame_count_ == 1) {
err = OptimizeSingleFrame(mux);
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;
}
// -----------------------------------------------------------------------------