fixed bug in jpeg2000 decoding
[swftools.git] / src / gif2swf.c
1 /* -*- mode: c; tab-width: 4; -*- ---------------------------[for (x)emacs]--
2
3    $Id: gif2swf.c,v 1.7 2008/02/08 11:43:12 kramm Exp $
4    GIF to SWF converter tool
5
6    Part of the swftools package.
7
8    Copyright (c) 2005 Daichi Shinozaki <dseg@shield.jp>
9
10    This program is free software; you can redistribute it and/or modify
11    it under the terms of the GNU General Public License as published by
12    the Free Software Foundation; either version 2 of the License, or
13    (at your option) any later version.
14
15    This program is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19
20    You should have received a copy of the GNU General Public License
21    along with this program; if not, write to the Free Software
22    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
23
24    This file is derived from png2swf.c */
25
26 #include <stdio.h>
27 #include <fcntl.h>
28 #include <gif_lib.h>
29 #include "../lib/rfxswf.h"
30 #include "../lib/args.h"
31
32 #define MAX_INPUT_FILES 1024
33 #define VERBOSE(x) (global.verbose>=x)
34 #define AS_FIRSTFRAME "if(!n) n=0;"
35 #define AS_LASTFRAME "if(n<%d){n=n+1;gotoAndPlay(1);}else stop();"
36
37 struct {
38     float framerate;
39     int max_image_width;
40     int max_image_height;
41     int force_width;
42     int force_height;
43     int nfiles;
44     int verbose;
45     int do_cgi;
46     int version;
47     char *outfile;
48     int imagecount;
49     int loopcount;
50 } global;
51
52 struct {
53     char *filename;
54 } image[MAX_INPUT_FILES];
55
56 struct gif_header {
57     int width;
58     int height;
59 };
60
61 enum disposal_method {
62     NONE,
63     DO_NOT_DISPOSE,
64     RESTORE_TO_BGCOLOR,
65     RESTORE_TO_PREVIOUS
66 };
67
68
69 void SetFrameAction(TAG ** t, const char *src, int ver)
70 {
71     ActionTAG *as;
72
73     as = swf_ActionCompile(src, ver);
74     if (!as)
75         fprintf(stderr, "Couldn't compile ActionScript\n");
76     else {
77         *t = swf_InsertTag(*t, ST_DOACTION);
78         swf_ActionSet(*t, as);
79         swf_ActionFree(as);
80     }
81 }
82
83 int getGifDisposalMethod(GifFileType * gft, int framenum)
84 {
85     int i;
86     ExtensionBlock *ext = gft->SavedImages[framenum].ExtensionBlocks;
87
88     for (i = 0; i < gft->SavedImages[framenum].ExtensionBlockCount; i++, ext++)
89         if (ext->Function == GRAPHICS_EXT_FUNC_CODE)
90             return ((ext->Bytes[0] & 0x1C) >> 2);
91
92     return -1;
93 }
94
95 int getGifLoopCount(GifFileType * gft)
96 {
97     int i, loop = -1;
98     ExtensionBlock *ext = gft->SavedImages[0].ExtensionBlocks;
99
100     for (i = 0; i < gft->SavedImages[0].ExtensionBlockCount; i++, ext++)
101         if (ext->Function == APPLICATION_EXT_FUNC_CODE) {
102             // info: http://semmix.pl/color/exgraf/eeg24.htm
103             if (ext->ByteCount == 11 &&
104                 (strncmp(&ext->Bytes[0], "NETSCAPE2.0", 11) == 0 ||
105                  strncmp(&ext->Bytes[0], "ANIMEXTS1.0", 11) == 0)) {
106                 // check for the subblock
107                 ext++;
108                 if (ext->ByteCount != 3)
109                     ext--;
110                 else {
111                     loop = GET16(&ext->Bytes[1]);
112                     break;
113                 }
114             }
115         }
116
117     return loop;
118 }
119
120 U16 getGifDelayTime(GifFileType * gft, int framenum)
121 {
122     int i;
123     ExtensionBlock *ext = gft->SavedImages[framenum].ExtensionBlocks;
124
125     for (i = 0; i < gft->SavedImages[framenum].ExtensionBlockCount; i++, ext++)
126         if (ext->Function == GRAPHICS_EXT_FUNC_CODE)
127             return GET16(&ext->Bytes[1]);
128
129     return 0;
130 }
131
132 int getTransparentColor(GifFileType * gft, int framenum)
133 {
134     int i;
135     ExtensionBlock *ext = gft->SavedImages[framenum].ExtensionBlocks;
136
137     // Get transparency color from graphic extension block
138     for (i = 0; i < gft->SavedImages[framenum].ExtensionBlockCount; i++, ext++)
139         if ((ext->Function == GRAPHICS_EXT_FUNC_CODE) && (ext->Bytes[0] & 1)) {
140             // there is a transparent color
141             return ext->Bytes[3] == 0 ? 0 :     // exception
142                 (U8) ext->Bytes[3];             // transparency color
143         }
144
145     return -1;
146 }
147
148 TAG *MovieStart(SWF * swf, float framerate, int dx, int dy)
149 {
150     TAG *t;
151     RGBA rgb;
152
153     memset(swf, 0x00, sizeof(SWF));
154
155     swf->fileVersion = global.version;
156     swf->frameRate = (int) (256.0 * framerate);
157     swf->movieSize.xmax = dx * 20;
158     swf->movieSize.ymax = dy * 20;
159
160     t = swf->firstTag = swf_InsertTag(NULL, ST_SETBACKGROUNDCOLOR);
161
162     rgb.r = rgb.g = rgb.b = rgb.a = 0x00;
163
164     //rgb.g = 0xff; //<--- handy for testing alpha conversion
165     swf_SetRGB(t, &rgb);
166
167     return t;
168 }
169
170 int MovieFinish(SWF * swf, TAG * t, char *sname)
171 {
172     int f, so = fileno(stdout);
173     t = swf_InsertTag(t, ST_END);
174
175     if ((!isatty(so)) && (!sname))
176         f = so;
177     else {
178         if (!sname)
179             sname = "output.swf";
180         f = open(sname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644);
181     }
182
183     if (global.do_cgi) {
184         if FAILED
185             (swf_WriteCGI(swf)) fprintf(stderr, "WriteCGI() failed.\n");
186     } else {
187         if (swf_WriteSWF(f, swf) < 0)
188             fprintf(stderr, "Unable to write output file: %s\n", sname);
189         if (f != so)
190             close(f);
191     }
192
193     swf_FreeTags(swf);
194     return 0;
195 }
196
197 TAG *MovieAddFrame(SWF * swf, TAG * t, char *sname, int id, int imgidx)
198 {
199     SHAPE *s;
200     SRECT r;
201     MATRIX m;
202     int fs;
203
204     U8 *imagedata, *from, *to;
205     GifImageDesc *img;
206     RGBA *pal;
207
208     struct gif_header header;
209
210     int i, j, numcolors, alphapalette;
211     U8 bgcolor;
212     int bpp;                    // byte per pixel
213     int swf_width, padlen;
214
215     ColorMapObject *colormap;
216     GifColorType c;
217     int interlacedOffset[] = { 0, 4, 2, 1 };    // The way Interlaced image should
218     int interlacedJumps[] = { 8, 8, 4, 2 };     // be read - offsets and jumps...
219     U16 delay, depth;
220     int disposal;
221     char *as_lastframe;
222
223     GifFileType *gft;
224     FILE *fi;
225
226     if ((fi = fopen(sname, "rb")) == NULL) {
227         if (VERBOSE(1))
228             fprintf(stderr, "Read access failed: %s\n", sname);
229         return t;
230     }
231     fclose(fi);
232
233     if ((gft = DGifOpenFileName(sname)) == NULL) {
234         fprintf(stderr, "%s is not a GIF file!\n", sname);
235         return t;
236     }
237
238     if (DGifSlurp(gft) != GIF_OK) {
239         PrintGifError();
240         return t;
241     }
242
243     header.width = gft->SWidth;
244     header.height = gft->SHeight;
245
246     pal = (RGBA *) malloc(256 * sizeof(RGBA));
247     memset(pal, 0, 256 * sizeof(RGBA));
248
249     img = &gft->SavedImages[imgidx].ImageDesc;
250
251     // Local colormap has precedence over Global colormap
252     colormap = img->ColorMap ? img->ColorMap : gft->SColorMap;
253     numcolors = colormap->ColorCount;
254     alphapalette = getTransparentColor(gft, imgidx);
255     if (VERBOSE(3))
256         fprintf(stderr, "transparent palette index => %d\n", alphapalette);
257     bpp = (alphapalette >= 0 ? 4 : 3);
258
259     // bgcolor is the background color to fill the bitmap
260     if (gft->SColorMap)         // There is a GlobalColorMap
261         bgcolor = (U8) gft->SBackGroundColor;   // The SBGColor is meaningful
262     else if (alphapalette >= 0) // There is a transparency color
263         bgcolor = alphapalette; // set the bgcolor to tranparent
264     else
265         bgcolor = 0;
266     // Don't know what to do here.
267     // If this doesn't work, we could
268     // create a new color and set the
269     // alpha-channel to transparent
270     // (unless we are using all the 256
271     // colors, in which case either we
272     // give up, or move to 16-bits palette
273     if (VERBOSE(3))
274         fprintf(stderr, "BG palette index => %u\n", bgcolor);
275
276     for (i = 0; i < numcolors; i++) {
277         c = colormap->Colors[i];
278         if (i == bgcolor || i == alphapalette)
279             pal[i].r = pal[i].g = pal[i].b = pal[i].a = 0;      // Fully transparent
280         else {
281             pal[i].r = c.Red;
282             pal[i].g = c.Green;
283             pal[i].b = c.Blue;
284             pal[i].a = 255;     // Fully opaque
285         }
286     }
287
288     t = swf_InsertTag(t, bpp == 4 ? ST_DEFINEBITSLOSSLESS2 : ST_DEFINEBITSLOSSLESS);
289     swf_SetU16(t, id);          // id
290
291     // Ah! The Flash specs says scanlines must be DWORD ALIGNED!
292     // (but image width is the correct number of pixels)
293     swf_width = BYTES_PER_SCANLINE(header.width);
294
295     if ((imagedata = (U8 *) malloc(swf_width * header.height)) == NULL) {
296         fprintf(stderr, "Failed to allocate memory required, aborted.");
297         exit(2);
298     }
299
300     to = imagedata;
301     from = (U8 *) gft->SavedImages[imgidx].RasterBits;
302
303     if (swf_width == header.width) {
304         // we are all nicely aligned and don't need to move the bitmap around.
305         // Just copy the bits into the image buffer.
306         if (!gft->Image.Interlace)
307             if (header.width == img->Width && header.height == img->Height)
308                 memcpy(to, from, header.width * header.height);
309             else {              //small screen
310                 for (i = 0; i < header.height; i++, to += header.width) {
311                     memset(to, bgcolor, header.width);
312                     if (i >= img->Top && i < img->Top + img->Height) {
313                         memcpy(to + img->Left, from, img->Width);
314                         from += img->Width;
315                     }
316                 }
317             }
318
319         else                    // Need to perform 4 passes on the interlaced images
320             for (i = 0; i < 4; i++)
321                 for (j = interlacedOffset[i]; j < header.height;
322                      j += interlacedJumps[i], from += header.width)
323                     memcpy(to + header.width * j, from, header.width);
324     } else {
325         padlen = swf_width - header.width;
326
327         // here we need to pad the scanline
328         if (!gft->Image.Interlace) {
329             if (header.width == img->Width && header.height == img->Height) {
330                 for (i = 0; i < header.height; i++, from += header.width, to += swf_width) {
331                     memcpy(to, from, header.width);
332                     memset(to + header.width, bgcolor, padlen);
333                 }
334             } else {            //small screen
335                 for (i = 0; i < header.height; i++, to += swf_width) {
336                     memset(to, bgcolor, swf_width);
337                     if (i >= img->Top && i < img->Top + img->Height) {
338                         memcpy(to + img->Left, from, img->Width);
339                         from += img->Width;
340                     }
341                 }
342             }
343         } else {                // Need to perform 4 passes on the interlaced images
344             for (i = 0; i < 4; i++)
345                 for (j = interlacedOffset[i]; j < header.height;
346                      j += interlacedJumps[i], from += header.width) {
347                     memcpy(to + swf_width * j, from, header.width);
348                     memset(to + swf_width * j, bgcolor, padlen);
349                 }
350         }
351     }
352     swf_SetLosslessBitsIndexed(t, header.width, header.height, imagedata, pal, 256);
353
354     t = swf_InsertTag(t, ST_DEFINESHAPE);
355
356     swf_ShapeNew(&s);
357     swf_GetMatrix(NULL, &m);
358     m.sx = 20 * 0x10000;
359     m.sy = 20 * 0x10000;
360     fs = swf_ShapeAddBitmapFillStyle(s, &m, id, 0);
361
362     swf_SetU16(t, id + 1);      // id
363
364     r.xmin = r.ymin = 0;
365     r.xmax = header.width * 20;
366     r.ymax = header.height * 20;
367     swf_SetRect(t, &r);
368
369     swf_SetShapeHeader(t, s);
370
371     swf_ShapeSetAll(t, s, 0, 0, 0, fs, 0);
372     swf_ShapeSetLine(t, s, r.xmax, 0);
373     swf_ShapeSetLine(t, s, 0, r.ymax);
374     swf_ShapeSetLine(t, s, -r.xmax, 0);
375     swf_ShapeSetLine(t, s, 0, -r.ymax);
376
377     swf_ShapeSetEnd(t);
378
379     depth = imgidx + 1;
380     if ((imgidx > 0) &&         // REMOVEOBJECT2 not needed at frame 1(imgidx==0)
381         (global.imagecount > 1)) {
382         // check last frame's disposal method
383         if ((disposal = getGifDisposalMethod(gft, imgidx - 1)) >= 0) {
384             switch (disposal) {
385             case NONE:
386                 // [Replace one full-size, non-transparent frame with another]
387                 t = swf_InsertTag(t, ST_REMOVEOBJECT2);
388                 swf_SetU16(t, depth - 1);
389                 if (VERBOSE(3))
390                     fprintf(stdout, "  [none]\n");
391                 break;
392             case DO_NOT_DISPOSE:
393                 // [Any pixels not covered up by the next frame continue to display]
394                 if (VERBOSE(3))
395                     fprintf(stdout, "  [don't dispose]\n");
396                 break;
397             case RESTORE_TO_BGCOLOR:
398                 // [The background color or background tile -rather than a previous frame-
399                 //  shows through transparent pixels]
400                 t = swf_InsertTag(t, ST_REMOVEOBJECT2);
401                 swf_SetU16(t, depth - 2);
402                 if (VERBOSE(3))
403                     fprintf(stdout, "  [restore to bg color]\n");
404                 break;
405             case RESTORE_TO_PREVIOUS:
406                 // [Restores to the state of a previous, undisposed frame]
407                 // ** NOT IMPLEMENTED YET (same as "restore to bgcolor") **
408                 t = swf_InsertTag(t, ST_REMOVEOBJECT2);
409                 swf_SetU16(t, depth - 1);
410                 if (VERBOSE(3))
411                     fprintf(stdout, "  [restore to previous]\n");
412                 break;
413             default:
414                 break;
415             }
416         }
417     }
418
419     swf_SetU16(t, depth);
420     t = swf_InsertTag(t, ST_PLACEOBJECT2);
421
422     swf_GetMatrix(NULL, &m);
423     m.tx = (swf->movieSize.xmax - (int) header.width * 20) / 2;
424     m.ty = (swf->movieSize.ymax - (int) header.height * 20) / 2;
425     swf_ObjectPlace(t, id + 1, depth, &m, NULL, NULL);
426
427     if ((global.imagecount > 1) && (global.loopcount > 0)) { // 0 means infinite loop
428         if (imgidx == 0)
429             SetFrameAction(&t, AS_FIRSTFRAME, global.version);
430     }
431
432     t = swf_InsertTag(t, ST_SHOWFRAME);
433
434     if (global.imagecount > 1) { // multi-frame GIF?
435         int framecnt;
436         delay = getGifDelayTime(gft, imgidx); // delay in 1/100 sec
437         framecnt = (int) (global.framerate * (delay / 100.0));
438         if (framecnt > 1) {
439             if (VERBOSE(2))
440                 fprintf(stderr, "at frame %d: pad %d frames(%.3f sec)\n",
441                         imgidx + 1, framecnt, delay / 100.0);
442
443             framecnt -= 1; // already inserted a frame
444             while (framecnt--)
445                 t = swf_InsertTag(t, ST_SHOWFRAME);
446         }
447         if ((imgidx == global.imagecount - 1) &&global.loopcount > 0) { // last frame
448             as_lastframe = malloc(strlen(AS_LASTFRAME) + 5); // 0-99999
449             sprintf(as_lastframe, AS_LASTFRAME, global.loopcount);
450             SetFrameAction(&t, as_lastframe, global.version);
451             if (as_lastframe)
452                 free(as_lastframe);
453         }
454     }
455
456     free(pal);
457     free(imagedata);
458     DGifCloseFile(gft);
459
460     return t;
461 }
462
463 int CheckInputFile(char *fname, char **realname)
464 {
465     FILE *fi;
466     char *s = malloc(strlen(fname) + 5);
467     GifFileType *gft;
468
469     if (!s)
470         exit(2);
471     (*realname) = s;
472     strcpy(s, fname);
473
474     // Check whether file exists (with typical extensions)
475
476     if ((fi = fopen(s, "rb")) == NULL) {
477         sprintf(s, "%s.gif", fname);
478         if ((fi = fopen(s, "rb")) == NULL) {
479             sprintf(s, "%s.GIF", fname);
480             if ((fi = fopen(s, "rb")) == NULL) {
481                 sprintf(s, "%s.Gif", fname);
482                 if ((fi = fopen(s, "rb")) == NULL) {
483                     fprintf(stderr, "Couldn't open %s!\n", fname);
484                     return -1;
485                 }
486             }
487         }
488     }
489     fclose(fi);
490
491     if ((gft = DGifOpenFileName(s)) == NULL) {
492         fprintf(stderr, "%s is not a GIF file!\n", fname);
493         return -1;
494     }
495
496     if (global.max_image_width < gft->SWidth)
497         global.max_image_width = gft->SWidth;
498     if (global.max_image_height < gft->SHeight)
499         global.max_image_height = gft->SHeight;
500
501     if (DGifSlurp(gft) != GIF_OK) { 
502         PrintGifError();
503         return -1;
504     }
505     // After DGifSlurp() call, gft->ImageCount become available
506     if ((global.imagecount = gft->ImageCount) >1) {
507         if (global.loopcount < 0) {
508             global.loopcount = getGifLoopCount(gft);
509             if (VERBOSE(3))
510                 fprintf(stderr, "Loops => %d\n", global.loopcount);
511         }
512     }
513     if (VERBOSE(2)) {
514         U8 i;
515         fprintf(stderr, "%d x %d, %d images total\n", gft->SWidth, gft->SHeight, gft->ImageCount);
516
517         for (i = 0; i < gft->ImageCount; i++)
518             fprintf(stderr, "frame: %u, delay: %.3f sec\n", i + 1, getGifDelayTime(gft, i) / 100.0);
519     }
520
521     DGifCloseFile(gft);
522
523     return 0;
524 }
525
526 int args_callback_option(char *arg, char *val)
527 {
528     int res = 0;
529     if (arg[1])
530         res = -1;
531     else
532         switch (arg[0]) {
533         case 'l':
534             if (val)
535                 global.loopcount = atoi(val);
536             res = 1;
537             break;
538
539         case 'r':
540             if (val)
541                 global.framerate = atof(val);
542             if ((global.framerate < 1.0 / 256) ||(global.framerate >= 256.0)) {
543                 if (VERBOSE(1))
544                     fprintf(stderr,
545                             "Error: You must specify a valid framerate between 1/256 and 255.\n");
546                 exit(1);
547             }
548             res = 1;
549             break;
550
551         case 'o':
552             if (val)
553                 global.outfile = val;
554             res = 1;
555             break;
556
557         case 'z':
558             global.version = 6;
559             res = 0;
560             break;
561
562         case 'C':
563             global.do_cgi = 1;
564             break;
565
566         case 'v':
567             if (val)
568                 global.verbose = atoi(val);
569             res = 1;
570             break;
571
572         case 'X':
573             if (val)
574                 global.force_width = atoi(val);
575             res = 1;
576             break;
577
578         case 'Y':
579             if (val)
580                 global.force_height = atoi(val);
581             res = 1;
582             break;
583
584         case 'V':
585             printf("gif2swf - part of %s %s\n", PACKAGE, VERSION);
586             exit(0);
587
588         default:
589             res = -1;
590             break;
591         }
592
593     if (res < 0) {
594         if (VERBOSE(1))
595             fprintf(stderr, "Unknown option: -%s\n", arg);
596         exit(1);
597         return 0;
598     }
599     return res;
600 }
601
602 static struct options_t options[] = {
603 {"r", "rate"},
604 {"o", "output"},
605 {"z", "zlib"},
606 {"l", "loop"},
607 {"X", "pixel"},
608 {"Y", "pixel"},
609 {"v", "verbose"},
610 {"C", "cgi"},
611 {"V", "version"},
612 {0,0}
613 };
614
615 int args_callback_longoption(char *name, char *val)
616 {
617     return args_long2shortoption(options, name, val);
618 }
619
620 int args_callback_command(char *arg, char *next) // actually used as filename
621 {
622     char *s;
623     if (CheckInputFile(arg, &s) < 0) {
624         if (VERBOSE(1))
625             fprintf(stderr, "Error opening input file: %s\n", arg);
626         free(s);
627
628     } else {
629         image[global.nfiles].filename = s;
630         global.nfiles++;
631         if (global.nfiles >= MAX_INPUT_FILES) {
632             if (VERBOSE(1))
633                 fprintf(stderr, "Error: Too many input files.\n");
634             exit(1);
635         }
636     }
637
638     return 0;
639 }
640
641 void args_callback_usage(char *name)
642 {
643     printf("\n");
644     printf("Usage: %s [-X width] [-Y height] [-o file.swf] [-r rate] file1.gif [file2.gif ...]\n", name);
645     printf("\n");
646     printf("-r , --rate <framerate>        Set movie framerate (frames per second)\n");
647     printf("-o , --output <filename>       Set name for SWF output file.\n");
648     printf("-z , --zlib <zlib>             Enable Flash 6 (MX) Zlib Compression\n");
649     printf("-l , --loop <loop count>           Set loop count. (default: 0 [=infinite loop])\n");
650     printf("-X , --pixel <width>           Force movie width to <width> (default: autodetect)\n");
651     printf("-Y , --pixel <height>          Force movie height to <height> (default: autodetect)\n");
652     printf("-v , --verbose <level>         Set verbose level (0=quiet, 1=default, 2=debug)\n");
653     printf("-C , --cgi                     For use as CGI- prepend http header, write to stdout\n");
654     printf("-V , --version                 Print version information and exit\n");
655     printf("\n");
656 }
657
658 int main(int argc, char **argv)
659 {
660     SWF swf;
661     TAG *t;
662
663     memset(&global, 0x00, sizeof(global));
664
665     global.framerate = 1.0;
666     global.verbose = 1;
667     global.version = 5;
668     global.loopcount = -1;
669
670     processargs(argc, argv);
671
672     if (global.nfiles <= 0) {
673         fprintf(stderr, "No gif files found in arguments\n");
674         return 1;
675     }
676
677     if (VERBOSE(2))
678         fprintf(stderr, "Processing %i file(s)...\n", global.nfiles);
679
680     if (global.imagecount > 1)       // multi-frame GIF?
681         if (global.framerate == 1.0) // user not specified '-r' option?
682             global.framerate = 10.0;
683
684     t = MovieStart(&swf, global.framerate,
685                    global.force_width ? global.force_width : global.max_image_width,
686                    global.force_height ? global.force_height : global.max_image_height);
687     {
688         int i, j;
689         for (i = 0; i < global.nfiles; i++) {
690             if (VERBOSE(3))
691                 fprintf(stderr, "[%03i] %s\n", i, image[i].filename);
692             t = MovieAddFrame(&swf, t, image[i].filename, (i * 2) + 1, 0);
693             for (j = 2; j <= global.imagecount; j++)
694                 t = MovieAddFrame(&swf, t, image[i].filename, (j * 2) - 1, j - 1);
695             free(image[i].filename);
696         }
697     }
698
699     MovieFinish(&swf, t, global.outfile);
700
701     return 0;
702 }