gif2webp.c (21538B)
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 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 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. 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 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 //------------------------------------------------------------------------------