1 // Copyright 2011 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 command-line to create a WebP container file and to extract or strip
11 // relevant data from the container file.
12 //
13 // Authors: Vikas (vikaas.arora@gmail.com),
14 // Urvang (urvang@google.com)
15
16 /* Usage examples:
17
18 Create container WebP file:
19 webpmux -frame anim_1.webp +100+10+10 \
20 -frame anim_2.webp +100+25+25+1 \
21 -frame anim_3.webp +100+50+50+1 \
22 -frame anim_4.webp +100 \
23 -loop 10 -bgcolor 128,255,255,255 \
24 -o out_animation_container.webp
25
26 webpmux -set icc image_profile.icc in.webp -o out_icc_container.webp
27 webpmux -set exif image_metadata.exif in.webp -o out_exif_container.webp
28 webpmux -set xmp image_metadata.xmp in.webp -o out_xmp_container.webp
29 webpmux -set loop 1 in.webp -o out_looped.webp
30
31 Extract relevant data from WebP container file:
32 webpmux -get frame n in.webp -o out_frame.webp
33 webpmux -get icc in.webp -o image_profile.icc
34 webpmux -get exif in.webp -o image_metadata.exif
35 webpmux -get xmp in.webp -o image_metadata.xmp
36
37 Strip data from WebP Container file:
38 webpmux -strip icc in.webp -o out.webp
39 webpmux -strip exif in.webp -o out.webp
40 webpmux -strip xmp in.webp -o out.webp
41
42 Change duration of frame intervals:
43 webpmux -duration 150 in.webp -o out.webp
44 webpmux -duration 33,2 in.webp -o out.webp
45 webpmux -duration 200,10,0 -duration 150,6,50 in.webp -o out.webp
46
47 Misc:
48 webpmux -info in.webp
49 webpmux [ -h | -help ]
50 webpmux -version
51 webpmux argument_file_name
52 */
53
54 #ifdef HAVE_CONFIG_H
55 #include "webp/config.h"
56 #endif
57
58 #include <assert.h>
59 #include <stdio.h>
60 #include <stdlib.h>
61 #include <string.h>
62 #include "webp/decode.h"
63 #include "webp/mux.h"
64 #include "../examples/example_util.h"
65 #include "../imageio/imageio_util.h"
66 #include "./unicode.h"
67
68 //------------------------------------------------------------------------------
69 // Config object to parse command-line arguments.
70
71 typedef enum {
72 NIL_ACTION = 0,
73 ACTION_GET,
74 ACTION_SET,
75 ACTION_STRIP,
76 ACTION_INFO,
77 ACTION_HELP,
78 ACTION_DURATION
79 } ActionType;
80
81 typedef enum {
82 NIL_SUBTYPE = 0,
83 SUBTYPE_ANMF,
84 SUBTYPE_LOOP,
85 SUBTYPE_BGCOLOR
86 } FeatureSubType;
87
88 typedef struct {
89 FeatureSubType subtype_;
90 const char* filename_;
91 const char* params_;
92 } FeatureArg;
93
94 typedef enum {
95 NIL_FEATURE = 0,
96 FEATURE_EXIF,
97 FEATURE_XMP,
98 FEATURE_ICCP,
99 FEATURE_ANMF,
100 FEATURE_DURATION,
101 FEATURE_LOOP,
102 LAST_FEATURE
103 } FeatureType;
104
105 static const char* const kFourccList[LAST_FEATURE] = {
106 NULL, "EXIF", "XMP ", "ICCP", "ANMF"
107 };
108
109 static const char* const kDescriptions[LAST_FEATURE] = {
110 NULL, "EXIF metadata", "XMP metadata", "ICC profile",
111 "Animation frame"
112 };
113
114 typedef struct {
115 CommandLineArguments cmd_args_;
116
117 ActionType action_type_;
118 const char* input_;
119 const char* output_;
120 FeatureType type_;
121 FeatureArg* args_;
122 int arg_count_;
123 } Config;
124
125 //------------------------------------------------------------------------------
126 // Helper functions.
127
CountOccurrences(const CommandLineArguments * const args,const char * const arg)128 static int CountOccurrences(const CommandLineArguments* const args,
129 const char* const arg) {
130 int i;
131 int num_occurences = 0;
132
133 for (i = 0; i < args->argc_; ++i) {
134 if (!strcmp(args->argv_[i], arg)) {
135 ++num_occurences;
136 }
137 }
138 return num_occurences;
139 }
140
141 static const char* const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
142 "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_BAD_DATA",
143 "WEBP_MUX_MEMORY_ERROR", "WEBP_MUX_NOT_ENOUGH_DATA"
144 };
145
ErrorString(WebPMuxError err)146 static const char* ErrorString(WebPMuxError err) {
147 assert(err <= WEBP_MUX_NOT_FOUND && err >= WEBP_MUX_NOT_ENOUGH_DATA);
148 return kErrorMessages[-err];
149 }
150
151 #define RETURN_IF_ERROR(ERR_MSG) \
152 if (err != WEBP_MUX_OK) { \
153 fprintf(stderr, ERR_MSG); \
154 return err; \
155 }
156
157 #define RETURN_IF_ERROR3(ERR_MSG, FORMAT_STR1, FORMAT_STR2) \
158 if (err != WEBP_MUX_OK) { \
159 fprintf(stderr, ERR_MSG, FORMAT_STR1, FORMAT_STR2); \
160 return err; \
161 }
162
163 #define ERROR_GOTO1(ERR_MSG, LABEL) \
164 do { \
165 fprintf(stderr, ERR_MSG); \
166 ok = 0; \
167 goto LABEL; \
168 } while (0)
169
170 #define ERROR_GOTO2(ERR_MSG, FORMAT_STR, LABEL) \
171 do { \
172 fprintf(stderr, ERR_MSG, FORMAT_STR); \
173 ok = 0; \
174 goto LABEL; \
175 } while (0)
176
177 #define ERROR_GOTO3(ERR_MSG, FORMAT_STR1, FORMAT_STR2, LABEL) \
178 do { \
179 fprintf(stderr, ERR_MSG, FORMAT_STR1, FORMAT_STR2); \
180 ok = 0; \
181 goto LABEL; \
182 } while (0)
183
DisplayInfo(const WebPMux * mux)184 static WebPMuxError DisplayInfo(const WebPMux* mux) {
185 int width, height;
186 uint32_t flag;
187
188 WebPMuxError err = WebPMuxGetCanvasSize(mux, &width, &height);
189 assert(err == WEBP_MUX_OK); // As WebPMuxCreate() was successful earlier.
190 printf("Canvas size: %d x %d\n", width, height);
191
192 err = WebPMuxGetFeatures(mux, &flag);
193 RETURN_IF_ERROR("Failed to retrieve features\n");
194
195 if (flag == 0) {
196 printf("No features present.\n");
197 return err;
198 }
199
200 // Print the features present.
201 printf("Features present:");
202 if (flag & ANIMATION_FLAG) printf(" animation");
203 if (flag & ICCP_FLAG) printf(" ICC profile");
204 if (flag & EXIF_FLAG) printf(" EXIF metadata");
205 if (flag & XMP_FLAG) printf(" XMP metadata");
206 if (flag & ALPHA_FLAG) printf(" transparency");
207 printf("\n");
208
209 if (flag & ANIMATION_FLAG) {
210 const WebPChunkId id = WEBP_CHUNK_ANMF;
211 const char* const type_str = "frame";
212 int nFrames;
213
214 WebPMuxAnimParams params;
215 err = WebPMuxGetAnimationParams(mux, ¶ms);
216 assert(err == WEBP_MUX_OK);
217 printf("Background color : 0x%.8X Loop Count : %d\n",
218 params.bgcolor, params.loop_count);
219
220 err = WebPMuxNumChunks(mux, id, &nFrames);
221 assert(err == WEBP_MUX_OK);
222
223 printf("Number of %ss: %d\n", type_str, nFrames);
224 if (nFrames > 0) {
225 int i;
226 printf("No.: width height alpha x_offset y_offset ");
227 printf("duration dispose blend ");
228 printf("image_size compression\n");
229 for (i = 1; i <= nFrames; i++) {
230 WebPMuxFrameInfo frame;
231 err = WebPMuxGetFrame(mux, i, &frame);
232 if (err == WEBP_MUX_OK) {
233 WebPBitstreamFeatures features;
234 const VP8StatusCode status = WebPGetFeatures(
235 frame.bitstream.bytes, frame.bitstream.size, &features);
236 assert(status == VP8_STATUS_OK); // Checked by WebPMuxCreate().
237 (void)status;
238 printf("%3d: %5d %5d %5s %8d %8d ", i, features.width,
239 features.height, features.has_alpha ? "yes" : "no",
240 frame.x_offset, frame.y_offset);
241 {
242 const char* const dispose =
243 (frame.dispose_method == WEBP_MUX_DISPOSE_NONE) ? "none"
244 : "background";
245 const char* const blend =
246 (frame.blend_method == WEBP_MUX_BLEND) ? "yes" : "no";
247 printf("%8d %10s %5s ", frame.duration, dispose, blend);
248 }
249 printf("%10d %11s\n", (int)frame.bitstream.size,
250 (features.format == 1) ? "lossy" :
251 (features.format == 2) ? "lossless" :
252 "undefined");
253 }
254 WebPDataClear(&frame.bitstream);
255 RETURN_IF_ERROR3("Failed to retrieve %s#%d\n", type_str, i);
256 }
257 }
258 }
259
260 if (flag & ICCP_FLAG) {
261 WebPData icc_profile;
262 err = WebPMuxGetChunk(mux, "ICCP", &icc_profile);
263 assert(err == WEBP_MUX_OK);
264 printf("Size of the ICC profile data: %d\n", (int)icc_profile.size);
265 }
266
267 if (flag & EXIF_FLAG) {
268 WebPData exif;
269 err = WebPMuxGetChunk(mux, "EXIF", &exif);
270 assert(err == WEBP_MUX_OK);
271 printf("Size of the EXIF metadata: %d\n", (int)exif.size);
272 }
273
274 if (flag & XMP_FLAG) {
275 WebPData xmp;
276 err = WebPMuxGetChunk(mux, "XMP ", &xmp);
277 assert(err == WEBP_MUX_OK);
278 printf("Size of the XMP metadata: %d\n", (int)xmp.size);
279 }
280
281 if ((flag & ALPHA_FLAG) && !(flag & ANIMATION_FLAG)) {
282 WebPMuxFrameInfo image;
283 err = WebPMuxGetFrame(mux, 1, &image);
284 if (err == WEBP_MUX_OK) {
285 printf("Size of the image (with alpha): %d\n", (int)image.bitstream.size);
286 }
287 WebPDataClear(&image.bitstream);
288 RETURN_IF_ERROR("Failed to retrieve the image\n");
289 }
290
291 return WEBP_MUX_OK;
292 }
293
PrintHelp(void)294 static void PrintHelp(void) {
295 printf("Usage: webpmux -get GET_OPTIONS INPUT -o OUTPUT\n");
296 printf(" webpmux -set SET_OPTIONS INPUT -o OUTPUT\n");
297 printf(" webpmux -duration DURATION_OPTIONS [-duration ...]\n");
298 printf(" INPUT -o OUTPUT\n");
299 printf(" webpmux -strip STRIP_OPTIONS INPUT -o OUTPUT\n");
300 printf(" webpmux -frame FRAME_OPTIONS [-frame...] [-loop LOOP_COUNT]"
301 "\n");
302 printf(" [-bgcolor BACKGROUND_COLOR] -o OUTPUT\n");
303 printf(" webpmux -info INPUT\n");
304 printf(" webpmux [-h|-help]\n");
305 printf(" webpmux -version\n");
306 printf(" webpmux argument_file_name\n");
307
308 printf("\n");
309 printf("GET_OPTIONS:\n");
310 printf(" Extract relevant data:\n");
311 printf(" icc get ICC profile\n");
312 printf(" exif get EXIF metadata\n");
313 printf(" xmp get XMP metadata\n");
314 printf(" frame n get nth frame\n");
315
316 printf("\n");
317 printf("SET_OPTIONS:\n");
318 printf(" Set color profile/metadata:\n");
319 printf(" loop LOOP_COUNT set the loop count\n");
320 printf(" icc file.icc set ICC profile\n");
321 printf(" exif file.exif set EXIF metadata\n");
322 printf(" xmp file.xmp set XMP metadata\n");
323 printf(" where: 'file.icc' contains the ICC profile to be set,\n");
324 printf(" 'file.exif' contains the EXIF metadata to be set\n");
325 printf(" 'file.xmp' contains the XMP metadata to be set\n");
326
327 printf("\n");
328 printf("DURATION_OPTIONS:\n");
329 printf(" Set duration of selected frames:\n");
330 printf(" duration set duration for each frames\n");
331 printf(" duration,frame set duration of a particular frame\n");
332 printf(" duration,start,end set duration of frames in the\n");
333 printf(" interval [start,end])\n");
334 printf(" where: 'duration' is the duration in milliseconds\n");
335 printf(" 'start' is the start frame index\n");
336 printf(" 'end' is the inclusive end frame index\n");
337 printf(" The special 'end' value '0' means: last frame.\n");
338
339 printf("\n");
340 printf("STRIP_OPTIONS:\n");
341 printf(" Strip color profile/metadata:\n");
342 printf(" icc strip ICC profile\n");
343 printf(" exif strip EXIF metadata\n");
344 printf(" xmp strip XMP metadata\n");
345
346 printf("\n");
347 printf("FRAME_OPTIONS(i):\n");
348 printf(" Create animation:\n");
349 printf(" file_i +di+[xi+yi[+mi[bi]]]\n");
350 printf(" where: 'file_i' is the i'th animation frame (WebP format),\n");
351 printf(" 'di' is the pause duration before next frame,\n");
352 printf(" 'xi','yi' specify the image offset for this frame,\n");
353 printf(" 'mi' is the dispose method for this frame (0 or 1),\n");
354 printf(" 'bi' is the blending method for this frame (+b or -b)"
355 "\n");
356
357 printf("\n");
358 printf("LOOP_COUNT:\n");
359 printf(" Number of times to repeat the animation.\n");
360 printf(" Valid range is 0 to 65535 [Default: 0 (infinite)].\n");
361
362 printf("\n");
363 printf("BACKGROUND_COLOR:\n");
364 printf(" Background color of the canvas.\n");
365 printf(" A,R,G,B\n");
366 printf(" where: 'A', 'R', 'G' and 'B' are integers in the range 0 to 255 "
367 "specifying\n");
368 printf(" the Alpha, Red, Green and Blue component values "
369 "respectively\n");
370 printf(" [Default: 255,255,255,255]\n");
371
372 printf("\nINPUT & OUTPUT are in WebP format.\n");
373
374 printf("\nNote: The nature of EXIF, XMP and ICC data is not checked");
375 printf(" and is assumed to be\nvalid.\n");
376 printf("\nNote: if a single file name is passed as the argument, the "
377 "arguments will be\n");
378 printf("tokenized from this file. The file name must not start with "
379 "the character '-'.\n");
380 }
381
WarnAboutOddOffset(const WebPMuxFrameInfo * const info)382 static void WarnAboutOddOffset(const WebPMuxFrameInfo* const info) {
383 if ((info->x_offset | info->y_offset) & 1) {
384 fprintf(stderr, "Warning: odd offsets will be snapped to even values"
385 " (%d, %d) -> (%d, %d)\n", info->x_offset, info->y_offset,
386 info->x_offset & ~1, info->y_offset & ~1);
387 }
388 }
389
CreateMux(const char * const filename,WebPMux ** mux)390 static int CreateMux(const char* const filename, WebPMux** mux) {
391 WebPData bitstream;
392 assert(mux != NULL);
393 if (!ExUtilReadFileToWebPData(filename, &bitstream)) return 0;
394 *mux = WebPMuxCreate(&bitstream, 1);
395 WebPDataClear(&bitstream);
396 if (*mux != NULL) return 1;
397 WFPRINTF(stderr, "Failed to create mux object from file %s.\n",
398 (const W_CHAR*)filename);
399 return 0;
400 }
401
WriteData(const char * filename,const WebPData * const webpdata)402 static int WriteData(const char* filename, const WebPData* const webpdata) {
403 int ok = 0;
404 FILE* fout = WSTRCMP(filename, "-") ? WFOPEN(filename, "wb")
405 : ImgIoUtilSetBinaryMode(stdout);
406 if (fout == NULL) {
407 WFPRINTF(stderr, "Error opening output WebP file %s!\n",
408 (const W_CHAR*)filename);
409 return 0;
410 }
411 if (fwrite(webpdata->bytes, webpdata->size, 1, fout) != 1) {
412 WFPRINTF(stderr, "Error writing file %s!\n", (const W_CHAR*)filename);
413 } else {
414 WFPRINTF(stderr, "Saved file %s (%d bytes)\n",
415 (const W_CHAR*)filename, (int)webpdata->size);
416 ok = 1;
417 }
418 if (fout != stdout) fclose(fout);
419 return ok;
420 }
421
WriteWebP(WebPMux * const mux,const char * filename)422 static int WriteWebP(WebPMux* const mux, const char* filename) {
423 int ok;
424 WebPData webp_data;
425 const WebPMuxError err = WebPMuxAssemble(mux, &webp_data);
426 if (err != WEBP_MUX_OK) {
427 fprintf(stderr, "Error (%s) assembling the WebP file.\n", ErrorString(err));
428 return 0;
429 }
430 ok = WriteData(filename, &webp_data);
431 WebPDataClear(&webp_data);
432 return ok;
433 }
434
DuplicateMuxHeader(const WebPMux * const mux)435 static WebPMux* DuplicateMuxHeader(const WebPMux* const mux) {
436 WebPMux* new_mux = WebPMuxNew();
437 WebPMuxAnimParams p;
438 WebPMuxError err;
439 int i;
440 int ok = 1;
441
442 if (new_mux == NULL) return NULL;
443
444 err = WebPMuxGetAnimationParams(mux, &p);
445 if (err == WEBP_MUX_OK) {
446 err = WebPMuxSetAnimationParams(new_mux, &p);
447 if (err != WEBP_MUX_OK) {
448 ERROR_GOTO2("Error (%s) handling animation params.\n",
449 ErrorString(err), End);
450 }
451 } else {
452 /* it might not be an animation. Just keep moving. */
453 }
454
455 for (i = 1; i <= 3; ++i) {
456 WebPData metadata;
457 err = WebPMuxGetChunk(mux, kFourccList[i], &metadata);
458 if (err == WEBP_MUX_OK && metadata.size > 0) {
459 err = WebPMuxSetChunk(new_mux, kFourccList[i], &metadata, 1);
460 if (err != WEBP_MUX_OK) {
461 ERROR_GOTO1("Error transferring metadata in DuplicateMux().", End);
462 }
463 }
464 }
465
466 End:
467 if (!ok) {
468 WebPMuxDelete(new_mux);
469 new_mux = NULL;
470 }
471 return new_mux;
472 }
473
ParseFrameArgs(const char * args,WebPMuxFrameInfo * const info)474 static int ParseFrameArgs(const char* args, WebPMuxFrameInfo* const info) {
475 int dispose_method, unused;
476 char plus_minus, blend_method;
477 const int num_args = sscanf(args, "+%d+%d+%d+%d%c%c+%d", &info->duration,
478 &info->x_offset, &info->y_offset, &dispose_method,
479 &plus_minus, &blend_method, &unused);
480 switch (num_args) {
481 case 1:
482 info->x_offset = info->y_offset = 0; // fall through
483 case 3:
484 dispose_method = 0; // fall through
485 case 4:
486 plus_minus = '+';
487 blend_method = 'b'; // fall through
488 case 6:
489 break;
490 case 2:
491 case 5:
492 default:
493 return 0;
494 }
495
496 WarnAboutOddOffset(info);
497
498 // Note: The validity of the following conversion is checked by
499 // WebPMuxPushFrame().
500 info->dispose_method = (WebPMuxAnimDispose)dispose_method;
501
502 if (blend_method != 'b') return 0;
503 if (plus_minus != '-' && plus_minus != '+') return 0;
504 info->blend_method =
505 (plus_minus == '+') ? WEBP_MUX_BLEND : WEBP_MUX_NO_BLEND;
506 return 1;
507 }
508
ParseBgcolorArgs(const char * args,uint32_t * const bgcolor)509 static int ParseBgcolorArgs(const char* args, uint32_t* const bgcolor) {
510 uint32_t a, r, g, b;
511 if (sscanf(args, "%u,%u,%u,%u", &a, &r, &g, &b) != 4) return 0;
512 if (a >= 256 || r >= 256 || g >= 256 || b >= 256) return 0;
513 *bgcolor = (a << 24) | (r << 16) | (g << 8) | (b << 0);
514 return 1;
515 }
516
517 //------------------------------------------------------------------------------
518 // Clean-up.
519
DeleteConfig(Config * const config)520 static void DeleteConfig(Config* const config) {
521 if (config != NULL) {
522 free(config->args_);
523 ExUtilDeleteCommandLineArguments(&config->cmd_args_);
524 memset(config, 0, sizeof(*config));
525 }
526 }
527
528 //------------------------------------------------------------------------------
529 // Parsing.
530
531 // Basic syntactic checks on the command-line arguments.
532 // Returns 1 on valid, 0 otherwise.
533 // Also fills up num_feature_args to be number of feature arguments given.
534 // (e.g. if there are 4 '-frame's and 1 '-loop', then num_feature_args = 5).
ValidateCommandLine(const CommandLineArguments * const cmd_args,int * num_feature_args)535 static int ValidateCommandLine(const CommandLineArguments* const cmd_args,
536 int* num_feature_args) {
537 int num_frame_args;
538 int num_loop_args;
539 int num_bgcolor_args;
540 int num_durations_args;
541 int ok = 1;
542
543 assert(num_feature_args != NULL);
544 *num_feature_args = 0;
545
546 // Simple checks.
547 if (CountOccurrences(cmd_args, "-get") > 1) {
548 ERROR_GOTO1("ERROR: Multiple '-get' arguments specified.\n", ErrValidate);
549 }
550 if (CountOccurrences(cmd_args, "-set") > 1) {
551 ERROR_GOTO1("ERROR: Multiple '-set' arguments specified.\n", ErrValidate);
552 }
553 if (CountOccurrences(cmd_args, "-strip") > 1) {
554 ERROR_GOTO1("ERROR: Multiple '-strip' arguments specified.\n", ErrValidate);
555 }
556 if (CountOccurrences(cmd_args, "-info") > 1) {
557 ERROR_GOTO1("ERROR: Multiple '-info' arguments specified.\n", ErrValidate);
558 }
559 if (CountOccurrences(cmd_args, "-o") > 1) {
560 ERROR_GOTO1("ERROR: Multiple output files specified.\n", ErrValidate);
561 }
562
563 // Compound checks.
564 num_frame_args = CountOccurrences(cmd_args, "-frame");
565 num_loop_args = CountOccurrences(cmd_args, "-loop");
566 num_bgcolor_args = CountOccurrences(cmd_args, "-bgcolor");
567 num_durations_args = CountOccurrences(cmd_args, "-duration");
568
569 if (num_loop_args > 1) {
570 ERROR_GOTO1("ERROR: Multiple loop counts specified.\n", ErrValidate);
571 }
572 if (num_bgcolor_args > 1) {
573 ERROR_GOTO1("ERROR: Multiple background colors specified.\n", ErrValidate);
574 }
575
576 if ((num_frame_args == 0) && (num_loop_args + num_bgcolor_args > 0)) {
577 ERROR_GOTO1("ERROR: Loop count and background color are relevant only in "
578 "case of animation.\n", ErrValidate);
579 }
580 if (num_durations_args > 0 && num_frame_args != 0) {
581 ERROR_GOTO1("ERROR: Can not combine -duration and -frame commands.\n",
582 ErrValidate);
583 }
584
585 assert(ok == 1);
586 if (num_durations_args > 0) {
587 *num_feature_args = num_durations_args;
588 } else if (num_frame_args == 0) {
589 // Single argument ('set' action for ICCP/EXIF/XMP, OR a 'get' action).
590 *num_feature_args = 1;
591 } else {
592 // Multiple arguments ('set' action for animation)
593 *num_feature_args = num_frame_args + num_loop_args + num_bgcolor_args;
594 }
595
596 ErrValidate:
597 return ok;
598 }
599
600 #define ACTION_IS_NIL (config->action_type_ == NIL_ACTION)
601
602 #define FEATURETYPE_IS_NIL (config->type_ == NIL_FEATURE)
603
604 #define CHECK_NUM_ARGS_AT_LEAST(NUM, LABEL) \
605 if (argc < i + (NUM)) { \
606 fprintf(stderr, "ERROR: Too few arguments for '%s'.\n", argv[i]); \
607 goto LABEL; \
608 }
609
610 #define CHECK_NUM_ARGS_AT_MOST(NUM, LABEL) \
611 if (argc > i + (NUM)) { \
612 fprintf(stderr, "ERROR: Too many arguments for '%s'.\n", argv[i]); \
613 goto LABEL; \
614 }
615
616 #define CHECK_NUM_ARGS_EXACTLY(NUM, LABEL) \
617 CHECK_NUM_ARGS_AT_LEAST(NUM, LABEL); \
618 CHECK_NUM_ARGS_AT_MOST(NUM, LABEL);
619
620 // Parses command-line arguments to fill up config object. Also performs some
621 // semantic checks. unicode_argv contains wchar_t arguments or is null.
ParseCommandLine(Config * config,const W_CHAR ** const unicode_argv)622 static int ParseCommandLine(Config* config, const W_CHAR** const unicode_argv) {
623 int i = 0;
624 int feature_arg_index = 0;
625 int ok = 1;
626 int argc = config->cmd_args_.argc_;
627 const char* const* argv = config->cmd_args_.argv_;
628 // Unicode file paths will be used if available.
629 const char* const* wargv =
630 (unicode_argv != NULL) ? (const char**)(unicode_argv + 1) : argv;
631
632 while (i < argc) {
633 FeatureArg* const arg = &config->args_[feature_arg_index];
634 if (argv[i][0] == '-') { // One of the action types or output.
635 if (!strcmp(argv[i], "-set")) {
636 if (ACTION_IS_NIL) {
637 config->action_type_ = ACTION_SET;
638 } else {
639 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
640 }
641 ++i;
642 } else if (!strcmp(argv[i], "-duration")) {
643 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
644 if (ACTION_IS_NIL || config->action_type_ == ACTION_DURATION) {
645 config->action_type_ = ACTION_DURATION;
646 } else {
647 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
648 }
649 if (FEATURETYPE_IS_NIL || config->type_ == FEATURE_DURATION) {
650 config->type_ = FEATURE_DURATION;
651 } else {
652 ERROR_GOTO1("ERROR: Multiple features specified.\n", ErrParse);
653 }
654 arg->params_ = argv[i + 1];
655 ++feature_arg_index;
656 i += 2;
657 } else if (!strcmp(argv[i], "-get")) {
658 if (ACTION_IS_NIL) {
659 config->action_type_ = ACTION_GET;
660 } else {
661 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
662 }
663 ++i;
664 } else if (!strcmp(argv[i], "-strip")) {
665 if (ACTION_IS_NIL) {
666 config->action_type_ = ACTION_STRIP;
667 config->arg_count_ = 0;
668 } else {
669 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
670 }
671 ++i;
672 } else if (!strcmp(argv[i], "-frame")) {
673 CHECK_NUM_ARGS_AT_LEAST(3, ErrParse);
674 if (ACTION_IS_NIL || config->action_type_ == ACTION_SET) {
675 config->action_type_ = ACTION_SET;
676 } else {
677 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
678 }
679 if (FEATURETYPE_IS_NIL || config->type_ == FEATURE_ANMF) {
680 config->type_ = FEATURE_ANMF;
681 } else {
682 ERROR_GOTO1("ERROR: Multiple features specified.\n", ErrParse);
683 }
684 arg->subtype_ = SUBTYPE_ANMF;
685 arg->filename_ = argv[i + 1];
686 arg->params_ = argv[i + 2];
687 ++feature_arg_index;
688 i += 3;
689 } else if (!strcmp(argv[i], "-loop") || !strcmp(argv[i], "-bgcolor")) {
690 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
691 if (ACTION_IS_NIL || config->action_type_ == ACTION_SET) {
692 config->action_type_ = ACTION_SET;
693 } else {
694 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
695 }
696 if (FEATURETYPE_IS_NIL || config->type_ == FEATURE_ANMF) {
697 config->type_ = FEATURE_ANMF;
698 } else {
699 ERROR_GOTO1("ERROR: Multiple features specified.\n", ErrParse);
700 }
701 arg->subtype_ =
702 !strcmp(argv[i], "-loop") ? SUBTYPE_LOOP : SUBTYPE_BGCOLOR;
703 arg->params_ = argv[i + 1];
704 ++feature_arg_index;
705 i += 2;
706 } else if (!strcmp(argv[i], "-o")) {
707 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
708 config->output_ = wargv[i + 1];
709 i += 2;
710 } else if (!strcmp(argv[i], "-info")) {
711 CHECK_NUM_ARGS_EXACTLY(2, ErrParse);
712 if (config->action_type_ != NIL_ACTION) {
713 ERROR_GOTO1("ERROR: Multiple actions specified.\n", ErrParse);
714 } else {
715 config->action_type_ = ACTION_INFO;
716 config->arg_count_ = 0;
717 config->input_ = wargv[i + 1];
718 }
719 i += 2;
720 } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "-help")) {
721 PrintHelp();
722 DeleteConfig(config);
723 LOCAL_FREE((W_CHAR** const)unicode_argv);
724 exit(0);
725 } else if (!strcmp(argv[i], "-version")) {
726 const int version = WebPGetMuxVersion();
727 printf("%d.%d.%d\n",
728 (version >> 16) & 0xff, (version >> 8) & 0xff, version & 0xff);
729 DeleteConfig(config);
730 LOCAL_FREE((W_CHAR** const)unicode_argv);
731 exit(0);
732 } else if (!strcmp(argv[i], "--")) {
733 if (i < argc - 1) {
734 ++i;
735 if (config->input_ == NULL) {
736 config->input_ = wargv[i];
737 } else {
738 ERROR_GOTO2("ERROR at '%s': Multiple input files specified.\n",
739 argv[i], ErrParse);
740 }
741 }
742 break;
743 } else {
744 ERROR_GOTO2("ERROR: Unknown option: '%s'.\n", argv[i], ErrParse);
745 }
746 } else { // One of the feature types or input.
747 if (ACTION_IS_NIL) {
748 ERROR_GOTO1("ERROR: Action must be specified before other arguments.\n",
749 ErrParse);
750 }
751 if (!strcmp(argv[i], "icc") || !strcmp(argv[i], "exif") ||
752 !strcmp(argv[i], "xmp")) {
753 if (FEATURETYPE_IS_NIL) {
754 config->type_ = (!strcmp(argv[i], "icc")) ? FEATURE_ICCP :
755 (!strcmp(argv[i], "exif")) ? FEATURE_EXIF : FEATURE_XMP;
756 } else {
757 ERROR_GOTO1("ERROR: Multiple features specified.\n", ErrParse);
758 }
759 if (config->action_type_ == ACTION_SET) {
760 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
761 arg->filename_ = wargv[i + 1];
762 ++feature_arg_index;
763 i += 2;
764 } else {
765 ++i;
766 }
767 } else if (!strcmp(argv[i], "frame") &&
768 (config->action_type_ == ACTION_GET)) {
769 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
770 config->type_ = FEATURE_ANMF;
771 arg->params_ = argv[i + 1];
772 ++feature_arg_index;
773 i += 2;
774 } else if (!strcmp(argv[i], "loop") &&
775 (config->action_type_ == ACTION_SET)) {
776 CHECK_NUM_ARGS_AT_LEAST(2, ErrParse);
777 config->type_ = FEATURE_LOOP;
778 arg->params_ = argv[i + 1];
779 ++feature_arg_index;
780 i += 2;
781 } else { // Assume input file.
782 if (config->input_ == NULL) {
783 config->input_ = wargv[i];
784 } else {
785 ERROR_GOTO2("ERROR at '%s': Multiple input files specified.\n",
786 argv[i], ErrParse);
787 }
788 ++i;
789 }
790 }
791 }
792 ErrParse:
793 return ok;
794 }
795
796 // Additional checks after config is filled.
ValidateConfig(Config * const config)797 static int ValidateConfig(Config* const config) {
798 int ok = 1;
799
800 // Action.
801 if (ACTION_IS_NIL) {
802 ERROR_GOTO1("ERROR: No action specified.\n", ErrValidate2);
803 }
804
805 // Feature type.
806 if (FEATURETYPE_IS_NIL && config->action_type_ != ACTION_INFO) {
807 ERROR_GOTO1("ERROR: No feature specified.\n", ErrValidate2);
808 }
809
810 // Input file.
811 if (config->input_ == NULL) {
812 if (config->action_type_ != ACTION_SET) {
813 ERROR_GOTO1("ERROR: No input file specified.\n", ErrValidate2);
814 } else if (config->type_ != FEATURE_ANMF) {
815 ERROR_GOTO1("ERROR: No input file specified.\n", ErrValidate2);
816 }
817 }
818
819 // Output file.
820 if (config->output_ == NULL && config->action_type_ != ACTION_INFO) {
821 ERROR_GOTO1("ERROR: No output file specified.\n", ErrValidate2);
822 }
823
824 ErrValidate2:
825 return ok;
826 }
827
828 // Create config object from command-line arguments.
InitializeConfig(int argc,const char * argv[],Config * const config,const W_CHAR ** const unicode_argv)829 static int InitializeConfig(int argc, const char* argv[], Config* const config,
830 const W_CHAR** const unicode_argv) {
831 int num_feature_args = 0;
832 int ok;
833
834 memset(config, 0, sizeof(*config));
835
836 ok = ExUtilInitCommandLineArguments(argc, argv, &config->cmd_args_);
837 if (!ok) return 0;
838
839 // Validate command-line arguments.
840 if (!ValidateCommandLine(&config->cmd_args_, &num_feature_args)) {
841 ERROR_GOTO1("Exiting due to command-line parsing error.\n", Err1);
842 }
843
844 config->arg_count_ = num_feature_args;
845 config->args_ = (FeatureArg*)calloc(num_feature_args, sizeof(*config->args_));
846 if (config->args_ == NULL) {
847 ERROR_GOTO1("ERROR: Memory allocation error.\n", Err1);
848 }
849
850 // Parse command-line.
851 if (!ParseCommandLine(config, unicode_argv) || !ValidateConfig(config)) {
852 ERROR_GOTO1("Exiting due to command-line parsing error.\n", Err1);
853 }
854
855 Err1:
856 return ok;
857 }
858
859 #undef ACTION_IS_NIL
860 #undef FEATURETYPE_IS_NIL
861 #undef CHECK_NUM_ARGS_AT_LEAST
862 #undef CHECK_NUM_ARGS_AT_MOST
863 #undef CHECK_NUM_ARGS_EXACTLY
864
865 //------------------------------------------------------------------------------
866 // Processing.
867
GetFrame(const WebPMux * mux,const Config * config)868 static int GetFrame(const WebPMux* mux, const Config* config) {
869 WebPMuxError err = WEBP_MUX_OK;
870 WebPMux* mux_single = NULL;
871 int num = 0;
872 int ok = 1;
873 int parse_error = 0;
874 const WebPChunkId id = WEBP_CHUNK_ANMF;
875 WebPMuxFrameInfo info;
876 WebPDataInit(&info.bitstream);
877
878 num = ExUtilGetInt(config->args_[0].params_, 10, &parse_error);
879 if (num < 0) {
880 ERROR_GOTO1("ERROR: Frame/Fragment index must be non-negative.\n", ErrGet);
881 }
882 if (parse_error) goto ErrGet;
883
884 err = WebPMuxGetFrame(mux, num, &info);
885 if (err == WEBP_MUX_OK && info.id != id) err = WEBP_MUX_NOT_FOUND;
886 if (err != WEBP_MUX_OK) {
887 ERROR_GOTO3("ERROR (%s): Could not get frame %d.\n",
888 ErrorString(err), num, ErrGet);
889 }
890
891 mux_single = WebPMuxNew();
892 if (mux_single == NULL) {
893 err = WEBP_MUX_MEMORY_ERROR;
894 ERROR_GOTO2("ERROR (%s): Could not allocate a mux object.\n",
895 ErrorString(err), ErrGet);
896 }
897 err = WebPMuxSetImage(mux_single, &info.bitstream, 1);
898 if (err != WEBP_MUX_OK) {
899 ERROR_GOTO2("ERROR (%s): Could not create single image mux object.\n",
900 ErrorString(err), ErrGet);
901 }
902
903 ok = WriteWebP(mux_single, config->output_);
904
905 ErrGet:
906 WebPDataClear(&info.bitstream);
907 WebPMuxDelete(mux_single);
908 return ok && !parse_error;
909 }
910
911 // Read and process config.
Process(const Config * config)912 static int Process(const Config* config) {
913 WebPMux* mux = NULL;
914 WebPData chunk;
915 WebPMuxError err = WEBP_MUX_OK;
916 int ok = 1;
917
918 switch (config->action_type_) {
919 case ACTION_GET: {
920 ok = CreateMux(config->input_, &mux);
921 if (!ok) goto Err2;
922 switch (config->type_) {
923 case FEATURE_ANMF:
924 ok = GetFrame(mux, config);
925 break;
926
927 case FEATURE_ICCP:
928 case FEATURE_EXIF:
929 case FEATURE_XMP:
930 err = WebPMuxGetChunk(mux, kFourccList[config->type_], &chunk);
931 if (err != WEBP_MUX_OK) {
932 ERROR_GOTO3("ERROR (%s): Could not get the %s.\n",
933 ErrorString(err), kDescriptions[config->type_], Err2);
934 }
935 ok = WriteData(config->output_, &chunk);
936 break;
937
938 default:
939 ERROR_GOTO1("ERROR: Invalid feature for action 'get'.\n", Err2);
940 break;
941 }
942 break;
943 }
944 case ACTION_SET: {
945 switch (config->type_) {
946 case FEATURE_ANMF: {
947 int i;
948 WebPMuxAnimParams params = { 0xFFFFFFFF, 0 };
949 mux = WebPMuxNew();
950 if (mux == NULL) {
951 ERROR_GOTO2("ERROR (%s): Could not allocate a mux object.\n",
952 ErrorString(WEBP_MUX_MEMORY_ERROR), Err2);
953 }
954 for (i = 0; i < config->arg_count_; ++i) {
955 switch (config->args_[i].subtype_) {
956 case SUBTYPE_BGCOLOR: {
957 uint32_t bgcolor;
958 ok = ParseBgcolorArgs(config->args_[i].params_, &bgcolor);
959 if (!ok) {
960 ERROR_GOTO1("ERROR: Could not parse the background color \n",
961 Err2);
962 }
963 params.bgcolor = bgcolor;
964 break;
965 }
966 case SUBTYPE_LOOP: {
967 int parse_error = 0;
968 const int loop_count =
969 ExUtilGetInt(config->args_[i].params_, 10, &parse_error);
970 if (loop_count < 0 || loop_count > 65535) {
971 // Note: This is only a 'necessary' condition for loop_count
972 // to be valid. The 'sufficient' conditioned in checked in
973 // WebPMuxSetAnimationParams() method called later.
974 ERROR_GOTO1("ERROR: Loop count must be in the range 0 to "
975 "65535.\n", Err2);
976 }
977 ok = !parse_error;
978 if (!ok) goto Err2;
979 params.loop_count = loop_count;
980 break;
981 }
982 case SUBTYPE_ANMF: {
983 WebPMuxFrameInfo frame;
984 frame.id = WEBP_CHUNK_ANMF;
985 ok = ExUtilReadFileToWebPData(config->args_[i].filename_,
986 &frame.bitstream);
987 if (!ok) goto Err2;
988 ok = ParseFrameArgs(config->args_[i].params_, &frame);
989 if (!ok) {
990 WebPDataClear(&frame.bitstream);
991 ERROR_GOTO1("ERROR: Could not parse frame properties.\n",
992 Err2);
993 }
994 err = WebPMuxPushFrame(mux, &frame, 1);
995 WebPDataClear(&frame.bitstream);
996 if (err != WEBP_MUX_OK) {
997 ERROR_GOTO3("ERROR (%s): Could not add a frame at index %d."
998 "\n", ErrorString(err), i, Err2);
999 }
1000 break;
1001 }
1002 default: {
1003 ERROR_GOTO1("ERROR: Invalid subtype for 'frame'", Err2);
1004 break;
1005 }
1006 }
1007 }
1008 err = WebPMuxSetAnimationParams(mux, ¶ms);
1009 if (err != WEBP_MUX_OK) {
1010 ERROR_GOTO2("ERROR (%s): Could not set animation parameters.\n",
1011 ErrorString(err), Err2);
1012 }
1013 break;
1014 }
1015
1016 case FEATURE_ICCP:
1017 case FEATURE_EXIF:
1018 case FEATURE_XMP: {
1019 ok = CreateMux(config->input_, &mux);
1020 if (!ok) goto Err2;
1021 ok = ExUtilReadFileToWebPData(config->args_[0].filename_, &chunk);
1022 if (!ok) goto Err2;
1023 err = WebPMuxSetChunk(mux, kFourccList[config->type_], &chunk, 1);
1024 WebPDataClear(&chunk);
1025 if (err != WEBP_MUX_OK) {
1026 ERROR_GOTO3("ERROR (%s): Could not set the %s.\n",
1027 ErrorString(err), kDescriptions[config->type_], Err2);
1028 }
1029 break;
1030 }
1031 case FEATURE_LOOP: {
1032 WebPMuxAnimParams params = { 0xFFFFFFFF, 0 };
1033 int parse_error = 0;
1034 const int loop_count =
1035 ExUtilGetInt(config->args_[0].params_, 10, &parse_error);
1036 if (loop_count < 0 || loop_count > 65535 || parse_error) {
1037 ERROR_GOTO1("ERROR: Loop count must be in the range 0 to 65535.\n",
1038 Err2);
1039 }
1040 ok = CreateMux(config->input_, &mux);
1041 if (!ok) goto Err2;
1042 ok = (WebPMuxGetAnimationParams(mux, ¶ms) == WEBP_MUX_OK);
1043 if (!ok) {
1044 ERROR_GOTO1("ERROR: input file does not seem to be an animation.\n",
1045 Err2);
1046 }
1047 params.loop_count = loop_count;
1048 err = WebPMuxSetAnimationParams(mux, ¶ms);
1049 ok = (err == WEBP_MUX_OK);
1050 if (!ok) {
1051 ERROR_GOTO2("ERROR (%s): Could not set animation parameters.\n",
1052 ErrorString(err), Err2);
1053 }
1054 break;
1055 }
1056 default: {
1057 ERROR_GOTO1("ERROR: Invalid feature for action 'set'.\n", Err2);
1058 break;
1059 }
1060 }
1061 ok = WriteWebP(mux, config->output_);
1062 break;
1063 }
1064 case ACTION_DURATION: {
1065 int num_frames;
1066 ok = CreateMux(config->input_, &mux);
1067 if (!ok) goto Err2;
1068 err = WebPMuxNumChunks(mux, WEBP_CHUNK_ANMF, &num_frames);
1069 ok = (err == WEBP_MUX_OK);
1070 if (!ok) {
1071 ERROR_GOTO1("ERROR: can not parse the number of frames.\n", Err2);
1072 }
1073 if (num_frames == 0) {
1074 fprintf(stderr, "Doesn't look like the source is animated. "
1075 "Skipping duration setting.\n");
1076 ok = WriteWebP(mux, config->output_);
1077 if (!ok) goto Err2;
1078 } else {
1079 int i;
1080 int* durations = NULL;
1081 WebPMux* new_mux = DuplicateMuxHeader(mux);
1082 if (new_mux == NULL) goto Err2;
1083 durations = (int*)WebPMalloc((size_t)num_frames * sizeof(*durations));
1084 if (durations == NULL) goto Err2;
1085 for (i = 0; i < num_frames; ++i) durations[i] = -1;
1086
1087 // Parse intervals to process.
1088 for (i = 0; i < config->arg_count_; ++i) {
1089 int k;
1090 int args[3];
1091 int duration, start, end;
1092 const int nb_args = ExUtilGetInts(config->args_[i].params_,
1093 10, 3, args);
1094 ok = (nb_args >= 1);
1095 if (!ok) goto Err3;
1096 duration = args[0];
1097 if (duration < 0) {
1098 ERROR_GOTO1("ERROR: duration must be strictly positive.\n", Err3);
1099 }
1100
1101 if (nb_args == 1) { // only duration is present -> use full interval
1102 start = 1;
1103 end = num_frames;
1104 } else {
1105 start = args[1];
1106 if (start <= 0) {
1107 start = 1;
1108 } else if (start > num_frames) {
1109 start = num_frames;
1110 }
1111 end = (nb_args >= 3) ? args[2] : start;
1112 if (end == 0 || end > num_frames) end = num_frames;
1113 }
1114
1115 for (k = start; k <= end; ++k) {
1116 assert(k >= 1 && k <= num_frames);
1117 durations[k - 1] = duration;
1118 }
1119 }
1120
1121 // Apply non-negative durations to their destination frames.
1122 for (i = 1; i <= num_frames; ++i) {
1123 WebPMuxFrameInfo frame;
1124 err = WebPMuxGetFrame(mux, i, &frame);
1125 if (err != WEBP_MUX_OK || frame.id != WEBP_CHUNK_ANMF) {
1126 ERROR_GOTO2("ERROR: can not retrieve frame #%d.\n", i, Err3);
1127 }
1128 if (durations[i - 1] >= 0) frame.duration = durations[i - 1];
1129 err = WebPMuxPushFrame(new_mux, &frame, 1);
1130 if (err != WEBP_MUX_OK) {
1131 ERROR_GOTO2("ERROR: error push frame data #%d\n", i, Err3);
1132 }
1133 WebPDataClear(&frame.bitstream);
1134 }
1135 WebPMuxDelete(mux);
1136 ok = WriteWebP(new_mux, config->output_);
1137 mux = new_mux; // transfer for the WebPMuxDelete() call
1138 new_mux = NULL;
1139
1140 Err3:
1141 WebPFree(durations);
1142 WebPMuxDelete(new_mux);
1143 if (!ok) goto Err2;
1144 }
1145 break;
1146 }
1147 case ACTION_STRIP: {
1148 ok = CreateMux(config->input_, &mux);
1149 if (!ok) goto Err2;
1150 if (config->type_ == FEATURE_ICCP || config->type_ == FEATURE_EXIF ||
1151 config->type_ == FEATURE_XMP) {
1152 err = WebPMuxDeleteChunk(mux, kFourccList[config->type_]);
1153 if (err != WEBP_MUX_OK) {
1154 ERROR_GOTO3("ERROR (%s): Could not strip the %s.\n",
1155 ErrorString(err), kDescriptions[config->type_], Err2);
1156 }
1157 } else {
1158 ERROR_GOTO1("ERROR: Invalid feature for action 'strip'.\n", Err2);
1159 break;
1160 }
1161 ok = WriteWebP(mux, config->output_);
1162 break;
1163 }
1164 case ACTION_INFO: {
1165 ok = CreateMux(config->input_, &mux);
1166 if (!ok) goto Err2;
1167 ok = (DisplayInfo(mux) == WEBP_MUX_OK);
1168 break;
1169 }
1170 default: {
1171 assert(0); // Invalid action.
1172 break;
1173 }
1174 }
1175
1176 Err2:
1177 WebPMuxDelete(mux);
1178 return ok;
1179 }
1180
1181 //------------------------------------------------------------------------------
1182 // Main.
1183
main(int argc,const char * argv[])1184 int main(int argc, const char* argv[]) {
1185 Config config;
1186 int ok;
1187
1188 INIT_WARGV(argc, argv);
1189
1190 ok = InitializeConfig(argc - 1, argv + 1, &config, GET_WARGV_OR_NULL());
1191 if (ok) {
1192 ok = Process(&config);
1193 } else {
1194 PrintHelp();
1195 }
1196 DeleteConfig(&config);
1197 FREE_WARGV_AND_RETURN(!ok);
1198 }
1199
1200 //------------------------------------------------------------------------------
1201