1 // Copyright 2014 Google Inc. All Rights Reserved.
2 //
3 // Use of this source code is governed by a BSD-style license
4 // that can be found in the COPYING file in the root of the source
5 // tree. An additional intellectual property rights grant can be found
6 // in the file PATENTS. All contributing project authors may
7 // be found in the AUTHORS file in the root of the source tree.
8 // -----------------------------------------------------------------------------
9 //
10 // AnimEncoder implementation.
11 //
12
13 #include <assert.h>
14 #include <limits.h>
15 #include <math.h> // for pow()
16 #include <stdio.h>
17 #include <stdlib.h> // for abs()
18
19 #include "../mux/animi.h"
20 #include "../utils/utils.h"
21 #include "../webp/decode.h"
22 #include "../webp/encode.h"
23 #include "../webp/format_constants.h"
24 #include "../webp/mux.h"
25
26 #if defined(_MSC_VER) && _MSC_VER < 1900
27 #define snprintf _snprintf
28 #endif
29
30 #define ERROR_STR_MAX_LENGTH 100
31
32 //------------------------------------------------------------------------------
33 // Internal structs.
34
35 // Stores frame rectangle dimensions.
36 typedef struct {
37 int x_offset_, y_offset_, width_, height_;
38 } FrameRect;
39
40 // Used to store two candidates of encoded data for an animation frame. One of
41 // the two will be chosen later.
42 typedef struct {
43 WebPMuxFrameInfo sub_frame_; // Encoded frame rectangle.
44 WebPMuxFrameInfo key_frame_; // Encoded frame if it is a key-frame.
45 int is_key_frame_; // True if 'key_frame' has been chosen.
46 } EncodedFrame;
47
48 struct WebPAnimEncoder {
49 const int canvas_width_; // Canvas width.
50 const int canvas_height_; // Canvas height.
51 const WebPAnimEncoderOptions options_; // Global encoding options.
52
53 FrameRect prev_rect_; // Previous WebP frame rectangle.
54 WebPConfig last_config_; // Cached in case a re-encode is needed.
55 WebPConfig last_config_reversed_; // If 'last_config_' uses lossless, then
56 // this config uses lossy and vice versa;
57 // only valid if 'options_.allow_mixed'
58 // is true.
59
60 WebPPicture* curr_canvas_; // Only pointer; we don't own memory.
61
62 // Canvas buffers.
63 WebPPicture curr_canvas_copy_; // Possibly modified current canvas.
64 int curr_canvas_copy_modified_; // True if pixels in 'curr_canvas_copy_'
65 // differ from those in 'curr_canvas_'.
66
67 WebPPicture prev_canvas_; // Previous canvas.
68 WebPPicture prev_canvas_disposed_; // Previous canvas disposed to background.
69
70 // Encoded data.
71 EncodedFrame* encoded_frames_; // Array of encoded frames.
72 size_t size_; // Number of allocated frames.
73 size_t start_; // Frame start index.
74 size_t count_; // Number of valid frames.
75 size_t flush_count_; // If >0, 'flush_count' frames starting from
76 // 'start' are ready to be added to mux.
77
78 // key-frame related.
79 int64_t best_delta_; // min(canvas size - frame size) over the frames.
80 // Can be negative in certain cases due to
81 // transparent pixels in a frame.
82 int keyframe_; // Index of selected key-frame relative to 'start_'.
83 int count_since_key_frame_; // Frames seen since the last key-frame.
84
85 int first_timestamp_; // Timestamp of the first frame.
86 int prev_timestamp_; // Timestamp of the last added frame.
87 int prev_candidate_undecided_; // True if it's not yet decided if previous
88 // frame would be a sub-frame or a key-frame.
89
90 // Misc.
91 int is_first_frame_; // True if first frame is yet to be added/being added.
92 int got_null_frame_; // True if WebPAnimEncoderAdd() has already been called
93 // with a NULL frame.
94
95 size_t in_frame_count_; // Number of input frames processed so far.
96 size_t out_frame_count_; // Number of frames added to mux so far. This may be
97 // different from 'in_frame_count_' due to merging.
98
99 WebPMux* mux_; // Muxer to assemble the WebP bitstream.
100 char error_str_[ERROR_STR_MAX_LENGTH]; // Error string. Empty if no error.
101 };
102
103 // -----------------------------------------------------------------------------
104 // Life of WebPAnimEncoder object.
105
106 #define DELTA_INFINITY (1ULL << 32)
107 #define KEYFRAME_NONE (-1)
108
109 // Reset the counters in the WebPAnimEncoder.
ResetCounters(WebPAnimEncoder * const enc)110 static void ResetCounters(WebPAnimEncoder* const enc) {
111 enc->start_ = 0;
112 enc->count_ = 0;
113 enc->flush_count_ = 0;
114 enc->best_delta_ = DELTA_INFINITY;
115 enc->keyframe_ = KEYFRAME_NONE;
116 }
117
DisableKeyframes(WebPAnimEncoderOptions * const enc_options)118 static void DisableKeyframes(WebPAnimEncoderOptions* const enc_options) {
119 enc_options->kmax = INT_MAX;
120 enc_options->kmin = enc_options->kmax - 1;
121 }
122
123 #define MAX_CACHED_FRAMES 30
124
SanitizeEncoderOptions(WebPAnimEncoderOptions * const enc_options)125 static void SanitizeEncoderOptions(WebPAnimEncoderOptions* const enc_options) {
126 int print_warning = enc_options->verbose;
127
128 if (enc_options->minimize_size) {
129 DisableKeyframes(enc_options);
130 }
131
132 if (enc_options->kmax == 1) { // All frames will be key-frames.
133 enc_options->kmin = 0;
134 enc_options->kmax = 0;
135 return;
136 } else if (enc_options->kmax <= 0) {
137 DisableKeyframes(enc_options);
138 print_warning = 0;
139 }
140
141 if (enc_options->kmin >= enc_options->kmax) {
142 enc_options->kmin = enc_options->kmax - 1;
143 if (print_warning) {
144 fprintf(stderr, "WARNING: Setting kmin = %d, so that kmin < kmax.\n",
145 enc_options->kmin);
146 }
147 } else {
148 const int kmin_limit = enc_options->kmax / 2 + 1;
149 if (enc_options->kmin < kmin_limit && kmin_limit < enc_options->kmax) {
150 // This ensures that enc.keyframe + kmin >= kmax is always true. So, we
151 // can flush all the frames in the 'count_since_key_frame == kmax' case.
152 enc_options->kmin = kmin_limit;
153 if (print_warning) {
154 fprintf(stderr,
155 "WARNING: Setting kmin = %d, so that kmin >= kmax / 2 + 1.\n",
156 enc_options->kmin);
157 }
158 }
159 }
160 // Limit the max number of frames that are allocated.
161 if (enc_options->kmax - enc_options->kmin > MAX_CACHED_FRAMES) {
162 enc_options->kmin = enc_options->kmax - MAX_CACHED_FRAMES;
163 if (print_warning) {
164 fprintf(stderr,
165 "WARNING: Setting kmin = %d, so that kmax - kmin <= %d.\n",
166 enc_options->kmin, MAX_CACHED_FRAMES);
167 }
168 }
169 assert(enc_options->kmin < enc_options->kmax);
170 }
171
172 #undef MAX_CACHED_FRAMES
173
DefaultEncoderOptions(WebPAnimEncoderOptions * const enc_options)174 static void DefaultEncoderOptions(WebPAnimEncoderOptions* const enc_options) {
175 enc_options->anim_params.loop_count = 0;
176 enc_options->anim_params.bgcolor = 0xffffffff; // White.
177 enc_options->minimize_size = 0;
178 DisableKeyframes(enc_options);
179 enc_options->allow_mixed = 0;
180 enc_options->verbose = 0;
181 }
182
WebPAnimEncoderOptionsInitInternal(WebPAnimEncoderOptions * enc_options,int abi_version)183 int WebPAnimEncoderOptionsInitInternal(WebPAnimEncoderOptions* enc_options,
184 int abi_version) {
185 if (enc_options == NULL ||
186 WEBP_ABI_IS_INCOMPATIBLE(abi_version, WEBP_MUX_ABI_VERSION)) {
187 return 0;
188 }
189 DefaultEncoderOptions(enc_options);
190 return 1;
191 }
192
193 // This starting value is more fit to WebPCleanupTransparentAreaLossless().
194 #define TRANSPARENT_COLOR 0x00000000
195
ClearRectangle(WebPPicture * const picture,int left,int top,int width,int height)196 static void ClearRectangle(WebPPicture* const picture,
197 int left, int top, int width, int height) {
198 int j;
199 for (j = top; j < top + height; ++j) {
200 uint32_t* const dst = picture->argb + j * picture->argb_stride;
201 int i;
202 for (i = left; i < left + width; ++i) {
203 dst[i] = TRANSPARENT_COLOR;
204 }
205 }
206 }
207
WebPUtilClearPic(WebPPicture * const picture,const FrameRect * const rect)208 static void WebPUtilClearPic(WebPPicture* const picture,
209 const FrameRect* const rect) {
210 if (rect != NULL) {
211 ClearRectangle(picture, rect->x_offset_, rect->y_offset_,
212 rect->width_, rect->height_);
213 } else {
214 ClearRectangle(picture, 0, 0, picture->width, picture->height);
215 }
216 }
217
MarkNoError(WebPAnimEncoder * const enc)218 static void MarkNoError(WebPAnimEncoder* const enc) {
219 enc->error_str_[0] = '\0'; // Empty string.
220 }
221
MarkError(WebPAnimEncoder * const enc,const char * str)222 static void MarkError(WebPAnimEncoder* const enc, const char* str) {
223 if (snprintf(enc->error_str_, ERROR_STR_MAX_LENGTH, "%s.", str) < 0) {
224 assert(0); // FIX ME!
225 }
226 }
227
MarkError2(WebPAnimEncoder * const enc,const char * str,int error_code)228 static void MarkError2(WebPAnimEncoder* const enc,
229 const char* str, int error_code) {
230 if (snprintf(enc->error_str_, ERROR_STR_MAX_LENGTH, "%s: %d.", str,
231 error_code) < 0) {
232 assert(0); // FIX ME!
233 }
234 }
235
WebPAnimEncoderNewInternal(int width,int height,const WebPAnimEncoderOptions * enc_options,int abi_version)236 WebPAnimEncoder* WebPAnimEncoderNewInternal(
237 int width, int height, const WebPAnimEncoderOptions* enc_options,
238 int abi_version) {
239 WebPAnimEncoder* enc;
240
241 if (WEBP_ABI_IS_INCOMPATIBLE(abi_version, WEBP_MUX_ABI_VERSION)) {
242 return NULL;
243 }
244 if (width <= 0 || height <= 0 ||
245 (width * (uint64_t)height) >= MAX_IMAGE_AREA) {
246 return NULL;
247 }
248
249 enc = (WebPAnimEncoder*)WebPSafeCalloc(1, sizeof(*enc));
250 if (enc == NULL) return NULL;
251 // sanity inits, so we can call WebPAnimEncoderDelete():
252 enc->encoded_frames_ = NULL;
253 enc->mux_ = NULL;
254 MarkNoError(enc);
255
256 // Dimensions and options.
257 *(int*)&enc->canvas_width_ = width;
258 *(int*)&enc->canvas_height_ = height;
259 if (enc_options != NULL) {
260 *(WebPAnimEncoderOptions*)&enc->options_ = *enc_options;
261 SanitizeEncoderOptions((WebPAnimEncoderOptions*)&enc->options_);
262 } else {
263 DefaultEncoderOptions((WebPAnimEncoderOptions*)&enc->options_);
264 }
265
266 // Canvas buffers.
267 if (!WebPPictureInit(&enc->curr_canvas_copy_) ||
268 !WebPPictureInit(&enc->prev_canvas_) ||
269 !WebPPictureInit(&enc->prev_canvas_disposed_)) {
270 goto Err;
271 }
272 enc->curr_canvas_copy_.width = width;
273 enc->curr_canvas_copy_.height = height;
274 enc->curr_canvas_copy_.use_argb = 1;
275 if (!WebPPictureAlloc(&enc->curr_canvas_copy_) ||
276 !WebPPictureCopy(&enc->curr_canvas_copy_, &enc->prev_canvas_) ||
277 !WebPPictureCopy(&enc->curr_canvas_copy_, &enc->prev_canvas_disposed_)) {
278 goto Err;
279 }
280 WebPUtilClearPic(&enc->prev_canvas_, NULL);
281 enc->curr_canvas_copy_modified_ = 1;
282
283 // Encoded frames.
284 ResetCounters(enc);
285 // Note: one extra storage is for the previous frame.
286 enc->size_ = enc->options_.kmax - enc->options_.kmin + 1;
287 // We need space for at least 2 frames. But when kmin, kmax are both zero,
288 // enc->size_ will be 1. So we handle that special case below.
289 if (enc->size_ < 2) enc->size_ = 2;
290 enc->encoded_frames_ =
291 (EncodedFrame*)WebPSafeCalloc(enc->size_, sizeof(*enc->encoded_frames_));
292 if (enc->encoded_frames_ == NULL) goto Err;
293
294 enc->mux_ = WebPMuxNew();
295 if (enc->mux_ == NULL) goto Err;
296
297 enc->count_since_key_frame_ = 0;
298 enc->first_timestamp_ = 0;
299 enc->prev_timestamp_ = 0;
300 enc->prev_candidate_undecided_ = 0;
301 enc->is_first_frame_ = 1;
302 enc->got_null_frame_ = 0;
303
304 return enc; // All OK.
305
306 Err:
307 WebPAnimEncoderDelete(enc);
308 return NULL;
309 }
310
311 // Release the data contained by 'encoded_frame'.
FrameRelease(EncodedFrame * const encoded_frame)312 static void FrameRelease(EncodedFrame* const encoded_frame) {
313 if (encoded_frame != NULL) {
314 WebPDataClear(&encoded_frame->sub_frame_.bitstream);
315 WebPDataClear(&encoded_frame->key_frame_.bitstream);
316 memset(encoded_frame, 0, sizeof(*encoded_frame));
317 }
318 }
319
WebPAnimEncoderDelete(WebPAnimEncoder * enc)320 void WebPAnimEncoderDelete(WebPAnimEncoder* enc) {
321 if (enc != NULL) {
322 WebPPictureFree(&enc->curr_canvas_copy_);
323 WebPPictureFree(&enc->prev_canvas_);
324 WebPPictureFree(&enc->prev_canvas_disposed_);
325 if (enc->encoded_frames_ != NULL) {
326 size_t i;
327 for (i = 0; i < enc->size_; ++i) {
328 FrameRelease(&enc->encoded_frames_[i]);
329 }
330 WebPSafeFree(enc->encoded_frames_);
331 }
332 WebPMuxDelete(enc->mux_);
333 WebPSafeFree(enc);
334 }
335 }
336
337 // -----------------------------------------------------------------------------
338 // Frame addition.
339
340 // Returns cached frame at the given 'position'.
GetFrame(const WebPAnimEncoder * const enc,size_t position)341 static EncodedFrame* GetFrame(const WebPAnimEncoder* const enc,
342 size_t position) {
343 assert(enc->start_ + position < enc->size_);
344 return &enc->encoded_frames_[enc->start_ + position];
345 }
346
347 typedef int (*ComparePixelsFunc)(const uint32_t*, int, const uint32_t*, int,
348 int, int);
349
350 // Returns true if 'length' number of pixels in 'src' and 'dst' are equal,
351 // assuming the given step sizes between pixels.
352 // 'max_allowed_diff' is unused and only there to allow function pointer use.
ComparePixelsLossless(const uint32_t * src,int src_step,const uint32_t * dst,int dst_step,int length,int max_allowed_diff)353 static WEBP_INLINE int ComparePixelsLossless(const uint32_t* src, int src_step,
354 const uint32_t* dst, int dst_step,
355 int length, int max_allowed_diff) {
356 (void)max_allowed_diff;
357 assert(length > 0);
358 while (length-- > 0) {
359 if (*src != *dst) {
360 return 0;
361 }
362 src += src_step;
363 dst += dst_step;
364 }
365 return 1;
366 }
367
368 // Helper to check if each channel in 'src' and 'dst' is at most off by
369 // 'max_allowed_diff'.
PixelsAreSimilar(uint32_t src,uint32_t dst,int max_allowed_diff)370 static WEBP_INLINE int PixelsAreSimilar(uint32_t src, uint32_t dst,
371 int max_allowed_diff) {
372 const int src_a = (src >> 24) & 0xff;
373 const int src_r = (src >> 16) & 0xff;
374 const int src_g = (src >> 8) & 0xff;
375 const int src_b = (src >> 0) & 0xff;
376 const int dst_a = (dst >> 24) & 0xff;
377 const int dst_r = (dst >> 16) & 0xff;
378 const int dst_g = (dst >> 8) & 0xff;
379 const int dst_b = (dst >> 0) & 0xff;
380
381 return (src_a == dst_a) &&
382 (abs(src_r - dst_r) * dst_a <= (max_allowed_diff * 255)) &&
383 (abs(src_g - dst_g) * dst_a <= (max_allowed_diff * 255)) &&
384 (abs(src_b - dst_b) * dst_a <= (max_allowed_diff * 255));
385 }
386
387 // Returns true if 'length' number of pixels in 'src' and 'dst' are within an
388 // error bound, assuming the given step sizes between pixels.
ComparePixelsLossy(const uint32_t * src,int src_step,const uint32_t * dst,int dst_step,int length,int max_allowed_diff)389 static WEBP_INLINE int ComparePixelsLossy(const uint32_t* src, int src_step,
390 const uint32_t* dst, int dst_step,
391 int length, int max_allowed_diff) {
392 assert(length > 0);
393 while (length-- > 0) {
394 if (!PixelsAreSimilar(*src, *dst, max_allowed_diff)) {
395 return 0;
396 }
397 src += src_step;
398 dst += dst_step;
399 }
400 return 1;
401 }
402
IsEmptyRect(const FrameRect * const rect)403 static int IsEmptyRect(const FrameRect* const rect) {
404 return (rect->width_ == 0) || (rect->height_ == 0);
405 }
406
QualityToMaxDiff(float quality)407 static int QualityToMaxDiff(float quality) {
408 const double val = pow(quality / 100., 0.5);
409 const double max_diff = 31 * (1 - val) + 1 * val;
410 return (int)(max_diff + 0.5);
411 }
412
413 // Assumes that an initial valid guess of change rectangle 'rect' is passed.
MinimizeChangeRectangle(const WebPPicture * const src,const WebPPicture * const dst,FrameRect * const rect,int is_lossless,float quality)414 static void MinimizeChangeRectangle(const WebPPicture* const src,
415 const WebPPicture* const dst,
416 FrameRect* const rect,
417 int is_lossless, float quality) {
418 int i, j;
419 const ComparePixelsFunc compare_pixels =
420 is_lossless ? ComparePixelsLossless : ComparePixelsLossy;
421 const int max_allowed_diff_lossy = QualityToMaxDiff(quality);
422 const int max_allowed_diff = is_lossless ? 0 : max_allowed_diff_lossy;
423
424 // Sanity checks.
425 assert(src->width == dst->width && src->height == dst->height);
426 assert(rect->x_offset_ + rect->width_ <= dst->width);
427 assert(rect->y_offset_ + rect->height_ <= dst->height);
428
429 // Left boundary.
430 for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {
431 const uint32_t* const src_argb =
432 &src->argb[rect->y_offset_ * src->argb_stride + i];
433 const uint32_t* const dst_argb =
434 &dst->argb[rect->y_offset_ * dst->argb_stride + i];
435 if (compare_pixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride,
436 rect->height_, max_allowed_diff)) {
437 --rect->width_; // Redundant column.
438 ++rect->x_offset_;
439 } else {
440 break;
441 }
442 }
443 if (rect->width_ == 0) goto NoChange;
444
445 // Right boundary.
446 for (i = rect->x_offset_ + rect->width_ - 1; i >= rect->x_offset_; --i) {
447 const uint32_t* const src_argb =
448 &src->argb[rect->y_offset_ * src->argb_stride + i];
449 const uint32_t* const dst_argb =
450 &dst->argb[rect->y_offset_ * dst->argb_stride + i];
451 if (compare_pixels(src_argb, src->argb_stride, dst_argb, dst->argb_stride,
452 rect->height_, max_allowed_diff)) {
453 --rect->width_; // Redundant column.
454 } else {
455 break;
456 }
457 }
458 if (rect->width_ == 0) goto NoChange;
459
460 // Top boundary.
461 for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {
462 const uint32_t* const src_argb =
463 &src->argb[j * src->argb_stride + rect->x_offset_];
464 const uint32_t* const dst_argb =
465 &dst->argb[j * dst->argb_stride + rect->x_offset_];
466 if (compare_pixels(src_argb, 1, dst_argb, 1, rect->width_,
467 max_allowed_diff)) {
468 --rect->height_; // Redundant row.
469 ++rect->y_offset_;
470 } else {
471 break;
472 }
473 }
474 if (rect->height_ == 0) goto NoChange;
475
476 // Bottom boundary.
477 for (j = rect->y_offset_ + rect->height_ - 1; j >= rect->y_offset_; --j) {
478 const uint32_t* const src_argb =
479 &src->argb[j * src->argb_stride + rect->x_offset_];
480 const uint32_t* const dst_argb =
481 &dst->argb[j * dst->argb_stride + rect->x_offset_];
482 if (compare_pixels(src_argb, 1, dst_argb, 1, rect->width_,
483 max_allowed_diff)) {
484 --rect->height_; // Redundant row.
485 } else {
486 break;
487 }
488 }
489 if (rect->height_ == 0) goto NoChange;
490
491 if (IsEmptyRect(rect)) {
492 NoChange:
493 rect->x_offset_ = 0;
494 rect->y_offset_ = 0;
495 rect->width_ = 0;
496 rect->height_ = 0;
497 }
498 }
499
500 // Snap rectangle to even offsets (and adjust dimensions if needed).
SnapToEvenOffsets(FrameRect * const rect)501 static WEBP_INLINE void SnapToEvenOffsets(FrameRect* const rect) {
502 rect->width_ += (rect->x_offset_ & 1);
503 rect->height_ += (rect->y_offset_ & 1);
504 rect->x_offset_ &= ~1;
505 rect->y_offset_ &= ~1;
506 }
507
508 typedef struct {
509 int should_try_; // Should try this set of parameters.
510 int empty_rect_allowed_; // Frame with empty rectangle can be skipped.
511 FrameRect rect_ll_; // Frame rectangle for lossless compression.
512 WebPPicture sub_frame_ll_; // Sub-frame pic for lossless compression.
513 FrameRect rect_lossy_; // Frame rectangle for lossy compression.
514 // Could be smaller than rect_ll_ as pixels
515 // with small diffs can be ignored.
516 WebPPicture sub_frame_lossy_; // Sub-frame pic for lossless compression.
517 } SubFrameParams;
518
SubFrameParamsInit(SubFrameParams * const params,int should_try,int empty_rect_allowed)519 static int SubFrameParamsInit(SubFrameParams* const params,
520 int should_try, int empty_rect_allowed) {
521 params->should_try_ = should_try;
522 params->empty_rect_allowed_ = empty_rect_allowed;
523 if (!WebPPictureInit(¶ms->sub_frame_ll_) ||
524 !WebPPictureInit(¶ms->sub_frame_lossy_)) {
525 return 0;
526 }
527 return 1;
528 }
529
SubFrameParamsFree(SubFrameParams * const params)530 static void SubFrameParamsFree(SubFrameParams* const params) {
531 WebPPictureFree(¶ms->sub_frame_ll_);
532 WebPPictureFree(¶ms->sub_frame_lossy_);
533 }
534
535 // Given previous and current canvas, picks the optimal rectangle for the
536 // current frame based on 'is_lossless' and other parameters. Assumes that the
537 // initial guess 'rect' is valid.
GetSubRect(const WebPPicture * const prev_canvas,const WebPPicture * const curr_canvas,int is_key_frame,int is_first_frame,int empty_rect_allowed,int is_lossless,float quality,FrameRect * const rect,WebPPicture * const sub_frame)538 static int GetSubRect(const WebPPicture* const prev_canvas,
539 const WebPPicture* const curr_canvas, int is_key_frame,
540 int is_first_frame, int empty_rect_allowed,
541 int is_lossless, float quality, FrameRect* const rect,
542 WebPPicture* const sub_frame) {
543 if (!is_key_frame || is_first_frame) { // Optimize frame rectangle.
544 // Note: This behaves as expected for first frame, as 'prev_canvas' is
545 // initialized to a fully transparent canvas in the beginning.
546 MinimizeChangeRectangle(prev_canvas, curr_canvas, rect,
547 is_lossless, quality);
548 }
549
550 if (IsEmptyRect(rect)) {
551 if (empty_rect_allowed) { // No need to get 'sub_frame'.
552 return 1;
553 } else { // Force a 1x1 rectangle.
554 rect->width_ = 1;
555 rect->height_ = 1;
556 assert(rect->x_offset_ == 0);
557 assert(rect->y_offset_ == 0);
558 }
559 }
560
561 SnapToEvenOffsets(rect);
562 return WebPPictureView(curr_canvas, rect->x_offset_, rect->y_offset_,
563 rect->width_, rect->height_, sub_frame);
564 }
565
566 // Picks optimal frame rectangle for both lossless and lossy compression. The
567 // initial guess for frame rectangles will be the full canvas.
GetSubRects(const WebPPicture * const prev_canvas,const WebPPicture * const curr_canvas,int is_key_frame,int is_first_frame,float quality,SubFrameParams * const params)568 static int GetSubRects(const WebPPicture* const prev_canvas,
569 const WebPPicture* const curr_canvas, int is_key_frame,
570 int is_first_frame, float quality,
571 SubFrameParams* const params) {
572 // Lossless frame rectangle.
573 params->rect_ll_.x_offset_ = 0;
574 params->rect_ll_.y_offset_ = 0;
575 params->rect_ll_.width_ = curr_canvas->width;
576 params->rect_ll_.height_ = curr_canvas->height;
577 if (!GetSubRect(prev_canvas, curr_canvas, is_key_frame, is_first_frame,
578 params->empty_rect_allowed_, 1, quality,
579 ¶ms->rect_ll_, ¶ms->sub_frame_ll_)) {
580 return 0;
581 }
582 // Lossy frame rectangle.
583 params->rect_lossy_ = params->rect_ll_; // seed with lossless rect.
584 return GetSubRect(prev_canvas, curr_canvas, is_key_frame, is_first_frame,
585 params->empty_rect_allowed_, 0, quality,
586 ¶ms->rect_lossy_, ¶ms->sub_frame_lossy_);
587 }
588
clip(int v,int min_v,int max_v)589 static WEBP_INLINE int clip(int v, int min_v, int max_v) {
590 return (v < min_v) ? min_v : (v > max_v) ? max_v : v;
591 }
592
WebPAnimEncoderRefineRect(const WebPPicture * const prev_canvas,const WebPPicture * const curr_canvas,int is_lossless,float quality,int * const x_offset,int * const y_offset,int * const width,int * const height)593 int WebPAnimEncoderRefineRect(
594 const WebPPicture* const prev_canvas, const WebPPicture* const curr_canvas,
595 int is_lossless, float quality, int* const x_offset, int* const y_offset,
596 int* const width, int* const height) {
597 FrameRect rect;
598 const int right = clip(*x_offset + *width, 0, curr_canvas->width);
599 const int left = clip(*x_offset, 0, curr_canvas->width - 1);
600 const int bottom = clip(*y_offset + *height, 0, curr_canvas->height);
601 const int top = clip(*y_offset, 0, curr_canvas->height - 1);
602 if (prev_canvas == NULL || curr_canvas == NULL ||
603 prev_canvas->width != curr_canvas->width ||
604 prev_canvas->height != curr_canvas->height ||
605 !prev_canvas->use_argb || !curr_canvas->use_argb) {
606 return 0;
607 }
608 rect.x_offset_ = left;
609 rect.y_offset_ = top;
610 rect.width_ = clip(right - left, 0, curr_canvas->width - rect.x_offset_);
611 rect.height_ = clip(bottom - top, 0, curr_canvas->height - rect.y_offset_);
612 MinimizeChangeRectangle(prev_canvas, curr_canvas, &rect, is_lossless,
613 quality);
614 SnapToEvenOffsets(&rect);
615 *x_offset = rect.x_offset_;
616 *y_offset = rect.y_offset_;
617 *width = rect.width_;
618 *height = rect.height_;
619 return 1;
620 }
621
DisposeFrameRectangle(int dispose_method,const FrameRect * const rect,WebPPicture * const curr_canvas)622 static void DisposeFrameRectangle(int dispose_method,
623 const FrameRect* const rect,
624 WebPPicture* const curr_canvas) {
625 assert(rect != NULL);
626 if (dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
627 WebPUtilClearPic(curr_canvas, rect);
628 }
629 }
630
RectArea(const FrameRect * const rect)631 static uint32_t RectArea(const FrameRect* const rect) {
632 return (uint32_t)rect->width_ * rect->height_;
633 }
634
IsLosslessBlendingPossible(const WebPPicture * const src,const WebPPicture * const dst,const FrameRect * const rect)635 static int IsLosslessBlendingPossible(const WebPPicture* const src,
636 const WebPPicture* const dst,
637 const FrameRect* const rect) {
638 int i, j;
639 assert(src->width == dst->width && src->height == dst->height);
640 assert(rect->x_offset_ + rect->width_ <= dst->width);
641 assert(rect->y_offset_ + rect->height_ <= dst->height);
642 for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {
643 for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {
644 const uint32_t src_pixel = src->argb[j * src->argb_stride + i];
645 const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i];
646 const uint32_t dst_alpha = dst_pixel >> 24;
647 if (dst_alpha != 0xff && src_pixel != dst_pixel) {
648 // In this case, if we use blending, we can't attain the desired
649 // 'dst_pixel' value for this pixel. So, blending is not possible.
650 return 0;
651 }
652 }
653 }
654 return 1;
655 }
656
IsLossyBlendingPossible(const WebPPicture * const src,const WebPPicture * const dst,const FrameRect * const rect,float quality)657 static int IsLossyBlendingPossible(const WebPPicture* const src,
658 const WebPPicture* const dst,
659 const FrameRect* const rect,
660 float quality) {
661 const int max_allowed_diff_lossy = QualityToMaxDiff(quality);
662 int i, j;
663 assert(src->width == dst->width && src->height == dst->height);
664 assert(rect->x_offset_ + rect->width_ <= dst->width);
665 assert(rect->y_offset_ + rect->height_ <= dst->height);
666 for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {
667 for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {
668 const uint32_t src_pixel = src->argb[j * src->argb_stride + i];
669 const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i];
670 const uint32_t dst_alpha = dst_pixel >> 24;
671 if (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 desired
674 // 'dst_pixel' value for this pixel. So, blending is not possible.
675 return 0;
676 }
677 }
678 }
679 return 1;
680 }
681
682 // For pixels in 'rect', replace those pixels in 'dst' that are same as 'src' by
683 // transparent pixels.
684 // Returns true if at least one pixel gets modified.
IncreaseTransparency(const WebPPicture * const src,const FrameRect * const rect,WebPPicture * const dst)685 static int IncreaseTransparency(const WebPPicture* const src,
686 const FrameRect* const rect,
687 WebPPicture* const dst) {
688 int i, j;
689 int modified = 0;
690 assert(src != NULL && dst != NULL && rect != NULL);
691 assert(src->width == dst->width && src->height == dst->height);
692 for (j = rect->y_offset_; j < rect->y_offset_ + rect->height_; ++j) {
693 const uint32_t* const psrc = src->argb + j * src->argb_stride;
694 uint32_t* const pdst = dst->argb + j * dst->argb_stride;
695 for (i = rect->x_offset_; i < rect->x_offset_ + rect->width_; ++i) {
696 if (psrc[i] == pdst[i] && pdst[i] != TRANSPARENT_COLOR) {
697 pdst[i] = TRANSPARENT_COLOR;
698 modified = 1;
699 }
700 }
701 }
702 return modified;
703 }
704
705 #undef TRANSPARENT_COLOR
706
707 // Replace similar blocks of pixels by a 'see-through' transparent block
708 // with uniform average color.
709 // Assumes lossy compression is being used.
710 // Returns true if at least one pixel gets modified.
FlattenSimilarBlocks(const WebPPicture * const src,const FrameRect * const rect,WebPPicture * const dst,float quality)711 static int FlattenSimilarBlocks(const WebPPicture* const src,
712 const FrameRect* const rect,
713 WebPPicture* const dst, float quality) {
714 const int max_allowed_diff_lossy = QualityToMaxDiff(quality);
715 int i, j;
716 int modified = 0;
717 const int block_size = 8;
718 const int y_start = (rect->y_offset_ + block_size) & ~(block_size - 1);
719 const int y_end = (rect->y_offset_ + rect->height_) & ~(block_size - 1);
720 const int x_start = (rect->x_offset_ + block_size) & ~(block_size - 1);
721 const int x_end = (rect->x_offset_ + rect->width_) & ~(block_size - 1);
722 assert(src != NULL && dst != NULL && rect != NULL);
723 assert(src->width == dst->width && src->height == dst->height);
724 assert((block_size & (block_size - 1)) == 0); // must be a power of 2
725 // Iterate over each block and count similar pixels.
726 for (j = y_start; j < y_end; j += block_size) {
727 for (i = x_start; i < x_end; i += block_size) {
728 int cnt = 0;
729 int avg_r = 0, avg_g = 0, avg_b = 0;
730 int x, y;
731 const uint32_t* const psrc = src->argb + j * src->argb_stride + i;
732 uint32_t* const pdst = dst->argb + j * dst->argb_stride + i;
733 for (y = 0; y < block_size; ++y) {
734 for (x = 0; x < block_size; ++x) {
735 const uint32_t src_pixel = psrc[x + y * src->argb_stride];
736 const int alpha = src_pixel >> 24;
737 if (alpha == 0xff &&
738 PixelsAreSimilar(src_pixel, pdst[x + y * dst->argb_stride],
739 max_allowed_diff_lossy)) {
740 ++cnt;
741 avg_r += (src_pixel >> 16) & 0xff;
742 avg_g += (src_pixel >> 8) & 0xff;
743 avg_b += (src_pixel >> 0) & 0xff;
744 }
745 }
746 }
747 // If we have a fully similar block, we replace it with an
748 // average transparent block. This compresses better in lossy mode.
749 if (cnt == block_size * block_size) {
750 const uint32_t color = (0x00 << 24) |
751 ((avg_r / cnt) << 16) |
752 ((avg_g / cnt) << 8) |
753 ((avg_b / cnt) << 0);
754 for (y = 0; y < block_size; ++y) {
755 for (x = 0; x < block_size; ++x) {
756 pdst[x + y * dst->argb_stride] = color;
757 }
758 }
759 modified = 1;
760 }
761 }
762 }
763 return modified;
764 }
765
EncodeFrame(const WebPConfig * const config,WebPPicture * const pic,WebPMemoryWriter * const memory)766 static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic,
767 WebPMemoryWriter* const memory) {
768 pic->use_argb = 1;
769 pic->writer = WebPMemoryWrite;
770 pic->custom_ptr = memory;
771 if (!WebPEncode(config, pic)) {
772 return 0;
773 }
774 return 1;
775 }
776
777 // Struct representing a candidate encoded frame including its metadata.
778 typedef struct {
779 WebPMemoryWriter mem_;
780 WebPMuxFrameInfo info_;
781 FrameRect rect_;
782 int evaluate_; // True if this candidate should be evaluated.
783 } Candidate;
784
785 // Generates a candidate encoded frame given a picture and metadata.
EncodeCandidate(WebPPicture * const sub_frame,const FrameRect * const rect,const WebPConfig * const encoder_config,int use_blending,Candidate * const candidate)786 static WebPEncodingError EncodeCandidate(WebPPicture* const sub_frame,
787 const FrameRect* const rect,
788 const WebPConfig* const encoder_config,
789 int use_blending,
790 Candidate* const candidate) {
791 WebPConfig config = *encoder_config;
792 WebPEncodingError error_code = VP8_ENC_OK;
793 assert(candidate != NULL);
794 memset(candidate, 0, sizeof(*candidate));
795
796 // Set frame rect and info.
797 candidate->rect_ = *rect;
798 candidate->info_.id = WEBP_CHUNK_ANMF;
799 candidate->info_.x_offset = rect->x_offset_;
800 candidate->info_.y_offset = rect->y_offset_;
801 candidate->info_.dispose_method = WEBP_MUX_DISPOSE_NONE; // Set later.
802 candidate->info_.blend_method =
803 use_blending ? WEBP_MUX_BLEND : WEBP_MUX_NO_BLEND;
804 candidate->info_.duration = 0; // Set in next call to WebPAnimEncoderAdd().
805
806 // Encode picture.
807 WebPMemoryWriterInit(&candidate->mem_);
808
809 if (!config.lossless && use_blending) {
810 // Disable filtering to avoid blockiness in reconstructed frames at the
811 // time of decoding.
812 config.autofilter = 0;
813 config.filter_strength = 0;
814 }
815 if (!EncodeFrame(&config, sub_frame, &candidate->mem_)) {
816 error_code = sub_frame->error_code;
817 goto Err;
818 }
819
820 candidate->evaluate_ = 1;
821 return error_code;
822
823 Err:
824 WebPMemoryWriterClear(&candidate->mem_);
825 return error_code;
826 }
827
CopyCurrentCanvas(WebPAnimEncoder * const enc)828 static void CopyCurrentCanvas(WebPAnimEncoder* const enc) {
829 if (enc->curr_canvas_copy_modified_) {
830 WebPCopyPixels(enc->curr_canvas_, &enc->curr_canvas_copy_);
831 enc->curr_canvas_copy_.progress_hook = enc->curr_canvas_->progress_hook;
832 enc->curr_canvas_copy_.user_data = enc->curr_canvas_->user_data;
833 enc->curr_canvas_copy_modified_ = 0;
834 }
835 }
836
837 enum {
838 LL_DISP_NONE = 0,
839 LL_DISP_BG,
840 LOSSY_DISP_NONE,
841 LOSSY_DISP_BG,
842 CANDIDATE_COUNT
843 };
844
845 #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.
847
848 // Generates candidates for a given dispose method given pre-filled sub-frame
849 // 'params'.
GenerateCandidates(WebPAnimEncoder * const enc,Candidate candidates[CANDIDATE_COUNT],WebPMuxAnimDispose dispose_method,int is_lossless,int is_key_frame,SubFrameParams * const params,const WebPConfig * const config_ll,const WebPConfig * const config_lossy)850 static WebPEncodingError GenerateCandidates(
851 WebPAnimEncoder* const enc, Candidate candidates[CANDIDATE_COUNT],
852 WebPMuxAnimDispose dispose_method, int is_lossless, int is_key_frame,
853 SubFrameParams* const params,
854 const WebPConfig* const config_ll, const WebPConfig* const config_lossy) {
855 WebPEncodingError error_code = VP8_ENC_OK;
856 const int is_dispose_none = (dispose_method == WEBP_MUX_DISPOSE_NONE);
857 Candidate* const candidate_ll =
858 is_dispose_none ? &candidates[LL_DISP_NONE] : &candidates[LL_DISP_BG];
859 Candidate* const candidate_lossy = is_dispose_none
860 ? &candidates[LOSSY_DISP_NONE]
861 : &candidates[LOSSY_DISP_BG];
862 WebPPicture* const curr_canvas = &enc->curr_canvas_copy_;
863 const WebPPicture* const prev_canvas =
864 is_dispose_none ? &enc->prev_canvas_ : &enc->prev_canvas_disposed_;
865 int use_blending_ll, use_blending_lossy;
866 int evaluate_ll, evaluate_lossy;
867
868 CopyCurrentCanvas(enc);
869 use_blending_ll =
870 !is_key_frame &&
871 IsLosslessBlendingPossible(prev_canvas, curr_canvas, ¶ms->rect_ll_);
872 use_blending_lossy =
873 !is_key_frame &&
874 IsLossyBlendingPossible(prev_canvas, curr_canvas, ¶ms->rect_lossy_,
875 config_lossy->quality);
876
877 // Pick candidates to be tried.
878 if (!enc->options_.allow_mixed) {
879 evaluate_ll = is_lossless;
880 evaluate_lossy = !is_lossless;
881 } else if (enc->options_.minimize_size) {
882 evaluate_ll = 1;
883 evaluate_lossy = 1;
884 } else { // Use a heuristic for trying lossless and/or lossy compression.
885 const int num_colors = WebPGetColorPalette(¶ms->sub_frame_ll_, NULL);
886 evaluate_ll = (num_colors < MAX_COLORS_LOSSLESS);
887 evaluate_lossy = (num_colors >= MIN_COLORS_LOSSY);
888 }
889
890 // Generate candidates.
891 if (evaluate_ll) {
892 CopyCurrentCanvas(enc);
893 if (use_blending_ll) {
894 enc->curr_canvas_copy_modified_ =
895 IncreaseTransparency(prev_canvas, ¶ms->rect_ll_, curr_canvas);
896 }
897 error_code = EncodeCandidate(¶ms->sub_frame_ll_, ¶ms->rect_ll_,
898 config_ll, use_blending_ll, candidate_ll);
899 if (error_code != VP8_ENC_OK) return error_code;
900 }
901 if (evaluate_lossy) {
902 CopyCurrentCanvas(enc);
903 if (use_blending_lossy) {
904 enc->curr_canvas_copy_modified_ =
905 FlattenSimilarBlocks(prev_canvas, ¶ms->rect_lossy_, curr_canvas,
906 config_lossy->quality);
907 }
908 error_code =
909 EncodeCandidate(¶ms->sub_frame_lossy_, ¶ms->rect_lossy_,
910 config_lossy, use_blending_lossy, candidate_lossy);
911 if (error_code != VP8_ENC_OK) return error_code;
912 enc->curr_canvas_copy_modified_ = 1;
913 }
914 return error_code;
915 }
916
917 #undef MIN_COLORS_LOSSY
918 #undef MAX_COLORS_LOSSLESS
919
GetEncodedData(const WebPMemoryWriter * const memory,WebPData * const encoded_data)920 static void GetEncodedData(const WebPMemoryWriter* const memory,
921 WebPData* const encoded_data) {
922 encoded_data->bytes = memory->mem;
923 encoded_data->size = memory->size;
924 }
925
926 // Sets dispose method of the previous frame to be 'dispose_method'.
SetPreviousDisposeMethod(WebPAnimEncoder * const enc,WebPMuxAnimDispose dispose_method)927 static void SetPreviousDisposeMethod(WebPAnimEncoder* const enc,
928 WebPMuxAnimDispose dispose_method) {
929 const size_t position = enc->count_ - 2;
930 EncodedFrame* const prev_enc_frame = GetFrame(enc, position);
931 assert(enc->count_ >= 2); // As current and previous frames are in enc.
932
933 if (enc->prev_candidate_undecided_) {
934 assert(dispose_method == WEBP_MUX_DISPOSE_NONE);
935 prev_enc_frame->sub_frame_.dispose_method = dispose_method;
936 prev_enc_frame->key_frame_.dispose_method = dispose_method;
937 } else {
938 WebPMuxFrameInfo* const prev_info = prev_enc_frame->is_key_frame_
939 ? &prev_enc_frame->key_frame_
940 : &prev_enc_frame->sub_frame_;
941 prev_info->dispose_method = dispose_method;
942 }
943 }
944
IncreasePreviousDuration(WebPAnimEncoder * const enc,int duration)945 static int IncreasePreviousDuration(WebPAnimEncoder* const enc, int duration) {
946 const size_t position = enc->count_ - 1;
947 EncodedFrame* const prev_enc_frame = GetFrame(enc, position);
948 int new_duration;
949
950 assert(enc->count_ >= 1);
951 assert(prev_enc_frame->sub_frame_.duration ==
952 prev_enc_frame->key_frame_.duration);
953 assert(prev_enc_frame->sub_frame_.duration ==
954 (prev_enc_frame->sub_frame_.duration & (MAX_DURATION - 1)));
955 assert(duration == (duration & (MAX_DURATION - 1)));
956
957 new_duration = prev_enc_frame->sub_frame_.duration + duration;
958 if (new_duration >= MAX_DURATION) { // Special case.
959 // Separate out previous frame from earlier merged frames to avoid overflow.
960 // We add a 1x1 transparent frame for the previous frame, with blending on.
961 const FrameRect rect = { 0, 0, 1, 1 };
962 const uint8_t lossless_1x1_bytes[] = {
963 0x52, 0x49, 0x46, 0x46, 0x14, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
964 0x56, 0x50, 0x38, 0x4c, 0x08, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00,
965 0x10, 0x88, 0x88, 0x08
966 };
967 const WebPData lossless_1x1 = {
968 lossless_1x1_bytes, sizeof(lossless_1x1_bytes)
969 };
970 const uint8_t lossy_1x1_bytes[] = {
971 0x52, 0x49, 0x46, 0x46, 0x40, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
972 0x56, 0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
973 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x4c, 0x50, 0x48, 0x02, 0x00,
974 0x00, 0x00, 0x00, 0x00, 0x56, 0x50, 0x38, 0x20, 0x18, 0x00, 0x00, 0x00,
975 0x30, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00,
976 0x34, 0x25, 0xa4, 0x00, 0x03, 0x70, 0x00, 0xfe, 0xfb, 0xfd, 0x50, 0x00
977 };
978 const WebPData lossy_1x1 = { lossy_1x1_bytes, sizeof(lossy_1x1_bytes) };
979 const int can_use_lossless =
980 (enc->last_config_.lossless || enc->options_.allow_mixed);
981 EncodedFrame* const curr_enc_frame = GetFrame(enc, enc->count_);
982 curr_enc_frame->is_key_frame_ = 0;
983 curr_enc_frame->sub_frame_.id = WEBP_CHUNK_ANMF;
984 curr_enc_frame->sub_frame_.x_offset = 0;
985 curr_enc_frame->sub_frame_.y_offset = 0;
986 curr_enc_frame->sub_frame_.dispose_method = WEBP_MUX_DISPOSE_NONE;
987 curr_enc_frame->sub_frame_.blend_method = WEBP_MUX_BLEND;
988 curr_enc_frame->sub_frame_.duration = duration;
989 if (!WebPDataCopy(can_use_lossless ? &lossless_1x1 : &lossy_1x1,
990 &curr_enc_frame->sub_frame_.bitstream)) {
991 return 0;
992 }
993 ++enc->count_;
994 ++enc->count_since_key_frame_;
995 enc->flush_count_ = enc->count_ - 1;
996 enc->prev_candidate_undecided_ = 0;
997 enc->prev_rect_ = rect;
998 } else { // Regular case.
999 // Increase duration of the previous frame by 'duration'.
1000 prev_enc_frame->sub_frame_.duration = new_duration;
1001 prev_enc_frame->key_frame_.duration = new_duration;
1002 }
1003 return 1;
1004 }
1005
1006 // Pick the candidate encoded frame with smallest size and release other
1007 // candidates.
1008 // TODO(later): Perhaps a rough SSIM/PSNR produced by the encoder should
1009 // also be a criteria, in addition to sizes.
PickBestCandidate(WebPAnimEncoder * const enc,Candidate * const candidates,int is_key_frame,EncodedFrame * const encoded_frame)1010 static void PickBestCandidate(WebPAnimEncoder* const enc,
1011 Candidate* const candidates, int is_key_frame,
1012 EncodedFrame* const encoded_frame) {
1013 int i;
1014 int best_idx = -1;
1015 size_t best_size = ~0;
1016 for (i = 0; i < CANDIDATE_COUNT; ++i) {
1017 if (candidates[i].evaluate_) {
1018 const size_t candidate_size = candidates[i].mem_.size;
1019 if (candidate_size < best_size) {
1020 best_idx = i;
1021 best_size = candidate_size;
1022 }
1023 }
1024 }
1025 assert(best_idx != -1);
1026 for (i = 0; i < CANDIDATE_COUNT; ++i) {
1027 if (candidates[i].evaluate_) {
1028 if (i == best_idx) {
1029 WebPMuxFrameInfo* const dst = is_key_frame
1030 ? &encoded_frame->key_frame_
1031 : &encoded_frame->sub_frame_;
1032 *dst = candidates[i].info_;
1033 GetEncodedData(&candidates[i].mem_, &dst->bitstream);
1034 if (!is_key_frame) {
1035 // Note: Previous dispose method only matters for non-keyframes.
1036 // Also, we don't want to modify previous dispose method that was
1037 // selected when a non key-frame was assumed.
1038 const WebPMuxAnimDispose prev_dispose_method =
1039 (best_idx == LL_DISP_NONE || best_idx == LOSSY_DISP_NONE)
1040 ? WEBP_MUX_DISPOSE_NONE
1041 : WEBP_MUX_DISPOSE_BACKGROUND;
1042 SetPreviousDisposeMethod(enc, prev_dispose_method);
1043 }
1044 enc->prev_rect_ = candidates[i].rect_; // save for next frame.
1045 } else {
1046 WebPMemoryWriterClear(&candidates[i].mem_);
1047 candidates[i].evaluate_ = 0;
1048 }
1049 }
1050 }
1051 }
1052
1053 // Depending on the configuration, tries different compressions
1054 // (lossy/lossless), dispose methods, blending methods etc to encode the current
1055 // frame and outputs the best one in 'encoded_frame'.
1056 // 'frame_skipped' will be set to true if this frame should actually be skipped.
SetFrame(WebPAnimEncoder * const enc,const WebPConfig * const config,int is_key_frame,EncodedFrame * const encoded_frame,int * const frame_skipped)1057 static WebPEncodingError SetFrame(WebPAnimEncoder* const enc,
1058 const WebPConfig* const config,
1059 int is_key_frame,
1060 EncodedFrame* const encoded_frame,
1061 int* const frame_skipped) {
1062 int i;
1063 WebPEncodingError error_code = VP8_ENC_OK;
1064 const WebPPicture* const curr_canvas = &enc->curr_canvas_copy_;
1065 const WebPPicture* const prev_canvas = &enc->prev_canvas_;
1066 Candidate candidates[CANDIDATE_COUNT];
1067 const int is_lossless = config->lossless;
1068 const int consider_lossless = is_lossless || enc->options_.allow_mixed;
1069 const int consider_lossy = !is_lossless || enc->options_.allow_mixed;
1070 const int is_first_frame = enc->is_first_frame_;
1071
1072 // First frame cannot be skipped as there is no 'previous frame' to merge it
1073 // to. So, empty rectangle is not allowed for the first frame.
1074 const int empty_rect_allowed_none = !is_first_frame;
1075
1076 // Even if there is exact pixel match between 'disposed previous canvas' and
1077 // 'current canvas', we can't skip current frame, as there may not be exact
1078 // pixel match between 'previous canvas' and 'current canvas'. So, we don't
1079 // allow empty rectangle in this case.
1080 const int empty_rect_allowed_bg = 0;
1081
1082 // If current frame is a key-frame, dispose method of previous frame doesn't
1083 // matter, so we don't try dispose to background.
1084 // Also, if key-frame insertion is on, and previous frame could be picked as
1085 // either a sub-frame or a key-frame, then we can't be sure about what frame
1086 // rectangle would be disposed. In that case too, we don't try dispose to
1087 // background.
1088 const int dispose_bg_possible =
1089 !is_key_frame && !enc->prev_candidate_undecided_;
1090
1091 SubFrameParams dispose_none_params;
1092 SubFrameParams dispose_bg_params;
1093
1094 WebPConfig config_ll = *config;
1095 WebPConfig config_lossy = *config;
1096 config_ll.lossless = 1;
1097 config_lossy.lossless = 0;
1098 enc->last_config_ = *config;
1099 enc->last_config_reversed_ = config->lossless ? config_lossy : config_ll;
1100 *frame_skipped = 0;
1101
1102 if (!SubFrameParamsInit(&dispose_none_params, 1, empty_rect_allowed_none) ||
1103 !SubFrameParamsInit(&dispose_bg_params, 0, empty_rect_allowed_bg)) {
1104 return VP8_ENC_ERROR_INVALID_CONFIGURATION;
1105 }
1106
1107 memset(candidates, 0, sizeof(candidates));
1108
1109 // Change-rectangle assuming previous frame was DISPOSE_NONE.
1110 if (!GetSubRects(prev_canvas, curr_canvas, is_key_frame, is_first_frame,
1111 config_lossy.quality, &dispose_none_params)) {
1112 error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;
1113 goto Err;
1114 }
1115
1116 if ((consider_lossless && IsEmptyRect(&dispose_none_params.rect_ll_)) ||
1117 (consider_lossy && IsEmptyRect(&dispose_none_params.rect_lossy_))) {
1118 // Don't encode the frame at all. Instead, the duration of the previous
1119 // frame will be increased later.
1120 assert(empty_rect_allowed_none);
1121 *frame_skipped = 1;
1122 goto End;
1123 }
1124
1125 if (dispose_bg_possible) {
1126 // Change-rectangle assuming previous frame was DISPOSE_BACKGROUND.
1127 WebPPicture* const prev_canvas_disposed = &enc->prev_canvas_disposed_;
1128 WebPCopyPixels(prev_canvas, prev_canvas_disposed);
1129 DisposeFrameRectangle(WEBP_MUX_DISPOSE_BACKGROUND, &enc->prev_rect_,
1130 prev_canvas_disposed);
1131
1132 if (!GetSubRects(prev_canvas_disposed, curr_canvas, is_key_frame,
1133 is_first_frame, config_lossy.quality,
1134 &dispose_bg_params)) {
1135 error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;
1136 goto Err;
1137 }
1138 assert(!IsEmptyRect(&dispose_bg_params.rect_ll_));
1139 assert(!IsEmptyRect(&dispose_bg_params.rect_lossy_));
1140
1141 if (enc->options_.minimize_size) { // Try both dispose methods.
1142 dispose_bg_params.should_try_ = 1;
1143 dispose_none_params.should_try_ = 1;
1144 } else if ((is_lossless &&
1145 RectArea(&dispose_bg_params.rect_ll_) <
1146 RectArea(&dispose_none_params.rect_ll_)) ||
1147 (!is_lossless &&
1148 RectArea(&dispose_bg_params.rect_lossy_) <
1149 RectArea(&dispose_none_params.rect_lossy_))) {
1150 dispose_bg_params.should_try_ = 1; // Pick DISPOSE_BACKGROUND.
1151 dispose_none_params.should_try_ = 0;
1152 }
1153 }
1154
1155 if (dispose_none_params.should_try_) {
1156 error_code = GenerateCandidates(
1157 enc, candidates, WEBP_MUX_DISPOSE_NONE, is_lossless, is_key_frame,
1158 &dispose_none_params, &config_ll, &config_lossy);
1159 if (error_code != VP8_ENC_OK) goto Err;
1160 }
1161
1162 if (dispose_bg_params.should_try_) {
1163 assert(!enc->is_first_frame_);
1164 assert(dispose_bg_possible);
1165 error_code = GenerateCandidates(
1166 enc, candidates, WEBP_MUX_DISPOSE_BACKGROUND, is_lossless, is_key_frame,
1167 &dispose_bg_params, &config_ll, &config_lossy);
1168 if (error_code != VP8_ENC_OK) goto Err;
1169 }
1170
1171 PickBestCandidate(enc, candidates, is_key_frame, encoded_frame);
1172
1173 goto End;
1174
1175 Err:
1176 for (i = 0; i < CANDIDATE_COUNT; ++i) {
1177 if (candidates[i].evaluate_) {
1178 WebPMemoryWriterClear(&candidates[i].mem_);
1179 }
1180 }
1181
1182 End:
1183 SubFrameParamsFree(&dispose_none_params);
1184 SubFrameParamsFree(&dispose_bg_params);
1185 return error_code;
1186 }
1187
1188 // Calculate the penalty incurred if we encode given frame as a key frame
1189 // instead of a sub-frame.
KeyFramePenalty(const EncodedFrame * const encoded_frame)1190 static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) {
1191 return ((int64_t)encoded_frame->key_frame_.bitstream.size -
1192 encoded_frame->sub_frame_.bitstream.size);
1193 }
1194
CacheFrame(WebPAnimEncoder * const enc,const WebPConfig * const config)1195 static int CacheFrame(WebPAnimEncoder* const enc,
1196 const WebPConfig* const config) {
1197 int ok = 0;
1198 int frame_skipped = 0;
1199 WebPEncodingError error_code = VP8_ENC_OK;
1200 const size_t position = enc->count_;
1201 EncodedFrame* const encoded_frame = GetFrame(enc, position);
1202
1203 ++enc->count_;
1204
1205 if (enc->is_first_frame_) { // Add this as a key-frame.
1206 error_code = SetFrame(enc, config, 1, encoded_frame, &frame_skipped);
1207 if (error_code != VP8_ENC_OK) goto End;
1208 assert(frame_skipped == 0); // First frame can't be skipped, even if empty.
1209 assert(position == 0 && enc->count_ == 1);
1210 encoded_frame->is_key_frame_ = 1;
1211 enc->flush_count_ = 0;
1212 enc->count_since_key_frame_ = 0;
1213 enc->prev_candidate_undecided_ = 0;
1214 } else {
1215 ++enc->count_since_key_frame_;
1216 if (enc->count_since_key_frame_ <= enc->options_.kmin) {
1217 // Add this as a frame rectangle.
1218 error_code = SetFrame(enc, config, 0, encoded_frame, &frame_skipped);
1219 if (error_code != VP8_ENC_OK) goto End;
1220 if (frame_skipped) goto Skip;
1221 encoded_frame->is_key_frame_ = 0;
1222 enc->flush_count_ = enc->count_ - 1;
1223 enc->prev_candidate_undecided_ = 0;
1224 } else {
1225 int64_t curr_delta;
1226 FrameRect prev_rect_key, prev_rect_sub;
1227
1228 // Add this as a frame rectangle to enc.
1229 error_code = SetFrame(enc, config, 0, encoded_frame, &frame_skipped);
1230 if (error_code != VP8_ENC_OK) goto End;
1231 if (frame_skipped) goto Skip;
1232 prev_rect_sub = enc->prev_rect_;
1233
1234
1235 // Add this as a key-frame to enc, too.
1236 error_code = SetFrame(enc, config, 1, encoded_frame, &frame_skipped);
1237 if (error_code != VP8_ENC_OK) goto End;
1238 assert(frame_skipped == 0); // Key-frame cannot be an empty rectangle.
1239 prev_rect_key = enc->prev_rect_;
1240
1241 // Analyze size difference of the two variants.
1242 curr_delta = KeyFramePenalty(encoded_frame);
1243 if (curr_delta <= enc->best_delta_) { // Pick this as the key-frame.
1244 if (enc->keyframe_ != KEYFRAME_NONE) {
1245 EncodedFrame* const old_keyframe = GetFrame(enc, enc->keyframe_);
1246 assert(old_keyframe->is_key_frame_);
1247 old_keyframe->is_key_frame_ = 0;
1248 }
1249 encoded_frame->is_key_frame_ = 1;
1250 enc->prev_candidate_undecided_ = 1;
1251 enc->keyframe_ = (int)position;
1252 enc->best_delta_ = curr_delta;
1253 enc->flush_count_ = enc->count_ - 1; // We can flush previous frames.
1254 } else {
1255 encoded_frame->is_key_frame_ = 0;
1256 enc->prev_candidate_undecided_ = 0;
1257 }
1258 // Note: We need '>=' below because when kmin and kmax are both zero,
1259 // count_since_key_frame will always be > kmax.
1260 if (enc->count_since_key_frame_ >= enc->options_.kmax) {
1261 enc->flush_count_ = enc->count_ - 1;
1262 enc->count_since_key_frame_ = 0;
1263 enc->keyframe_ = KEYFRAME_NONE;
1264 enc->best_delta_ = DELTA_INFINITY;
1265 }
1266 if (!enc->prev_candidate_undecided_) {
1267 enc->prev_rect_ =
1268 encoded_frame->is_key_frame_ ? prev_rect_key : prev_rect_sub;
1269 }
1270 }
1271 }
1272
1273 // Update previous to previous and previous canvases for next call.
1274 WebPCopyPixels(enc->curr_canvas_, &enc->prev_canvas_);
1275 enc->is_first_frame_ = 0;
1276
1277 Skip:
1278 ok = 1;
1279 ++enc->in_frame_count_;
1280
1281 End:
1282 if (!ok || frame_skipped) {
1283 FrameRelease(encoded_frame);
1284 // We reset some counters, as the frame addition failed/was skipped.
1285 --enc->count_;
1286 if (!enc->is_first_frame_) --enc->count_since_key_frame_;
1287 if (!ok) {
1288 MarkError2(enc, "ERROR adding frame. WebPEncodingError", error_code);
1289 }
1290 }
1291 enc->curr_canvas_->error_code = error_code; // report error_code
1292 assert(ok || error_code != VP8_ENC_OK);
1293 return ok;
1294 }
1295
FlushFrames(WebPAnimEncoder * const enc)1296 static int FlushFrames(WebPAnimEncoder* const enc) {
1297 while (enc->flush_count_ > 0) {
1298 WebPMuxError err;
1299 EncodedFrame* const curr = GetFrame(enc, 0);
1300 const WebPMuxFrameInfo* const info =
1301 curr->is_key_frame_ ? &curr->key_frame_ : &curr->sub_frame_;
1302 assert(enc->mux_ != NULL);
1303 err = WebPMuxPushFrame(enc->mux_, info, 1);
1304 if (err != WEBP_MUX_OK) {
1305 MarkError2(enc, "ERROR adding frame. WebPMuxError", err);
1306 return 0;
1307 }
1308 if (enc->options_.verbose) {
1309 fprintf(stderr, "INFO: Added frame. offset:%d,%d dispose:%d blend:%d\n",
1310 info->x_offset, info->y_offset, info->dispose_method,
1311 info->blend_method);
1312 }
1313 ++enc->out_frame_count_;
1314 FrameRelease(curr);
1315 ++enc->start_;
1316 --enc->flush_count_;
1317 --enc->count_;
1318 if (enc->keyframe_ != KEYFRAME_NONE) --enc->keyframe_;
1319 }
1320
1321 if (enc->count_ == 1 && enc->start_ != 0) {
1322 // Move enc->start to index 0.
1323 const int enc_start_tmp = (int)enc->start_;
1324 EncodedFrame temp = enc->encoded_frames_[0];
1325 enc->encoded_frames_[0] = enc->encoded_frames_[enc_start_tmp];
1326 enc->encoded_frames_[enc_start_tmp] = temp;
1327 FrameRelease(&enc->encoded_frames_[enc_start_tmp]);
1328 enc->start_ = 0;
1329 }
1330 return 1;
1331 }
1332
1333 #undef DELTA_INFINITY
1334 #undef KEYFRAME_NONE
1335
WebPAnimEncoderAdd(WebPAnimEncoder * enc,WebPPicture * frame,int timestamp,const WebPConfig * encoder_config)1336 int WebPAnimEncoderAdd(WebPAnimEncoder* enc, WebPPicture* frame, int timestamp,
1337 const WebPConfig* encoder_config) {
1338 WebPConfig config;
1339 int ok;
1340
1341 if (enc == NULL) {
1342 return 0;
1343 }
1344 MarkNoError(enc);
1345
1346 if (!enc->is_first_frame_) {
1347 // Make sure timestamps are non-decreasing (integer wrap-around is OK).
1348 const uint32_t prev_frame_duration =
1349 (uint32_t)timestamp - enc->prev_timestamp_;
1350 if (prev_frame_duration >= MAX_DURATION) {
1351 if (frame != NULL) {
1352 frame->error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;
1353 }
1354 MarkError(enc, "ERROR adding frame: timestamps must be non-decreasing");
1355 return 0;
1356 }
1357 if (!IncreasePreviousDuration(enc, (int)prev_frame_duration)) {
1358 return 0;
1359 }
1360 } else {
1361 enc->first_timestamp_ = timestamp;
1362 }
1363
1364 if (frame == NULL) { // Special: last call.
1365 enc->got_null_frame_ = 1;
1366 enc->prev_timestamp_ = timestamp;
1367 return 1;
1368 }
1369
1370 if (frame->width != enc->canvas_width_ ||
1371 frame->height != enc->canvas_height_) {
1372 frame->error_code = VP8_ENC_ERROR_INVALID_CONFIGURATION;
1373 MarkError(enc, "ERROR adding frame: Invalid frame dimensions");
1374 return 0;
1375 }
1376
1377 if (!frame->use_argb) { // Convert frame from YUV(A) to ARGB.
1378 if (enc->options_.verbose) {
1379 fprintf(stderr, "WARNING: Converting frame from YUV(A) to ARGB format; "
1380 "this incurs a small loss.\n");
1381 }
1382 if (!WebPPictureYUVAToARGB(frame)) {
1383 MarkError(enc, "ERROR converting frame from YUV(A) to ARGB");
1384 return 0;
1385 }
1386 }
1387
1388 if (encoder_config != NULL) {
1389 if (!WebPValidateConfig(encoder_config)) {
1390 MarkError(enc, "ERROR adding frame: Invalid WebPConfig");
1391 return 0;
1392 }
1393 config = *encoder_config;
1394 } else {
1395 WebPConfigInit(&config);
1396 config.lossless = 1;
1397 }
1398 assert(enc->curr_canvas_ == NULL);
1399 enc->curr_canvas_ = frame; // Store reference.
1400 assert(enc->curr_canvas_copy_modified_ == 1);
1401 CopyCurrentCanvas(enc);
1402
1403 ok = CacheFrame(enc, &config) && FlushFrames(enc);
1404
1405 enc->curr_canvas_ = NULL;
1406 enc->curr_canvas_copy_modified_ = 1;
1407 if (ok) {
1408 enc->prev_timestamp_ = timestamp;
1409 }
1410 return ok;
1411 }
1412
1413 // -----------------------------------------------------------------------------
1414 // Bitstream assembly.
1415
DecodeFrameOntoCanvas(const WebPMuxFrameInfo * const frame,WebPPicture * const canvas)1416 static int DecodeFrameOntoCanvas(const WebPMuxFrameInfo* const frame,
1417 WebPPicture* const canvas) {
1418 const WebPData* const image = &frame->bitstream;
1419 WebPPicture sub_image;
1420 WebPDecoderConfig config;
1421 WebPInitDecoderConfig(&config);
1422 WebPUtilClearPic(canvas, NULL);
1423 if (WebPGetFeatures(image->bytes, image->size, &config.input) !=
1424 VP8_STATUS_OK) {
1425 return 0;
1426 }
1427 if (!WebPPictureView(canvas, frame->x_offset, frame->y_offset,
1428 config.input.width, config.input.height, &sub_image)) {
1429 return 0;
1430 }
1431 config.output.is_external_memory = 1;
1432 config.output.colorspace = MODE_BGRA;
1433 config.output.u.RGBA.rgba = (uint8_t*)sub_image.argb;
1434 config.output.u.RGBA.stride = sub_image.argb_stride * 4;
1435 config.output.u.RGBA.size = config.output.u.RGBA.stride * sub_image.height;
1436
1437 if (WebPDecode(image->bytes, image->size, &config) != VP8_STATUS_OK) {
1438 return 0;
1439 }
1440 return 1;
1441 }
1442
FrameToFullCanvas(WebPAnimEncoder * const enc,const WebPMuxFrameInfo * const frame,WebPData * const full_image)1443 static int FrameToFullCanvas(WebPAnimEncoder* const enc,
1444 const WebPMuxFrameInfo* const frame,
1445 WebPData* const full_image) {
1446 WebPPicture* const canvas_buf = &enc->curr_canvas_copy_;
1447 WebPMemoryWriter mem1, mem2;
1448 WebPMemoryWriterInit(&mem1);
1449 WebPMemoryWriterInit(&mem2);
1450
1451 if (!DecodeFrameOntoCanvas(frame, canvas_buf)) goto Err;
1452 if (!EncodeFrame(&enc->last_config_, canvas_buf, &mem1)) goto Err;
1453 GetEncodedData(&mem1, full_image);
1454
1455 if (enc->options_.allow_mixed) {
1456 if (!EncodeFrame(&enc->last_config_reversed_, canvas_buf, &mem2)) goto Err;
1457 if (mem2.size < mem1.size) {
1458 GetEncodedData(&mem2, full_image);
1459 WebPMemoryWriterClear(&mem1);
1460 } else {
1461 WebPMemoryWriterClear(&mem2);
1462 }
1463 }
1464 return 1;
1465
1466 Err:
1467 WebPMemoryWriterClear(&mem1);
1468 WebPMemoryWriterClear(&mem2);
1469 return 0;
1470 }
1471
1472 // Convert a single-frame animation to a non-animated image if appropriate.
1473 // TODO(urvang): Can we pick one of the two heuristically (based on frame
1474 // rectangle and/or presence of alpha)?
OptimizeSingleFrame(WebPAnimEncoder * const enc,WebPData * const webp_data)1475 static WebPMuxError OptimizeSingleFrame(WebPAnimEncoder* const enc,
1476 WebPData* const webp_data) {
1477 WebPMuxError err = WEBP_MUX_OK;
1478 int canvas_width, canvas_height;
1479 WebPMuxFrameInfo frame;
1480 WebPData full_image;
1481 WebPData webp_data2;
1482 WebPMux* const mux = WebPMuxCreate(webp_data, 0);
1483 if (mux == NULL) return WEBP_MUX_BAD_DATA;
1484 assert(enc->out_frame_count_ == 1);
1485 WebPDataInit(&frame.bitstream);
1486 WebPDataInit(&full_image);
1487 WebPDataInit(&webp_data2);
1488
1489 err = WebPMuxGetFrame(mux, 1, &frame);
1490 if (err != WEBP_MUX_OK) goto End;
1491 if (frame.id != WEBP_CHUNK_ANMF) goto End; // Non-animation: nothing to do.
1492 err = WebPMuxGetCanvasSize(mux, &canvas_width, &canvas_height);
1493 if (err != WEBP_MUX_OK) goto End;
1494 if (!FrameToFullCanvas(enc, &frame, &full_image)) {
1495 err = WEBP_MUX_BAD_DATA;
1496 goto End;
1497 }
1498 err = WebPMuxSetImage(mux, &full_image, 1);
1499 if (err != WEBP_MUX_OK) goto End;
1500 err = WebPMuxAssemble(mux, &webp_data2);
1501 if (err != WEBP_MUX_OK) goto End;
1502
1503 if (webp_data2.size < webp_data->size) { // Pick 'webp_data2' if smaller.
1504 WebPDataClear(webp_data);
1505 *webp_data = webp_data2;
1506 WebPDataInit(&webp_data2);
1507 }
1508
1509 End:
1510 WebPDataClear(&frame.bitstream);
1511 WebPDataClear(&full_image);
1512 WebPMuxDelete(mux);
1513 WebPDataClear(&webp_data2);
1514 return err;
1515 }
1516
WebPAnimEncoderAssemble(WebPAnimEncoder * enc,WebPData * webp_data)1517 int WebPAnimEncoderAssemble(WebPAnimEncoder* enc, WebPData* webp_data) {
1518 WebPMux* mux;
1519 WebPMuxError err;
1520
1521 if (enc == NULL) {
1522 return 0;
1523 }
1524 MarkNoError(enc);
1525
1526 if (webp_data == NULL) {
1527 MarkError(enc, "ERROR assembling: NULL input");
1528 return 0;
1529 }
1530
1531 if (enc->in_frame_count_ == 0) {
1532 MarkError(enc, "ERROR: No frames to assemble");
1533 return 0;
1534 }
1535
1536 if (!enc->got_null_frame_ && enc->in_frame_count_ > 1 && enc->count_ > 0) {
1537 // set duration of the last frame to be avg of durations of previous frames.
1538 const double delta_time = enc->prev_timestamp_ - enc->first_timestamp_;
1539 const int average_duration = (int)(delta_time / (enc->in_frame_count_ - 1));
1540 if (!IncreasePreviousDuration(enc, average_duration)) {
1541 return 0;
1542 }
1543 }
1544
1545 // Flush any remaining frames.
1546 enc->flush_count_ = enc->count_;
1547 if (!FlushFrames(enc)) {
1548 return 0;
1549 }
1550
1551 // Set definitive canvas size.
1552 mux = enc->mux_;
1553 err = WebPMuxSetCanvasSize(mux, enc->canvas_width_, enc->canvas_height_);
1554 if (err != WEBP_MUX_OK) goto Err;
1555
1556 err = WebPMuxSetAnimationParams(mux, &enc->options_.anim_params);
1557 if (err != WEBP_MUX_OK) goto Err;
1558
1559 // Assemble into a WebP bitstream.
1560 err = WebPMuxAssemble(mux, webp_data);
1561 if (err != WEBP_MUX_OK) goto Err;
1562
1563 if (enc->out_frame_count_ == 1) {
1564 err = OptimizeSingleFrame(enc, webp_data);
1565 if (err != WEBP_MUX_OK) goto Err;
1566 }
1567 return 1;
1568
1569 Err:
1570 MarkError2(enc, "ERROR assembling WebP", err);
1571 return 0;
1572 }
1573
WebPAnimEncoderGetError(WebPAnimEncoder * enc)1574 const char* WebPAnimEncoderGetError(WebPAnimEncoder* enc) {
1575 if (enc == NULL) return NULL;
1576 return enc->error_str_;
1577 }
1578
1579 // -----------------------------------------------------------------------------
1580