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