xref: /aosp_15_r20/external/webp/examples/gif2webp.c (revision b2055c353e87c8814eb2b6b1b11112a1562253bd)
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