1 /*
2  * Copyright (C) 2014 haru <uobikiemukot at gmail dot com>
3  * Copyright (C) 2014 Hayaki Saito <user@zuse.jp>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "config.h"
20 #include "yaft.h"
21 #include "util.h"
22 #include "pseudo.h"
23 #include "terminal.h"
24 #include "function.h"
25 #include "dcs.h"
26 #include "parse.h"
27 #include "gifsave89.h"
28 
29 #include <stdio.h>
30 
31 #if HAVE_STDLIB_H
32 # include <stdlib.h>
33 #endif
34 #if HAVE_STRING_H
35 # include <string.h>
36 #endif
37 #if HAVE_UNISTD_H
38 # include <unistd.h>
39 #endif
40 #if HAVE_GETOPT_H
41 # include <getopt.h>
42 #endif
43 #if HAVE_ERRNO_H
44 # include <errno.h>
45 #endif
46 #if HAVE_FCNTL_H
47 # include <fcntl.h>
48 #endif
49 
50 
51 #if !defined(HAVE_MEMCPY)
52 # define memcpy(d, s, n) (bcopy ((s), (d), (n)))
53 #endif
54 
55 #if !defined(O_BINARY) && defined(_O_BINARY)
56 # define O_BINARY _O_BINARY
57 #endif  /* !defined(O_BINARY) && !defined(_O_BINARY) */
58 
59 struct settings_t {
60     int width;
61     int height;
62     int show_version;
63     int show_help;
64     int last_frame_delay;
65     int foreground_color;
66     int background_color;
67     int cursor_color;
68     int tabwidth;
69     int cjkwidth;
70     int repeat;
71     char *input;
72     char *output;
73 };
74 
75 enum cmap_bitfield {
76     RED_SHIFT   = 5,
77     GREEN_SHIFT = 2,
78     BLUE_SHIFT  = 0,
79     RED_MASK    = 3,
80     GREEN_MASK  = 3,
81     BLUE_MASK   = 2
82 };
83 
pb_init(struct pseudobuffer * pb,int width,int height)84 static void pb_init(struct pseudobuffer *pb, int width, int height)
85 {
86     pb->width  = width;
87     pb->height = height;
88     pb->bytes_per_pixel = BYTES_PER_PIXEL;
89     pb->line_length = pb->width * pb->bytes_per_pixel;
90     pb->buf = ecalloc(pb->width * pb->height, pb->bytes_per_pixel);
91 }
92 
pb_die(struct pseudobuffer * pb)93 static void pb_die(struct pseudobuffer *pb)
94 {
95     free(pb->buf);
96 }
97 
set_colormap(int colormap[COLORS * BYTES_PER_PIXEL+1])98 static void set_colormap(int colormap[COLORS * BYTES_PER_PIXEL + 1])
99 {
100     int i, ci, r, g, b;
101     uint8_t index;
102 
103     /* colormap: terminal 256color
104     for (i = 0; i < COLORS; i++) {
105         ci = i * BYTES_PER_PIXEL;
106 
107         r = (color_list[i] >> 16) & bit_mask[8];
108         g = (color_list[i] >> 8)  & bit_mask[8];
109         b = (color_list[i] >> 0)  & bit_mask[8];
110 
111         colormap[ci + 0] = r;
112         colormap[ci + 1] = g;
113         colormap[ci + 2] = b;
114     }
115     */
116 
117     /* colormap: red/green: 3bit blue: 2bit
118     */
119     for (i = 0; i < COLORS; i++) {
120         index = (uint8_t) i;
121         ci = i * BYTES_PER_PIXEL;
122 
123         r = (index >> RED_SHIFT)   & bit_mask[RED_MASK];
124         g = (index >> GREEN_SHIFT) & bit_mask[GREEN_MASK];
125         b = (index >> BLUE_SHIFT)  & bit_mask[BLUE_MASK];
126 
127         colormap[ci + 0] = r * bit_mask[BITS_PER_BYTE] / bit_mask[RED_MASK];
128         colormap[ci + 1] = g * bit_mask[BITS_PER_BYTE] / bit_mask[GREEN_MASK];
129         colormap[ci + 2] = b * bit_mask[BITS_PER_BYTE] / bit_mask[BLUE_MASK];
130     }
131     colormap[COLORS * BYTES_PER_PIXEL] = -1;
132 }
133 
pixel2index(uint32_t pixel)134 static uint32_t pixel2index(uint32_t pixel)
135 {
136     /* pixel is always 24bpp */
137     uint32_t r, g, b;
138 
139     /* split r, g, b bits */
140     r = (pixel >> 16) & bit_mask[8];
141     g = (pixel >> 8)  & bit_mask[8];
142     b = (pixel >> 0)  & bit_mask[8];
143 
144     /* colormap: terminal 256color
145     if (r == g && r == b) { // 24 gray scale
146         r = 24 * r / COLORS;
147         return 232 + r;
148     }                       // 6x6x6 color cube
149 
150     r = 6 * r / COLORS;
151     g = 6 * g / COLORS;
152     b = 6 * b / COLORS;
153 
154     return 16 + (r * 36) + (g * 6) + b;
155     */
156 
157     /* colormap: red/green: 3bit blue: 2bit
158     */
159     // get MSB ..._MASK bits
160     r = (r >> (8 - RED_MASK))   & bit_mask[RED_MASK];
161     g = (g >> (8 - GREEN_MASK)) & bit_mask[GREEN_MASK];
162     b = (b >> (8 - BLUE_MASK))  & bit_mask[BLUE_MASK];
163 
164     return (r << RED_SHIFT) | (g << GREEN_SHIFT) | (b << BLUE_SHIFT);
165 }
166 
apply_colormap(struct pseudobuffer * pb,unsigned char * img)167 static void apply_colormap(struct pseudobuffer *pb, unsigned char *img)
168 {
169     int w, h;
170     uint32_t pixel = 0;
171 
172     for (h = 0; h < pb->height; h++) {
173         for (w = 0; w < pb->width; w++) {
174             memcpy(&pixel, pb->buf + h * pb->line_length
175                 + w * pb->bytes_per_pixel, pb->bytes_per_pixel);
176             *(img + h * pb->width + w) = pixel2index(pixel) & bit_mask[BITS_PER_BYTE];
177         }
178     }
179 }
180 
write_gif(unsigned char * gifimage,int size,FILE * f)181 static size_t write_gif(unsigned char *gifimage, int size, FILE *f)
182 {
183     size_t wsize = 0;
184 
185     wsize = fwrite(gifimage, sizeof(unsigned char), size, f);
186     return wsize;
187 }
188 
show_version()189 static void show_version()
190 {
191     printf(PACKAGE_NAME " " PACKAGE_VERSION "\n"
192            "Copyright (C) 2014 haru <uobikiemukot at gmail dot com>\n"
193            "Copyright (C) 2012-2014 Hayaki Saito <user@zuse.jp>.\n"
194            "\n"
195            "This program is free software; you can redistribute it and/or modify\n"
196            "it under the terms of the GNU General Public License as published by\n"
197            "the Free Software Foundation; either version 3 of the License, or\n"
198            "(at your option) any later version.\n"
199            "\n"
200            "This program is distributed in the hope that it will be useful,\n"
201            "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
202            "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
203            "GNU General Public License for more details.\n"
204            "\n"
205            "You should have received a copy of the GNU General Public License\n"
206            "along with this program. If not, see http://www.gnu.org/licenses/.\n"
207            "%s\n", copyright
208           );
209 }
210 
show_help()211 static void show_help()
212 {
213     fprintf(stderr,
214             "Usage: seq2gif [Options] < ttyrecord > record.gif\n"
215             "       seq2gif [Options] -i ttyrecord -o record.gif\n"
216             "\n"
217             "Options:\n"
218             "-w WIDTH, --width=WIDTH               specify terminal width in cell size.\n"
219             "                                      (default: 80)\n"
220             "-h HEIGHT, --height=HEIGHT            specify terminal height in cell size.\n"
221             "                                      (default: 24)\n"
222             "-l DELAY, --last-frame-delay=DELAY    specify delay in msec which is added\n"
223             "                                      to the last frame. (default: 300)\n"
224             "-f COLORNO --foreground-color=COLORNO specify foreground color palette.\n"
225             "                                      number.\n"
226             "-b COLORNO --background-color=COLORNO specify background color palette\n"
227             "                                      number.\n"
228             "-c COLORNO --cursor-color=COLORNO     specify cursor color palette\n"
229             "                                      number.\n"
230             "-t TABSTOP --tabstop=TABSTOP          specify hardware tabstop(default: 8)\n"
231             "-j --cjkwidth                         treat East Asian Ambiguous width\n"
232             "                                      characters (UAX#11) as wide.\n"
233             "-r COUNT --repeat=COUNT               specify animation repeat count. loop\n"
234             "                                      infinitely if 0 is given. (default: 0)\n"
235             "-i FILE --input=FILE                  specify input file name. use STDIN\n"
236             "                                      if '-' is given. (default: '-')\n"
237             "-o FILE --output=FILE                 specify output file name. use STDOUT\n"
238             "                                      if '-' is given. (default: '-')\n"
239             "-V, --version                         show version and license information.\n"
240             "-H, --help                            show this help.\n"
241            );
242 }
243 
parse_args(int argc,char * argv[],struct settings_t * psettings)244 static int parse_args(int argc, char *argv[], struct settings_t *psettings)
245 {
246     int long_opt;
247     int n;
248     char const *optstring = "w:h:HVl:f:b:c:t:jr:i:o:";
249 #if HAVE_GETOPT_LONG
250     int option_index;
251 #endif  /* HAVE_GETOPT_LONG */
252 
253 #if HAVE_GETOPT_LONG
254     struct option long_options[] = {
255         {"width",             required_argument,  &long_opt, 'w'},
256         {"height",            required_argument,  &long_opt, 'h'},
257         {"last-frame-delay",  required_argument,  &long_opt, 'l'},
258         {"foreground-color",  required_argument,  &long_opt, 'f'},
259         {"background-color",  required_argument,  &long_opt, 'b'},
260         {"cursor-color",      required_argument,  &long_opt, 'c'},
261         {"tabstop",           required_argument,  &long_opt, 't'},
262         {"cjkwidth",          no_argument,        &long_opt, 'j'},
263         {"repeat",            required_argument,  &long_opt, 'r'},
264         {"input",             required_argument,  &long_opt, 'i'},
265         {"output",            required_argument,  &long_opt, 'o'},
266         {"help",              no_argument,        &long_opt, 'H'},
267         {"version",           no_argument,        &long_opt, 'V'},
268         {0, 0, 0, 0}
269     };
270 #endif  /* HAVE_GETOPT_LONG */
271 
272     for (;;) {
273 
274 #if HAVE_GETOPT_LONG
275         n = getopt_long(argc, argv, optstring,
276                         long_options, &option_index);
277 #else
278         n = getopt(argc, argv, optstring);
279 #endif  /* HAVE_GETOPT_LONG */
280         if (n == -1) {
281             break;
282         }
283         if (n == 0) {
284             n = long_opt;
285         }
286         switch(n) {
287         case 'w':
288             psettings->width = atoi(optarg);
289             if (psettings->width < 1) {
290                 goto argerr;
291             }
292             break;
293         case 'h':
294             psettings->height = atoi(optarg);
295             if (psettings->height < 1) {
296                 goto argerr;
297             }
298             break;
299         case 'l':
300             psettings->last_frame_delay = atoi(optarg);
301             if (psettings->last_frame_delay < 0) {
302                 goto argerr;
303             }
304             break;
305         case 'f':
306             psettings->foreground_color = atoi(optarg);
307             if (psettings->foreground_color < 0) {
308                 goto argerr;
309             }
310             if (psettings->foreground_color > 255) {
311                 goto argerr;
312             }
313             break;
314         case 'b':
315             psettings->background_color = atoi(optarg);
316             if (psettings->background_color < 0) {
317                 goto argerr;
318             }
319             if (psettings->background_color > 255) {
320                 goto argerr;
321             }
322             break;
323         case 'c':
324             psettings->cursor_color = atoi(optarg);
325             if (psettings->cursor_color < 0) {
326                 goto argerr;
327             }
328             if (psettings->cursor_color > 255) {
329                 goto argerr;
330             }
331             break;
332         case 't':
333             psettings->tabwidth = atoi(optarg);
334             if (psettings->tabwidth < 0) {
335                 goto argerr;
336             }
337             if (psettings->tabwidth > 255) {
338                 goto argerr;
339             }
340             break;
341         case 'j':
342             psettings->cjkwidth = 1;
343             break;
344         case 'r':
345             psettings->repeat = atoi(optarg);
346             if (psettings->repeat < 0) {
347                 goto argerr;
348             }
349             if (psettings->repeat > 0xffff) {
350                 goto argerr;
351             }
352             break;
353         case 'i':
354             psettings->input = strdup(optarg);
355             break;
356         case 'o':
357             psettings->output = strdup(optarg);
358             break;
359         case 'H':
360             psettings->show_help = 1;
361             break;
362         case 'V':
363             psettings->show_version = 1;
364             break;
365         default:
366             goto argerr;
367         }
368     }
369     return 0;
370 
371 argerr:
372     show_help();
373     return 1;
374 }
375 
open_input_file(char const * filename)376 static FILE * open_input_file(char const *filename)
377 {
378     FILE *f;
379 
380     if (filename == NULL || strcmp(filename, "-") == 0) {
381         /* for windows */
382 #if defined(O_BINARY)
383 # if HAVE__SETMODE
384         _setmode(fileno(stdin), O_BINARY);
385 # elif HAVE_SETMODE
386         setmode(fileno(stdin), O_BINARY);
387 # endif  /* HAVE_SETMODE */
388 #endif  /* defined(O_BINARY) */
389         return stdin;
390     }
391     f = fopen(filename, "rb");
392     if (!f) {
393 #if _ERRNO_H
394         fprintf(stderr, "fopen('%s') failed.\n" "reason: %s.\n",
395                 filename, strerror(errno));
396 #endif  /* HAVE_ERRNO_H */
397         return NULL;
398     }
399     return f;
400 }
401 
open_output_file(char const * filename)402 static FILE * open_output_file(char const *filename)
403 {
404     FILE *f;
405 
406     if (filename == NULL || strcmp(filename, "-") == 0) {
407         /* for windows */
408 #if defined(O_BINARY)
409 # if HAVE__SETMODE
410         _setmode(fileno(stdout), O_BINARY);
411 # elif HAVE_SETMODE
412         setmode(fileno(stdout), O_BINARY);
413 # endif  /* HAVE_SETMODE */
414 #endif  /* defined(O_BINARY) */
415         return stdout;
416     }
417     f = fopen(filename, "wb");
418     if (!f) {
419 #if _ERRNO_H
420         fprintf(stderr, "fopen('%s') failed.\n" "reason: %s.\n",
421                 filename, strerror(errno));
422 #endif  /* HAVE_ERRNO_H */
423         return NULL;
424     }
425     return f;
426 }
427 
readtime(FILE * f,uint8_t * obuf)428 static int32_t readtime(FILE *f, uint8_t *obuf)
429 {
430     int nread;
431     int32_t tv_sec;
432     int32_t tv_usec;
433 
434     nread = fread(obuf, 1, sizeof(tv_sec), f);
435     if (nread != sizeof(tv_sec)) {
436         return -1;
437     }
438     tv_sec = obuf[0] | obuf[1] << 8
439            | obuf[2] << 16 | obuf[3] << 24;
440     nread = fread(obuf, 1, sizeof(tv_usec), f);
441     if (nread != sizeof(tv_usec)) {
442         return -1;
443     }
444     tv_usec = obuf[0] | obuf[1] << 8
445             | obuf[2] << 16 | obuf[3] << 24;
446 
447     return tv_sec * 1000000 + tv_usec;
448 }
449 
readlen(FILE * f,uint8_t * obuf)450 static int32_t readlen(FILE *f, uint8_t *obuf)
451 {
452     int nread;
453     uint32_t len;
454 
455     nread = fread(obuf, 1, sizeof(len), f);
456     if (nread != sizeof(len)) {
457         return -1;
458     }
459     len = obuf[0]
460         | obuf[1] << 8
461         | obuf[2] << 16
462         | obuf[3] << 24;
463 
464     return len;
465 }
466 
main(int argc,char * argv[])467 int main(int argc, char *argv[])
468 {
469     uint8_t *obuf;
470     ssize_t nread;
471     struct terminal term;
472     struct pseudobuffer pb;
473     uint32_t prev = 0;
474     uint32_t now = 0;
475     ssize_t len = 0;
476     size_t maxlen = 0;
477     int delay = 0;
478     int dirty = 0;
479     FILE *in_file = NULL;
480     FILE *out_file = NULL;
481     int nret = EXIT_SUCCESS;
482 
483     void *gsdata;
484     unsigned char *gifimage = NULL;
485     int gifsize, colormap[COLORS * BYTES_PER_PIXEL + 1];
486     unsigned char *img;
487 
488     struct settings_t settings = {
489         80,     /* width */
490         24,     /* height */
491         0,      /* show_version */
492         0,      /* show_help */
493         300,    /* last_frame_delay */
494         7,      /* foreground_color */
495         0,      /* background_color */
496         2,      /* cursor_color */
497         8,      /* tabwidth */
498         0,      /* cjkwidth */
499         0,      /* repeat */
500         NULL,   /* input */
501         NULL,   /* output */
502     };
503 
504     if (parse_args(argc, argv, &settings) != 0) {
505         exit(1);
506     }
507 
508     if (settings.show_help) {
509         show_help();
510         exit(0);
511     }
512 
513     if (settings.show_version) {
514         show_version();
515         exit(0);
516     }
517 
518     /* init */
519     pb_init(&pb, settings.width * CELL_WIDTH, settings.height * CELL_HEIGHT);
520     term_init(&term, pb.width, pb.height,
521               settings.foreground_color,
522               settings.background_color,
523               settings.cursor_color,
524               settings.tabwidth,
525               settings.cjkwidth);
526 
527     /* init gif */
528     img = (unsigned char *) ecalloc(pb.width * pb.height, 1);
529     set_colormap(colormap);
530     if (!(gsdata = newgif((void **) &gifimage, pb.width, pb.height, colormap, 0))) {
531         free(img);
532         term_die(&term);
533         pb_die(&pb);
534         return EXIT_FAILURE;
535     }
536 
537     animategif(gsdata, /* repetitions */ settings.repeat, 0,
538         /* transparent background */  -1, /* disposal */ 2);
539 
540     in_file = open_input_file(settings.input);
541     out_file = open_output_file(settings.output);
542 
543     maxlen = 2048;
544     obuf = malloc(maxlen);
545     prev = now = readtime(in_file, obuf);
546 
547     /* main loop */
548     for(;;) {
549         len = readlen(in_file, obuf);
550         if (len <= 0) {
551             nret = EXIT_FAILURE;
552             break;
553         }
554         if (len > maxlen) {
555             obuf = realloc(obuf, len);
556             maxlen = len;
557         }
558         nread = fread(obuf, 1, len, in_file);
559         if (nread != len) {
560             nret = EXIT_FAILURE;
561             break;
562         }
563         parse(&term, obuf, nread, &dirty);
564         now = readtime(in_file, obuf);
565         if (now == -1) {
566             break;
567         }
568         if (term.esc.state != STATE_DCS || dirty) {
569             refresh(&pb, &term);
570             delay = now - prev;
571 
572             /* take screenshot */
573             apply_colormap(&pb, img);
574             controlgif(gsdata, -1, delay / 10000, 0, 0);
575             prev = now;
576             putgif(gsdata, img);
577         }
578         dirty = 0;
579     }
580     if (settings.last_frame_delay > 0) {
581         controlgif(gsdata, -1, settings.last_frame_delay / 10, 0, 0);
582         putgif(gsdata, img);
583     }
584 
585     /* output gif */
586     gifsize = endgif(gsdata);
587     if (gifsize > 0) {
588         write_gif(gifimage, gifsize, out_file);
589         free(gifimage);
590     }
591 
592     /* normal exit */
593     free(img);
594     if (settings.input) {
595         if (fileno(in_file) != STDIN_FILENO) {
596             fclose(in_file);
597         }
598         free(settings.input);
599     }
600     if (settings.output) {
601         if (fileno(out_file) != STDOUT_FILENO) {
602             fclose(out_file);
603         }
604         free(settings.output);
605     }
606     term_die(&term);
607     pb_die(&pb);
608     free(obuf);
609 
610     return nret;
611 }
612 
613 /* emacs, -*- Mode: C; tab-width: 4; indent-tabs-mode: nil -*- */
614 /* vim: set expandtab ts=4 : */
615 /* EOF */
616