1 // Copyright 2012 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 // simple tool to convert animated GIFs to WebP
11 //
12 // Authors: Skal ([email protected])
13 // Urvang ([email protected])
14
15 #include <assert.h>
16 #include <stdio.h>
17 #include <stdlib.h>
18 #include <string.h>
19
20 #ifdef HAVE_CONFIG_H
21 #include "webp/config.h"
22 #endif
23
24 #ifdef WEBP_HAVE_GIF
25
26 #if defined(HAVE_UNISTD_H) && HAVE_UNISTD_H
27 #include <unistd.h>
28 #endif
29
30 #include <gif_lib.h>
31 #include "webp/encode.h"
32 #include "webp/mux.h"
33 #include "../examples/example_util.h"
34 #include "../imageio/imageio_util.h"
35 #include "./gifdec.h"
36 #include "./unicode.h"
37 #include "./unicode_gif.h"
38
39 #if !defined(STDIN_FILENO)
40 #define STDIN_FILENO 0
41 #endif
42
43 //------------------------------------------------------------------------------
44
45 static int transparent_index = GIF_INDEX_INVALID; // Opaque by default.
46
47 static const char* const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
48 "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA",
49 "WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA"
50 };
51
ErrorString(WebPMuxError err)52 static const char* ErrorString(WebPMuxError err) {
53 assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA);
54 return kErrorMessages[-err];
55 }
56
57 enum {
58 METADATA_ICC = (1 << 0),
59 METADATA_XMP = (1 << 1),
60 METADATA_ALL = METADATA_ICC | METADATA_XMP
61 };
62
63 //------------------------------------------------------------------------------
64
Help(void)65 static void Help(void) {
66 printf("Usage:\n");
67 printf(" gif2webp [options] gif_file -o webp_file\n");
68 printf("Options:\n");
69 printf(" -h / -help ............. this help\n");
70 printf(" -lossy ................. encode image using lossy compression\n");
71 printf(" -mixed ................. for each frame in the image, pick lossy\n"
72 " or lossless compression heuristically\n");
73 printf(" -q <float> ............. quality factor (0:small..100:big)\n");
74 printf(" -m <int> ............... compression method (0=fast, 6=slowest)\n");
75 printf(" -min_size .............. minimize output size (default:off)\n"
76 " lossless compression by default; can be\n"
77 " combined with -q, -m, -lossy or -mixed\n"
78 " options\n");
79 printf(" -kmin <int> ............ min distance between key frames\n");
80 printf(" -kmax <int> ............ max distance between key frames\n");
81 printf(" -f <int> ............... filter strength (0=off..100)\n");
82 printf(" -metadata <string> ..... comma separated list of metadata to\n");
83 printf(" ");
84 printf("copy from the input to the output if present\n");
85 printf(" ");
86 printf("Valid values: all, none, icc, xmp (default)\n");
87 printf(" -loop_compatibility .... use compatibility mode for Chrome\n");
88 printf(" version prior to M62 (inclusive)\n");
89 printf(" -mt .................... use multi-threading if available\n");
90 printf("\n");
91 printf(" -version ............... print version number and exit\n");
92 printf(" -v ..................... verbose\n");
93 printf(" -quiet ................. don't print anything\n");
94 printf("\n");
95 }
96
97 //------------------------------------------------------------------------------
98
main(int argc,const char * argv[])99 int main(int argc, const char* argv[]) {
100 int verbose = 0;
101 int gif_error = GIF_ERROR;
102 WebPMuxError err = WEBP_MUX_OK;
103 int ok = 0;
104 const W_CHAR* in_file = NULL, *out_file = NULL;
105 GifFileType* gif = NULL;
106 int frame_duration = 0;
107 int frame_timestamp = 0;
108 GIFDisposeMethod orig_dispose = GIF_DISPOSE_NONE;
109
110 WebPPicture frame; // Frame rectangle only (not disposed).
111 WebPPicture curr_canvas; // Not disposed.
112 WebPPicture prev_canvas; // Disposed.
113
114 WebPAnimEncoder* enc = NULL;
115 WebPAnimEncoderOptions enc_options;
116 WebPConfig config;
117
118 int frame_number = 0; // Whether we are processing the first frame.
119 int done;
120 int c;
121 int quiet = 0;
122 WebPData webp_data;
123
124 int keep_metadata = METADATA_XMP; // ICC not output by default.
125 WebPData icc_data;
126 int stored_icc = 0; // Whether we have already stored an ICC profile.
127 WebPData xmp_data;
128 int stored_xmp = 0; // Whether we have already stored an XMP profile.
129 int loop_count = 0; // default: infinite
130 int stored_loop_count = 0; // Whether we have found an explicit loop count.
131 int loop_compatibility = 0;
132 WebPMux* mux = NULL;
133
134 int default_kmin = 1; // Whether to use default kmin value.
135 int default_kmax = 1;
136
137 INIT_WARGV(argc, argv);
138
139 if (!WebPConfigInit(&config) || !WebPAnimEncoderOptionsInit(&enc_options) ||
140 !WebPPictureInit(&frame) || !WebPPictureInit(&curr_canvas) ||
141 !WebPPictureInit(&prev_canvas)) {
142 fprintf(stderr, "Error! Version mismatch!\n");
143 FREE_WARGV_AND_RETURN(-1);
144 }
145 config.lossless = 1; // Use lossless compression by default.
146
147 WebPDataInit(&webp_data);
148 WebPDataInit(&icc_data);
149 WebPDataInit(&xmp_data);
150
151 if (argc == 1) {
152 Help();
153 FREE_WARGV_AND_RETURN(0);
154 }
155
156 for (c = 1; c < argc; ++c) {
157 int parse_error = 0;
158 if (!strcmp(argv[c], "-h") || !strcmp(argv[c], "-help")) {
159 Help();
160 FREE_WARGV_AND_RETURN(0);
161 } else if (!strcmp(argv[c], "-o") && c < argc - 1) {
162 out_file = GET_WARGV(argv, ++c);
163 } else if (!strcmp(argv[c], "-lossy")) {
164 config.lossless = 0;
165 } else if (!strcmp(argv[c], "-mixed")) {
166 enc_options.allow_mixed = 1;
167 config.lossless = 0;
168 } else if (!strcmp(argv[c], "-loop_compatibility")) {
169 loop_compatibility = 1;
170 } else if (!strcmp(argv[c], "-q") && c < argc - 1) {
171 config.quality = ExUtilGetFloat(argv[++c], &parse_error);
172 } else if (!strcmp(argv[c], "-m") && c < argc - 1) {
173 config.method = ExUtilGetInt(argv[++c], 0, &parse_error);
174 } else if (!strcmp(argv[c], "-min_size")) {
175 enc_options.minimize_size = 1;
176 } else if (!strcmp(argv[c], "-kmax") && c < argc - 1) {
177 enc_options.kmax = ExUtilGetInt(argv[++c], 0, &parse_error);
178 default_kmax = 0;
179 } else if (!strcmp(argv[c], "-kmin") && c < argc - 1) {
180 enc_options.kmin = ExUtilGetInt(argv[++c], 0, &parse_error);
181 default_kmin = 0;
182 } else if (!strcmp(argv[c], "-f") && c < argc - 1) {
183 config.filter_strength = ExUtilGetInt(argv[++c], 0, &parse_error);
184 } else if (!strcmp(argv[c], "-metadata") && c < argc - 1) {
185 static const struct {
186 const char* option;
187 int flag;
188 } kTokens[] = {
189 { "all", METADATA_ALL },
190 { "none", 0 },
191 { "icc", METADATA_ICC },
192 { "xmp", METADATA_XMP },
193 };
194 const size_t kNumTokens = sizeof(kTokens) / sizeof(*kTokens);
195 const char* start = argv[++c];
196 const char* const end = start + strlen(start);
197
198 keep_metadata = 0;
199 while (start < end) {
200 size_t i;
201 const char* token = strchr(start, ',');
202 if (token == NULL) token = end;
203
204 for (i = 0; i < kNumTokens; ++i) {
205 if ((size_t)(token - start) == strlen(kTokens[i].option) &&
206 !strncmp(start, kTokens[i].option, strlen(kTokens[i].option))) {
207 if (kTokens[i].flag != 0) {
208 keep_metadata |= kTokens[i].flag;
209 } else {
210 keep_metadata = 0;
211 }
212 break;
213 }
214 }
215 if (i == kNumTokens) {
216 fprintf(stderr, "Error! Unknown metadata type '%.*s'\n",
217 (int)(token - start), start);
218 Help();
219 FREE_WARGV_AND_RETURN(-1);
220 }
221 start = token + 1;
222 }
223 } else if (!strcmp(argv[c], "-mt")) {
224 ++config.thread_level;
225 } else if (!strcmp(argv[c], "-version")) {
226 const int enc_version = WebPGetEncoderVersion();
227 const int mux_version = WebPGetMuxVersion();
228 printf("WebP Encoder version: %d.%d.%d\nWebP Mux version: %d.%d.%d\n",
229 (enc_version >> 16) & 0xff, (enc_version >> 8) & 0xff,
230 enc_version & 0xff, (mux_version >> 16) & 0xff,
231 (mux_version >> 8) & 0xff, mux_version & 0xff);
232 FREE_WARGV_AND_RETURN(0);
233 } else if (!strcmp(argv[c], "-quiet")) {
234 quiet = 1;
235 enc_options.verbose = 0;
236 } else if (!strcmp(argv[c], "-v")) {
237 verbose = 1;
238 enc_options.verbose = 1;
239 } else if (!strcmp(argv[c], "--")) {
240 if (c < argc - 1) in_file = GET_WARGV(argv, ++c);
241 break;
242 } else if (argv[c][0] == '-') {
243 fprintf(stderr, "Error! Unknown option '%s'\n", argv[c]);
244 Help();
245 FREE_WARGV_AND_RETURN(-1);
246 } else {
247 in_file = GET_WARGV(argv, c);
248 }
249
250 if (parse_error) {
251 Help();
252 FREE_WARGV_AND_RETURN(-1);
253 }
254 }
255
256 // Appropriate default kmin, kmax values for lossy and lossless.
257 if (default_kmin) {
258 enc_options.kmin = config.lossless ? 9 : 3;
259 }
260 if (default_kmax) {
261 enc_options.kmax = config.lossless ? 17 : 5;
262 }
263
264 if (!WebPValidateConfig(&config)) {
265 fprintf(stderr, "Error! Invalid configuration.\n");
266 goto End;
267 }
268
269 if (in_file == NULL) {
270 fprintf(stderr, "No input file specified!\n");
271 Help();
272 goto End;
273 }
274
275 // Start the decoder object
276 gif = DGifOpenFileUnicode(in_file, &gif_error);
277 if (gif == NULL) goto End;
278
279 // Loop over GIF images
280 done = 0;
281 do {
282 GifRecordType type;
283 if (DGifGetRecordType(gif, &type) == GIF_ERROR) goto End;
284
285 switch (type) {
286 case IMAGE_DESC_RECORD_TYPE: {
287 GIFFrameRect gif_rect;
288 GifImageDesc* const image_desc = &gif->Image;
289
290 if (!DGifGetImageDesc(gif)) goto End;
291
292 if (frame_number == 0) {
293 if (verbose) {
294 printf("Canvas screen: %d x %d\n", gif->SWidth, gif->SHeight);
295 }
296 // Fix some broken GIF global headers that report
297 // 0 x 0 screen dimension.
298 if (gif->SWidth == 0 || gif->SHeight == 0) {
299 image_desc->Left = 0;
300 image_desc->Top = 0;
301 gif->SWidth = image_desc->Width;
302 gif->SHeight = image_desc->Height;
303 if (gif->SWidth <= 0 || gif->SHeight <= 0) {
304 goto End;
305 }
306 if (verbose) {
307 printf("Fixed canvas screen dimension to: %d x %d\n",
308 gif->SWidth, gif->SHeight);
309 }
310 }
311 // Allocate current buffer.
312 frame.width = gif->SWidth;
313 frame.height = gif->SHeight;
314 frame.use_argb = 1;
315 if (!WebPPictureAlloc(&frame)) goto End;
316 GIFClearPic(&frame, NULL);
317 if (!(WebPPictureCopy(&frame, &curr_canvas) &&
318 WebPPictureCopy(&frame, &prev_canvas))) {
319 fprintf(stderr, "Error allocating canvas.\n");
320 goto End;
321 }
322
323 // Background color.
324 GIFGetBackgroundColor(gif->SColorMap, gif->SBackGroundColor,
325 transparent_index,
326 &enc_options.anim_params.bgcolor);
327
328 // Initialize encoder.
329 enc = WebPAnimEncoderNew(curr_canvas.width, curr_canvas.height,
330 &enc_options);
331 if (enc == NULL) {
332 fprintf(stderr,
333 "Error! Could not create encoder object. Possibly due to "
334 "a memory error.\n");
335 goto End;
336 }
337 }
338
339 // Some even more broken GIF can have sub-rect with zero width/height.
340 if (image_desc->Width == 0 || image_desc->Height == 0) {
341 image_desc->Width = gif->SWidth;
342 image_desc->Height = gif->SHeight;
343 }
344
345 if (!GIFReadFrame(gif, transparent_index, &gif_rect, &frame)) {
346 goto End;
347 }
348 // Blend frame rectangle with previous canvas to compose full canvas.
349 // Note that 'curr_canvas' is same as 'prev_canvas' at this point.
350 GIFBlendFrames(&frame, &gif_rect, &curr_canvas);
351
352 if (!WebPAnimEncoderAdd(enc, &curr_canvas, frame_timestamp, &config)) {
353 fprintf(stderr, "Error while adding frame #%d: %s\n", frame_number,
354 WebPAnimEncoderGetError(enc));
355 goto End;
356 } else {
357 ++frame_number;
358 }
359
360 // Update canvases.
361 GIFDisposeFrame(orig_dispose, &gif_rect, &prev_canvas, &curr_canvas);
362 GIFCopyPixels(&curr_canvas, &prev_canvas);
363
364 // Force frames with a small or no duration to 100ms to be consistent
365 // with web browsers and other transcoding tools. This also avoids
366 // incorrect durations between frames when padding frames are
367 // discarded.
368 if (frame_duration <= 10) {
369 frame_duration = 100;
370 }
371
372 // Update timestamp (for next frame).
373 frame_timestamp += frame_duration;
374
375 // In GIF, graphic control extensions are optional for a frame, so we
376 // may not get one before reading the next frame. To handle this case,
377 // we reset frame properties to reasonable defaults for the next frame.
378 orig_dispose = GIF_DISPOSE_NONE;
379 frame_duration = 0;
380 transparent_index = GIF_INDEX_INVALID;
381 break;
382 }
383 case EXTENSION_RECORD_TYPE: {
384 int extension;
385 GifByteType* data = NULL;
386 if (DGifGetExtension(gif, &extension, &data) == GIF_ERROR) {
387 goto End;
388 }
389 if (data == NULL) continue;
390
391 switch (extension) {
392 case COMMENT_EXT_FUNC_CODE: {
393 break; // Do nothing for now.
394 }
395 case GRAPHICS_EXT_FUNC_CODE: {
396 if (!GIFReadGraphicsExtension(data, &frame_duration, &orig_dispose,
397 &transparent_index)) {
398 goto End;
399 }
400 break;
401 }
402 case PLAINTEXT_EXT_FUNC_CODE: {
403 break;
404 }
405 case APPLICATION_EXT_FUNC_CODE: {
406 if (data[0] != 11) break; // Chunk is too short
407 if (!memcmp(data + 1, "NETSCAPE2.0", 11) ||
408 !memcmp(data + 1, "ANIMEXTS1.0", 11)) {
409 if (!GIFReadLoopCount(gif, &data, &loop_count)) {
410 goto End;
411 }
412 if (verbose) {
413 fprintf(stderr, "Loop count: %d\n", loop_count);
414 }
415 stored_loop_count = loop_compatibility ? (loop_count != 0) : 1;
416 } else { // An extension containing metadata.
417 // We only store the first encountered chunk of each type, and
418 // only if requested by the user.
419 const int is_xmp = (keep_metadata & METADATA_XMP) &&
420 !stored_xmp &&
421 !memcmp(data + 1, "XMP DataXMP", 11);
422 const int is_icc = (keep_metadata & METADATA_ICC) &&
423 !stored_icc &&
424 !memcmp(data + 1, "ICCRGBG1012", 11);
425 if (is_xmp || is_icc) {
426 if (!GIFReadMetadata(gif, &data,
427 is_xmp ? &xmp_data : &icc_data)) {
428 goto End;
429 }
430 if (is_icc) {
431 stored_icc = 1;
432 } else if (is_xmp) {
433 stored_xmp = 1;
434 }
435 }
436 }
437 break;
438 }
439 default: {
440 break; // skip
441 }
442 }
443 while (data != NULL) {
444 if (DGifGetExtensionNext(gif, &data) == GIF_ERROR) goto End;
445 }
446 break;
447 }
448 case TERMINATE_RECORD_TYPE: {
449 done = 1;
450 break;
451 }
452 default: {
453 if (verbose) {
454 fprintf(stderr, "Skipping over unknown record type %d\n", type);
455 }
456 break;
457 }
458 }
459 } while (!done);
460
461 // Last NULL frame.
462 if (!WebPAnimEncoderAdd(enc, NULL, frame_timestamp, NULL)) {
463 fprintf(stderr, "Error flushing WebP muxer.\n");
464 fprintf(stderr, "%s\n", WebPAnimEncoderGetError(enc));
465 }
466
467 if (!WebPAnimEncoderAssemble(enc, &webp_data)) {
468 fprintf(stderr, "%s\n", WebPAnimEncoderGetError(enc));
469 goto End;
470 }
471 // If there's only one frame, we don't need to handle loop count.
472 if (frame_number == 1) {
473 loop_count = 0;
474 } else if (!loop_compatibility) {
475 if (!stored_loop_count) {
476 // if no loop-count element is seen, the default is '1' (loop-once)
477 // and we need to signal it explicitly in WebP. Note however that
478 // in case there's a single frame, we still don't need to store it.
479 if (frame_number > 1) {
480 stored_loop_count = 1;
481 loop_count = 1;
482 }
483 } else if (loop_count > 0 && loop_count < 65535) {
484 // adapt GIF's semantic to WebP's (except in the infinite-loop case)
485 loop_count += 1;
486 }
487 }
488 // loop_count of 0 is the default (infinite), so no need to signal it
489 if (loop_count == 0) stored_loop_count = 0;
490
491 if (stored_loop_count || stored_icc || stored_xmp) {
492 // Re-mux to add loop count and/or metadata as needed.
493 mux = WebPMuxCreate(&webp_data, 1);
494 if (mux == NULL) {
495 fprintf(stderr, "ERROR: Could not re-mux to add loop count/metadata.\n");
496 goto End;
497 }
498 WebPDataClear(&webp_data);
499
500 if (stored_loop_count) { // Update loop count.
501 WebPMuxAnimParams new_params;
502 err = WebPMuxGetAnimationParams(mux, &new_params);
503 if (err != WEBP_MUX_OK) {
504 fprintf(stderr, "ERROR (%s): Could not fetch loop count.\n",
505 ErrorString(err));
506 goto End;
507 }
508 new_params.loop_count = loop_count;
509 err = WebPMuxSetAnimationParams(mux, &new_params);
510 if (err != WEBP_MUX_OK) {
511 fprintf(stderr, "ERROR (%s): Could not update loop count.\n",
512 ErrorString(err));
513 goto End;
514 }
515 }
516
517 if (stored_icc) { // Add ICCP chunk.
518 err = WebPMuxSetChunk(mux, "ICCP", &icc_data, 1);
519 if (verbose) {
520 fprintf(stderr, "ICC size: %d\n", (int)icc_data.size);
521 }
522 if (err != WEBP_MUX_OK) {
523 fprintf(stderr, "ERROR (%s): Could not set ICC chunk.\n",
524 ErrorString(err));
525 goto End;
526 }
527 }
528
529 if (stored_xmp) { // Add XMP chunk.
530 err = WebPMuxSetChunk(mux, "XMP ", &xmp_data, 1);
531 if (verbose) {
532 fprintf(stderr, "XMP size: %d\n", (int)xmp_data.size);
533 }
534 if (err != WEBP_MUX_OK) {
535 fprintf(stderr, "ERROR (%s): Could not set XMP chunk.\n",
536 ErrorString(err));
537 goto End;
538 }
539 }
540
541 err = WebPMuxAssemble(mux, &webp_data);
542 if (err != WEBP_MUX_OK) {
543 fprintf(stderr, "ERROR (%s): Could not assemble when re-muxing to add "
544 "loop count/metadata.\n", ErrorString(err));
545 goto End;
546 }
547 }
548
549 if (out_file != NULL) {
550 if (!ImgIoUtilWriteFile((const char*)out_file, webp_data.bytes,
551 webp_data.size)) {
552 WFPRINTF(stderr, "Error writing output file: %s\n", out_file);
553 goto End;
554 }
555 if (!quiet) {
556 if (!WSTRCMP(out_file, "-")) {
557 fprintf(stderr, "Saved %d bytes to STDIO\n",
558 (int)webp_data.size);
559 } else {
560 WFPRINTF(stderr, "Saved output file (%d bytes): %s\n",
561 (int)webp_data.size, out_file);
562 }
563 }
564 } else {
565 if (!quiet) {
566 fprintf(stderr, "Nothing written; use -o flag to save the result "
567 "(%d bytes).\n", (int)webp_data.size);
568 }
569 }
570
571 // All OK.
572 ok = 1;
573 gif_error = GIF_OK;
574
575 End:
576 WebPDataClear(&icc_data);
577 WebPDataClear(&xmp_data);
578 WebPMuxDelete(mux);
579 WebPDataClear(&webp_data);
580 WebPPictureFree(&frame);
581 WebPPictureFree(&curr_canvas);
582 WebPPictureFree(&prev_canvas);
583 WebPAnimEncoderDelete(enc);
584
585 if (gif_error != GIF_OK) {
586 GIFDisplayError(gif, gif_error);
587 }
588 if (gif != NULL) {
589 #if LOCAL_GIF_PREREQ(5,1)
590 DGifCloseFile(gif, &gif_error);
591 #else
592 DGifCloseFile(gif);
593 #endif
594 }
595
596 FREE_WARGV_AND_RETURN(!ok);
597 }
598
599 #else // !WEBP_HAVE_GIF
600
main(int argc,const char * argv[])601 int main(int argc, const char* argv[]) {
602 fprintf(stderr, "GIF support not enabled in %s.\n", argv[0]);
603 (void)argc;
604 return 0;
605 }
606
607 #endif
608
609 //------------------------------------------------------------------------------
610