2012-11-06 02:35:36 +01:00
|
|
|
// Copyright 2012 Google Inc. All Rights Reserved.
|
|
|
|
//
|
2013-06-07 08:05:58 +02:00
|
|
|
// 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.
|
2012-11-06 02:35:36 +01:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
//
|
|
|
|
// simple tool to convert animated GIFs to WebP
|
|
|
|
//
|
|
|
|
// Authors: Skal (pascal.massimino@gmail.com)
|
|
|
|
// Urvang (urvang@google.com)
|
|
|
|
|
|
|
|
#include <assert.h>
|
|
|
|
#include <stdio.h>
|
2015-01-07 01:34:21 +01:00
|
|
|
#include <stdlib.h>
|
2012-11-06 02:35:36 +01:00
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
2014-06-11 02:53:44 +02:00
|
|
|
#include "webp/config.h"
|
2012-11-06 02:35:36 +01:00
|
|
|
#endif
|
|
|
|
|
2013-09-12 13:03:51 +02:00
|
|
|
#ifdef WEBP_HAVE_GIF
|
|
|
|
|
2018-01-26 08:22:54 +01:00
|
|
|
#if defined(HAVE_UNISTD_H) && HAVE_UNISTD_H
|
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
#include <gif_lib.h>
|
|
|
|
#include "webp/encode.h"
|
|
|
|
#include "webp/mux.h"
|
2016-07-22 01:10:05 +02:00
|
|
|
#include "../examples/example_util.h"
|
2016-07-21 03:25:00 +02:00
|
|
|
#include "../imageio/imageio_util.h"
|
2015-01-14 00:43:31 +01:00
|
|
|
#include "./gifdec.h"
|
2015-01-07 01:34:21 +01:00
|
|
|
|
2018-01-26 08:22:54 +01:00
|
|
|
#if !defined(STDIN_FILENO)
|
|
|
|
#define STDIN_FILENO 0
|
|
|
|
#endif
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
2015-01-14 00:43:31 +01:00
|
|
|
static int transparent_index = GIF_INDEX_INVALID; // Opaque by default.
|
2013-01-08 20:52:24 +01:00
|
|
|
|
2014-07-21 15:44:43 +02:00
|
|
|
static const char* const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
|
2012-11-06 02:35:36 +01:00
|
|
|
"WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA",
|
|
|
|
"WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA"
|
|
|
|
};
|
|
|
|
|
|
|
|
static const char* ErrorString(WebPMuxError err) {
|
|
|
|
assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA);
|
|
|
|
return kErrorMessages[-err];
|
|
|
|
}
|
|
|
|
|
2013-12-10 23:53:06 +01:00
|
|
|
enum {
|
|
|
|
METADATA_ICC = (1 << 0),
|
|
|
|
METADATA_XMP = (1 << 1),
|
|
|
|
METADATA_ALL = METADATA_ICC | METADATA_XMP
|
|
|
|
};
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
static void Help(void) {
|
|
|
|
printf("Usage:\n");
|
|
|
|
printf(" gif2webp [options] gif_file -o webp_file\n");
|
2014-07-24 05:02:43 +02:00
|
|
|
printf("Options:\n");
|
2016-06-25 04:26:07 +02:00
|
|
|
printf(" -h / -help ............. this help\n");
|
2014-07-24 05:02:43 +02:00
|
|
|
printf(" -lossy ................. encode image using lossy compression\n");
|
|
|
|
printf(" -mixed ................. for each frame in the image, pick lossy\n"
|
|
|
|
" or lossless compression heuristically\n");
|
2012-11-06 02:35:36 +01:00
|
|
|
printf(" -q <float> ............. quality factor (0:small..100:big)\n");
|
|
|
|
printf(" -m <int> ............... compression method (0=fast, 6=slowest)\n");
|
2014-10-30 23:17:09 +01:00
|
|
|
printf(" -min_size .............. minimize output size (default:off)\n"
|
|
|
|
" lossless compression by default; can be\n"
|
|
|
|
" combined with -q, -m, -lossy or -mixed\n"
|
|
|
|
" options\n");
|
2014-07-24 05:02:43 +02:00
|
|
|
printf(" -kmin <int> ............ min distance between key frames\n");
|
|
|
|
printf(" -kmax <int> ............ max distance between key frames\n");
|
2012-11-06 02:35:36 +01:00
|
|
|
printf(" -f <int> ............... filter strength (0=off..100)\n");
|
2013-12-10 23:53:06 +01:00
|
|
|
printf(" -metadata <string> ..... comma separated list of metadata to\n");
|
|
|
|
printf(" ");
|
2014-07-24 05:02:43 +02:00
|
|
|
printf("copy from the input to the output if present\n");
|
2017-09-19 08:38:09 +02:00
|
|
|
printf(" ");
|
|
|
|
printf("Valid values: all, none, icc, xmp (default)\n");
|
|
|
|
printf(" -loop_compatibility .... use compatibility mode for Chrome\n");
|
|
|
|
printf(" version prior to M62 (inclusive)\n");
|
2013-12-18 02:01:59 +01:00
|
|
|
printf(" -mt .................... use multi-threading if available\n");
|
|
|
|
printf("\n");
|
2014-07-24 05:02:43 +02:00
|
|
|
printf(" -version ............... print version number and exit\n");
|
|
|
|
printf(" -v ..................... verbose\n");
|
|
|
|
printf(" -quiet ................. don't print anything\n");
|
2012-11-06 02:35:36 +01:00
|
|
|
printf("\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
int main(int argc, const char *argv[]) {
|
|
|
|
int verbose = 0;
|
2013-03-18 02:06:39 +01:00
|
|
|
int gif_error = GIF_ERROR;
|
2012-11-06 02:35:36 +01:00
|
|
|
WebPMuxError err = WEBP_MUX_OK;
|
|
|
|
int ok = 0;
|
|
|
|
const char *in_file = NULL, *out_file = NULL;
|
|
|
|
FILE* out = NULL;
|
|
|
|
GifFileType* gif = NULL;
|
2015-04-14 20:51:47 +02:00
|
|
|
int frame_duration = 0;
|
|
|
|
int frame_timestamp = 0;
|
2015-01-07 01:34:21 +01:00
|
|
|
GIFDisposeMethod orig_dispose = GIF_DISPOSE_NONE;
|
|
|
|
|
|
|
|
WebPPicture frame; // Frame rectangle only (not disposed).
|
|
|
|
WebPPicture curr_canvas; // Not disposed.
|
|
|
|
WebPPicture prev_canvas; // Disposed.
|
|
|
|
|
|
|
|
WebPAnimEncoder* enc = NULL;
|
|
|
|
WebPAnimEncoderOptions enc_options;
|
|
|
|
WebPConfig config;
|
2012-11-15 09:15:04 +01:00
|
|
|
|
2017-09-20 23:08:31 +02:00
|
|
|
int frame_number = 0; // Whether we are processing the first frame.
|
2012-11-06 02:35:36 +01:00
|
|
|
int done;
|
|
|
|
int c;
|
|
|
|
int quiet = 0;
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPData webp_data;
|
|
|
|
|
2013-12-10 23:53:06 +01:00
|
|
|
int keep_metadata = METADATA_XMP; // ICC not output by default.
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPData icc_data;
|
|
|
|
int stored_icc = 0; // Whether we have already stored an ICC profile.
|
|
|
|
WebPData xmp_data;
|
|
|
|
int stored_xmp = 0; // Whether we have already stored an XMP profile.
|
2017-09-19 08:38:09 +02:00
|
|
|
int loop_count = 0; // default: infinite
|
2015-01-07 01:34:21 +01:00
|
|
|
int stored_loop_count = 0; // Whether we have found an explicit loop count.
|
2017-09-19 08:38:09 +02:00
|
|
|
int loop_compatibility = 0;
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPMux* mux = NULL;
|
2013-09-30 23:25:58 +02:00
|
|
|
|
|
|
|
int default_kmin = 1; // Whether to use default kmin value.
|
|
|
|
int default_kmax = 1;
|
2013-08-22 20:14:51 +02:00
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
if (!WebPConfigInit(&config) || !WebPAnimEncoderOptionsInit(&enc_options) ||
|
|
|
|
!WebPPictureInit(&frame) || !WebPPictureInit(&curr_canvas) ||
|
2016-03-10 23:44:19 +01:00
|
|
|
!WebPPictureInit(&prev_canvas)) {
|
2012-11-06 02:35:36 +01:00
|
|
|
fprintf(stderr, "Error! Version mismatch!\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
config.lossless = 1; // Use lossless compression by default.
|
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPDataInit(&webp_data);
|
|
|
|
WebPDataInit(&icc_data);
|
|
|
|
WebPDataInit(&xmp_data);
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
if (argc == 1) {
|
|
|
|
Help();
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (c = 1; c < argc; ++c) {
|
2014-09-11 08:35:48 +02:00
|
|
|
int parse_error = 0;
|
2012-11-06 02:35:36 +01:00
|
|
|
if (!strcmp(argv[c], "-h") || !strcmp(argv[c], "-help")) {
|
|
|
|
Help();
|
|
|
|
return 0;
|
|
|
|
} else if (!strcmp(argv[c], "-o") && c < argc - 1) {
|
|
|
|
out_file = argv[++c];
|
|
|
|
} else if (!strcmp(argv[c], "-lossy")) {
|
|
|
|
config.lossless = 0;
|
2013-11-18 03:04:07 +01:00
|
|
|
} else if (!strcmp(argv[c], "-mixed")) {
|
2015-01-07 01:34:21 +01:00
|
|
|
enc_options.allow_mixed = 1;
|
2013-11-18 03:04:07 +01:00
|
|
|
config.lossless = 0;
|
2017-09-19 08:38:09 +02:00
|
|
|
} else if (!strcmp(argv[c], "-loop_compatibility")) {
|
|
|
|
loop_compatibility = 1;
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (!strcmp(argv[c], "-q") && c < argc - 1) {
|
2014-09-11 08:35:48 +02:00
|
|
|
config.quality = ExUtilGetFloat(argv[++c], &parse_error);
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (!strcmp(argv[c], "-m") && c < argc - 1) {
|
2014-09-11 08:35:48 +02:00
|
|
|
config.method = ExUtilGetInt(argv[++c], 0, &parse_error);
|
2014-10-30 23:17:09 +01:00
|
|
|
} else if (!strcmp(argv[c], "-min_size")) {
|
2015-01-07 01:34:21 +01:00
|
|
|
enc_options.minimize_size = 1;
|
2013-08-09 23:42:36 +02:00
|
|
|
} else if (!strcmp(argv[c], "-kmax") && c < argc - 1) {
|
2015-02-03 22:57:52 +01:00
|
|
|
enc_options.kmax = ExUtilGetInt(argv[++c], 0, &parse_error);
|
2013-09-30 23:25:58 +02:00
|
|
|
default_kmax = 0;
|
2013-09-12 22:41:09 +02:00
|
|
|
} else if (!strcmp(argv[c], "-kmin") && c < argc - 1) {
|
2015-02-03 22:57:52 +01:00
|
|
|
enc_options.kmin = ExUtilGetInt(argv[++c], 0, &parse_error);
|
2013-09-30 23:25:58 +02:00
|
|
|
default_kmin = 0;
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (!strcmp(argv[c], "-f") && c < argc - 1) {
|
2014-09-11 08:35:48 +02:00
|
|
|
config.filter_strength = ExUtilGetInt(argv[++c], 0, &parse_error);
|
2013-12-10 23:53:06 +01:00
|
|
|
} else if (!strcmp(argv[c], "-metadata") && c < argc - 1) {
|
|
|
|
static const struct {
|
|
|
|
const char* option;
|
|
|
|
int flag;
|
|
|
|
} kTokens[] = {
|
|
|
|
{ "all", METADATA_ALL },
|
|
|
|
{ "none", 0 },
|
|
|
|
{ "icc", METADATA_ICC },
|
|
|
|
{ "xmp", METADATA_XMP },
|
|
|
|
};
|
|
|
|
const size_t kNumTokens = sizeof(kTokens) / sizeof(*kTokens);
|
|
|
|
const char* start = argv[++c];
|
|
|
|
const char* const end = start + strlen(start);
|
|
|
|
|
|
|
|
keep_metadata = 0;
|
|
|
|
while (start < end) {
|
|
|
|
size_t i;
|
|
|
|
const char* token = strchr(start, ',');
|
|
|
|
if (token == NULL) token = end;
|
|
|
|
|
|
|
|
for (i = 0; i < kNumTokens; ++i) {
|
|
|
|
if ((size_t)(token - start) == strlen(kTokens[i].option) &&
|
|
|
|
!strncmp(start, kTokens[i].option, strlen(kTokens[i].option))) {
|
|
|
|
if (kTokens[i].flag != 0) {
|
|
|
|
keep_metadata |= kTokens[i].flag;
|
|
|
|
} else {
|
|
|
|
keep_metadata = 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (i == kNumTokens) {
|
|
|
|
fprintf(stderr, "Error! Unknown metadata type '%.*s'\n",
|
|
|
|
(int)(token - start), start);
|
|
|
|
Help();
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
start = token + 1;
|
|
|
|
}
|
2013-12-18 02:01:59 +01:00
|
|
|
} else if (!strcmp(argv[c], "-mt")) {
|
|
|
|
++config.thread_level;
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (!strcmp(argv[c], "-version")) {
|
2013-02-26 23:22:06 +01:00
|
|
|
const int enc_version = WebPGetEncoderVersion();
|
|
|
|
const int mux_version = WebPGetMuxVersion();
|
|
|
|
printf("WebP Encoder version: %d.%d.%d\nWebP Mux version: %d.%d.%d\n",
|
|
|
|
(enc_version >> 16) & 0xff, (enc_version >> 8) & 0xff,
|
|
|
|
enc_version & 0xff, (mux_version >> 16) & 0xff,
|
|
|
|
(mux_version >> 8) & 0xff, mux_version & 0xff);
|
2012-11-06 02:35:36 +01:00
|
|
|
return 0;
|
|
|
|
} else if (!strcmp(argv[c], "-quiet")) {
|
|
|
|
quiet = 1;
|
2016-02-02 02:00:51 +01:00
|
|
|
enc_options.verbose = 0;
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (!strcmp(argv[c], "-v")) {
|
|
|
|
verbose = 1;
|
2015-01-07 01:34:21 +01:00
|
|
|
enc_options.verbose = 1;
|
2013-12-13 05:20:08 +01:00
|
|
|
} else if (!strcmp(argv[c], "--")) {
|
|
|
|
if (c < argc - 1) in_file = argv[++c];
|
|
|
|
break;
|
2012-11-06 02:35:36 +01:00
|
|
|
} else if (argv[c][0] == '-') {
|
|
|
|
fprintf(stderr, "Error! Unknown option '%s'\n", argv[c]);
|
|
|
|
Help();
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
in_file = argv[c];
|
|
|
|
}
|
2014-09-11 08:35:48 +02:00
|
|
|
|
|
|
|
if (parse_error) {
|
|
|
|
Help();
|
|
|
|
return -1;
|
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
2013-09-12 22:41:09 +02:00
|
|
|
|
2013-09-30 23:25:58 +02:00
|
|
|
// Appropriate default kmin, kmax values for lossy and lossless.
|
|
|
|
if (default_kmin) {
|
2015-01-07 01:34:21 +01:00
|
|
|
enc_options.kmin = config.lossless ? 9 : 3;
|
2013-09-30 23:25:58 +02:00
|
|
|
}
|
|
|
|
if (default_kmax) {
|
2015-01-07 01:34:21 +01:00
|
|
|
enc_options.kmax = config.lossless ? 17 : 5;
|
2013-09-30 23:25:58 +02:00
|
|
|
}
|
2013-09-12 22:41:09 +02:00
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
if (!WebPValidateConfig(&config)) {
|
|
|
|
fprintf(stderr, "Error! Invalid configuration.\n");
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_file == NULL) {
|
|
|
|
fprintf(stderr, "No input file specified!\n");
|
|
|
|
Help();
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the decoder object
|
2014-07-23 02:41:55 +02:00
|
|
|
#if LOCAL_GIF_PREREQ(5,0)
|
2018-01-26 08:22:54 +01:00
|
|
|
gif = !strcmp(in_file, "-") ? DGifOpenFileHandle(STDIN_FILENO, &gif_error)
|
|
|
|
: DGifOpenFileName(in_file, &gif_error);
|
2013-01-08 20:52:24 +01:00
|
|
|
#else
|
2018-01-26 08:22:54 +01:00
|
|
|
gif = !strcmp(in_file, "-") ? DGifOpenFileHandle(STDIN_FILENO)
|
|
|
|
: DGifOpenFileName(in_file);
|
2013-01-08 20:52:24 +01:00
|
|
|
#endif
|
2012-11-06 02:35:36 +01:00
|
|
|
if (gif == NULL) goto End;
|
|
|
|
|
|
|
|
// Loop over GIF images
|
|
|
|
done = 0;
|
|
|
|
do {
|
|
|
|
GifRecordType type;
|
|
|
|
if (DGifGetRecordType(gif, &type) == GIF_ERROR) goto End;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case IMAGE_DESC_RECORD_TYPE: {
|
2015-01-07 01:34:21 +01:00
|
|
|
GIFFrameRect gif_rect;
|
2014-06-24 17:34:45 +02:00
|
|
|
GifImageDesc* const image_desc = &gif->Image;
|
2013-05-22 23:49:24 +02:00
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
if (!DGifGetImageDesc(gif)) goto End;
|
2014-06-24 17:34:45 +02:00
|
|
|
|
2017-09-20 23:08:31 +02:00
|
|
|
if (frame_number == 0) {
|
2014-06-24 17:34:45 +02:00
|
|
|
if (verbose) {
|
|
|
|
printf("Canvas screen: %d x %d\n", gif->SWidth, gif->SHeight);
|
|
|
|
}
|
2015-01-07 01:34:21 +01:00
|
|
|
// Fix some broken GIF global headers that report
|
|
|
|
// 0 x 0 screen dimension.
|
2014-06-24 17:34:45 +02:00
|
|
|
if (gif->SWidth == 0 || gif->SHeight == 0) {
|
|
|
|
image_desc->Left = 0;
|
|
|
|
image_desc->Top = 0;
|
|
|
|
gif->SWidth = image_desc->Width;
|
|
|
|
gif->SHeight = image_desc->Height;
|
|
|
|
if (gif->SWidth <= 0 || gif->SHeight <= 0) {
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
if (verbose) {
|
|
|
|
printf("Fixed canvas screen dimension to: %d x %d\n",
|
|
|
|
gif->SWidth, gif->SHeight);
|
|
|
|
}
|
|
|
|
}
|
2014-06-30 07:00:49 +02:00
|
|
|
// Allocate current buffer.
|
2014-06-24 17:34:45 +02:00
|
|
|
frame.width = gif->SWidth;
|
|
|
|
frame.height = gif->SHeight;
|
|
|
|
frame.use_argb = 1;
|
|
|
|
if (!WebPPictureAlloc(&frame)) goto End;
|
2015-01-14 00:43:31 +01:00
|
|
|
GIFClearPic(&frame, NULL);
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPPictureCopy(&frame, &curr_canvas);
|
|
|
|
WebPPictureCopy(&frame, &prev_canvas);
|
2014-09-19 00:07:28 +02:00
|
|
|
|
|
|
|
// Background color.
|
2015-01-14 00:43:31 +01:00
|
|
|
GIFGetBackgroundColor(gif->SColorMap, gif->SBackGroundColor,
|
|
|
|
transparent_index,
|
|
|
|
&enc_options.anim_params.bgcolor);
|
2015-01-07 01:34:21 +01:00
|
|
|
|
|
|
|
// Initialize encoder.
|
|
|
|
enc = WebPAnimEncoderNew(curr_canvas.width, curr_canvas.height,
|
|
|
|
&enc_options);
|
2015-11-12 00:48:47 +01:00
|
|
|
if (enc == NULL) {
|
|
|
|
fprintf(stderr,
|
|
|
|
"Error! Could not create encoder object. Possibly due to "
|
|
|
|
"a memory error.\n");
|
|
|
|
goto End;
|
|
|
|
}
|
2014-06-24 17:34:45 +02:00
|
|
|
}
|
2015-01-07 01:34:21 +01:00
|
|
|
|
2014-06-24 17:34:45 +02:00
|
|
|
// Some even more broken GIF can have sub-rect with zero width/height.
|
|
|
|
if (image_desc->Width == 0 || image_desc->Height == 0) {
|
|
|
|
image_desc->Width = gif->SWidth;
|
|
|
|
image_desc->Height = gif->SHeight;
|
|
|
|
}
|
|
|
|
|
2015-01-14 00:43:31 +01:00
|
|
|
if (!GIFReadFrame(gif, transparent_index, &gif_rect, &frame)) {
|
2013-08-09 23:42:36 +02:00
|
|
|
goto End;
|
|
|
|
}
|
2015-01-07 01:34:21 +01:00
|
|
|
// Blend frame rectangle with previous canvas to compose full canvas.
|
|
|
|
// Note that 'curr_canvas' is same as 'prev_canvas' at this point.
|
2015-01-14 00:43:31 +01:00
|
|
|
GIFBlendFrames(&frame, &gif_rect, &curr_canvas);
|
2012-11-06 02:35:36 +01:00
|
|
|
|
2015-04-14 20:51:47 +02:00
|
|
|
if (!WebPAnimEncoderAdd(enc, &curr_canvas, frame_timestamp, &config)) {
|
2017-09-20 23:08:31 +02:00
|
|
|
fprintf(stderr, "Error while adding frame #%d: %s\n", frame_number,
|
|
|
|
WebPAnimEncoderGetError(enc));
|
|
|
|
goto End;
|
|
|
|
} else {
|
|
|
|
++frame_number;
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
// Update canvases.
|
2015-01-14 00:43:31 +01:00
|
|
|
GIFDisposeFrame(orig_dispose, &gif_rect, &prev_canvas, &curr_canvas);
|
|
|
|
GIFCopyPixels(&curr_canvas, &prev_canvas);
|
2014-09-11 23:35:21 +02:00
|
|
|
|
2018-03-24 06:48:17 +01:00
|
|
|
// Force frames with a small or no duration to 100ms to be consistent
|
|
|
|
// with web browsers and other transcoding tools. This also avoids
|
|
|
|
// incorrect durations between frames when padding frames are
|
|
|
|
// discarded.
|
|
|
|
if (frame_duration <= 10) {
|
|
|
|
frame_duration = 100;
|
|
|
|
}
|
|
|
|
|
2015-04-14 20:51:47 +02:00
|
|
|
// Update timestamp (for next frame).
|
|
|
|
frame_timestamp += frame_duration;
|
|
|
|
|
2014-09-11 23:35:21 +02:00
|
|
|
// In GIF, graphic control extensions are optional for a frame, so we
|
|
|
|
// may not get one before reading the next frame. To handle this case,
|
|
|
|
// we reset frame properties to reasonable defaults for the next frame.
|
2015-01-07 01:34:21 +01:00
|
|
|
orig_dispose = GIF_DISPOSE_NONE;
|
2015-04-14 20:51:47 +02:00
|
|
|
frame_duration = 0;
|
2015-01-14 00:43:31 +01:00
|
|
|
transparent_index = GIF_INDEX_INVALID;
|
2012-11-06 02:35:36 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case EXTENSION_RECORD_TYPE: {
|
|
|
|
int extension;
|
|
|
|
GifByteType *data = NULL;
|
|
|
|
if (DGifGetExtension(gif, &extension, &data) == GIF_ERROR) {
|
|
|
|
goto End;
|
|
|
|
}
|
2016-10-07 20:21:18 +02:00
|
|
|
if (data == NULL) continue;
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
switch (extension) {
|
|
|
|
case COMMENT_EXT_FUNC_CODE: {
|
|
|
|
break; // Do nothing for now.
|
|
|
|
}
|
|
|
|
case GRAPHICS_EXT_FUNC_CODE: {
|
2015-04-14 20:51:47 +02:00
|
|
|
if (!GIFReadGraphicsExtension(data, &frame_duration, &orig_dispose,
|
2015-01-14 00:43:31 +01:00
|
|
|
&transparent_index)) {
|
|
|
|
goto End;
|
2012-11-15 09:15:04 +01:00
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case PLAINTEXT_EXT_FUNC_CODE: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case APPLICATION_EXT_FUNC_CODE: {
|
|
|
|
if (data[0] != 11) break; // Chunk is too short
|
2014-06-23 23:26:36 +02:00
|
|
|
if (!memcmp(data + 1, "NETSCAPE2.0", 11) ||
|
|
|
|
!memcmp(data + 1, "ANIMEXTS1.0", 11)) {
|
2015-01-14 00:43:31 +01:00
|
|
|
if (!GIFReadLoopCount(gif, &data, &loop_count)) {
|
|
|
|
goto End;
|
|
|
|
}
|
2014-06-24 17:34:45 +02:00
|
|
|
if (verbose) {
|
2015-01-07 01:34:21 +01:00
|
|
|
fprintf(stderr, "Loop count: %d\n", loop_count);
|
2014-06-24 17:34:45 +02:00
|
|
|
}
|
2017-09-19 08:38:09 +02:00
|
|
|
stored_loop_count = loop_compatibility ? (loop_count != 0) : 1;
|
2013-04-18 23:19:25 +02:00
|
|
|
} else { // An extension containing metadata.
|
2013-12-10 23:53:06 +01:00
|
|
|
// We only store the first encountered chunk of each type, and
|
|
|
|
// only if requested by the user.
|
|
|
|
const int is_xmp = (keep_metadata & METADATA_XMP) &&
|
|
|
|
!stored_xmp &&
|
|
|
|
!memcmp(data + 1, "XMP DataXMP", 11);
|
|
|
|
const int is_icc = (keep_metadata & METADATA_ICC) &&
|
|
|
|
!stored_icc &&
|
|
|
|
!memcmp(data + 1, "ICCRGBG1012", 11);
|
2013-04-18 23:19:25 +02:00
|
|
|
if (is_xmp || is_icc) {
|
2015-01-14 00:43:31 +01:00
|
|
|
if (!GIFReadMetadata(gif, &data,
|
|
|
|
is_xmp ? &xmp_data : &icc_data)) {
|
|
|
|
goto End;
|
2013-04-18 23:19:25 +02:00
|
|
|
}
|
|
|
|
if (is_icc) {
|
|
|
|
stored_icc = 1;
|
|
|
|
} else if (is_xmp) {
|
|
|
|
stored_xmp = 1;
|
|
|
|
}
|
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
break; // skip
|
|
|
|
}
|
|
|
|
}
|
2013-04-18 23:19:25 +02:00
|
|
|
while (data != NULL) {
|
2012-11-06 02:35:36 +01:00
|
|
|
if (DGifGetExtensionNext(gif, &data) == GIF_ERROR) goto End;
|
2013-04-18 23:19:25 +02:00
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case TERMINATE_RECORD_TYPE: {
|
|
|
|
done = 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
if (verbose) {
|
|
|
|
fprintf(stderr, "Skipping over unknown record type %d\n", type);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} while (!done);
|
|
|
|
|
2015-04-14 20:51:47 +02:00
|
|
|
// Last NULL frame.
|
|
|
|
if (!WebPAnimEncoderAdd(enc, NULL, frame_timestamp, NULL)) {
|
|
|
|
fprintf(stderr, "Error flushing WebP muxer.\n");
|
2015-11-12 00:48:47 +01:00
|
|
|
fprintf(stderr, "%s\n", WebPAnimEncoderGetError(enc));
|
2015-04-14 20:51:47 +02:00
|
|
|
}
|
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
if (!WebPAnimEncoderAssemble(enc, &webp_data)) {
|
2015-11-12 00:48:47 +01:00
|
|
|
fprintf(stderr, "%s\n", WebPAnimEncoderGetError(enc));
|
2013-09-12 22:41:09 +02:00
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
|
2017-09-19 08:38:09 +02:00
|
|
|
if (!loop_compatibility) {
|
|
|
|
if (!stored_loop_count) {
|
|
|
|
// if no loop-count element is seen, the default is '1' (loop-once)
|
2017-09-20 23:08:31 +02:00
|
|
|
// and we need to signal it explicitly in WebP. Note however that
|
|
|
|
// in case there's a single frame, we still don't need to store it.
|
|
|
|
if (frame_number > 1) {
|
|
|
|
stored_loop_count = 1;
|
|
|
|
loop_count = 1;
|
|
|
|
}
|
2018-05-10 02:36:13 +02:00
|
|
|
} else if (loop_count > 0 && loop_count < 65535) {
|
2017-09-19 08:38:09 +02:00
|
|
|
// adapt GIF's semantic to WebP's (except in the infinite-loop case)
|
|
|
|
loop_count += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// loop_count of 0 is the default (infinite), so no need to signal it
|
|
|
|
if (loop_count == 0) stored_loop_count = 0;
|
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
if (stored_loop_count || stored_icc || stored_xmp) {
|
|
|
|
// Re-mux to add loop count and/or metadata as needed.
|
|
|
|
mux = WebPMuxCreate(&webp_data, 1);
|
|
|
|
if (mux == NULL) {
|
|
|
|
fprintf(stderr, "ERROR: Could not re-mux to add loop count/metadata.\n");
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
WebPDataClear(&webp_data);
|
|
|
|
|
|
|
|
if (stored_loop_count) { // Update loop count.
|
|
|
|
WebPMuxAnimParams new_params;
|
|
|
|
err = WebPMuxGetAnimationParams(mux, &new_params);
|
|
|
|
if (err != WEBP_MUX_OK) {
|
|
|
|
fprintf(stderr, "ERROR (%s): Could not fetch loop count.\n",
|
|
|
|
ErrorString(err));
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
new_params.loop_count = loop_count;
|
|
|
|
err = WebPMuxSetAnimationParams(mux, &new_params);
|
|
|
|
if (err != WEBP_MUX_OK) {
|
|
|
|
fprintf(stderr, "ERROR (%s): Could not update loop count.\n",
|
|
|
|
ErrorString(err));
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
|
2015-01-07 01:34:21 +01:00
|
|
|
if (stored_icc) { // Add ICCP chunk.
|
|
|
|
err = WebPMuxSetChunk(mux, "ICCP", &icc_data, 1);
|
|
|
|
if (verbose) {
|
|
|
|
fprintf(stderr, "ICC size: %d\n", (int)icc_data.size);
|
|
|
|
}
|
|
|
|
if (err != WEBP_MUX_OK) {
|
|
|
|
fprintf(stderr, "ERROR (%s): Could not set ICC chunk.\n",
|
|
|
|
ErrorString(err));
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stored_xmp) { // Add XMP chunk.
|
|
|
|
err = WebPMuxSetChunk(mux, "XMP ", &xmp_data, 1);
|
|
|
|
if (verbose) {
|
|
|
|
fprintf(stderr, "XMP size: %d\n", (int)xmp_data.size);
|
|
|
|
}
|
|
|
|
if (err != WEBP_MUX_OK) {
|
|
|
|
fprintf(stderr, "ERROR (%s): Could not set XMP chunk.\n",
|
|
|
|
ErrorString(err));
|
|
|
|
goto End;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = WebPMuxAssemble(mux, &webp_data);
|
|
|
|
if (err != WEBP_MUX_OK) {
|
|
|
|
fprintf(stderr, "ERROR (%s): Could not assemble when re-muxing to add "
|
|
|
|
"loop count/metadata.\n", ErrorString(err));
|
|
|
|
goto End;
|
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
2015-01-07 01:34:21 +01:00
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
if (out_file != NULL) {
|
2016-07-22 00:36:29 +02:00
|
|
|
if (!ImgIoUtilWriteFile(out_file, webp_data.bytes, webp_data.size)) {
|
2012-11-06 02:35:36 +01:00
|
|
|
fprintf(stderr, "Error writing output file: %s\n", out_file);
|
|
|
|
goto End;
|
|
|
|
}
|
2013-03-18 02:08:01 +01:00
|
|
|
if (!quiet) {
|
2018-01-26 08:22:54 +01:00
|
|
|
if (!strcmp(out_file, "-")) {
|
|
|
|
fprintf(stderr, "Saved %d bytes to STDIO\n",
|
|
|
|
(int)webp_data.size);
|
|
|
|
} else {
|
|
|
|
fprintf(stderr, "Saved output file (%d bytes): %s\n",
|
|
|
|
(int)webp_data.size, out_file);
|
|
|
|
}
|
2013-03-18 02:08:01 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!quiet) {
|
2015-08-25 02:36:57 +02:00
|
|
|
fprintf(stderr, "Nothing written; use -o flag to save the result "
|
|
|
|
"(%d bytes).\n", (int)webp_data.size);
|
2013-03-18 02:08:01 +01:00
|
|
|
}
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// All OK.
|
|
|
|
ok = 1;
|
2013-03-18 02:06:39 +01:00
|
|
|
gif_error = GIF_OK;
|
2012-11-06 02:35:36 +01:00
|
|
|
|
|
|
|
End:
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPDataClear(&icc_data);
|
|
|
|
WebPDataClear(&xmp_data);
|
2012-11-06 02:35:36 +01:00
|
|
|
WebPMuxDelete(mux);
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPDataClear(&webp_data);
|
2013-10-14 23:39:46 +02:00
|
|
|
WebPPictureFree(&frame);
|
2015-01-07 01:34:21 +01:00
|
|
|
WebPPictureFree(&curr_canvas);
|
|
|
|
WebPPictureFree(&prev_canvas);
|
|
|
|
WebPAnimEncoderDelete(enc);
|
2012-11-06 02:35:36 +01:00
|
|
|
if (out != NULL && out_file != NULL) fclose(out);
|
|
|
|
|
2013-03-18 02:06:39 +01:00
|
|
|
if (gif_error != GIF_OK) {
|
2015-01-14 00:43:31 +01:00
|
|
|
GIFDisplayError(gif, gif_error);
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
|
|
|
if (gif != NULL) {
|
2014-07-23 02:48:24 +02:00
|
|
|
#if LOCAL_GIF_PREREQ(5,1)
|
|
|
|
DGifCloseFile(gif, &gif_error);
|
|
|
|
#else
|
2012-11-06 02:35:36 +01:00
|
|
|
DGifCloseFile(gif);
|
2014-07-23 02:48:24 +02:00
|
|
|
#endif
|
2012-11-06 02:35:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return !ok;
|
|
|
|
}
|
|
|
|
|
2013-09-12 13:03:51 +02:00
|
|
|
#else // !WEBP_HAVE_GIF
|
|
|
|
|
|
|
|
int main(int argc, const char *argv[]) {
|
|
|
|
fprintf(stderr, "GIF support not enabled in %s.\n", argv[0]);
|
|
|
|
(void)argc;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
2012-11-06 02:35:36 +01:00
|
|
|
//------------------------------------------------------------------------------
|