├── .gitignore ├── .npmignore ├── 22.gif ├── HISTORY.md ├── LICENSE.md ├── README.md ├── back.gif ├── bin └── gifify ├── example ├── index.js ├── movie.ass └── movie.mp4 ├── index.js ├── package.json ├── screencast.gif └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | example/movie.gif 2 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | screencast.gif 3 | 22.gif 4 | back.gif 5 | -------------------------------------------------------------------------------- /22.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/gifify/e4aff97fc3d31457a55ffa0d997ed78aac939efc/22.gif -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.2.0 (2016-11-03) 2 | 3 | * feat: add --no-loop avoids looping in GIF fixes #66 #68 4 | 5 | # 2.1.6 (2016-03-11) 6 | 7 | * fix: windows comes with fontconfig pre-installed in ImageMagick 8 | 9 | # 2.1.5 (2015-09-30) 10 | 11 | * fix: --speed now working (using gifsicle instead of convert) 12 | 13 | # 2.1.4 (2015-05-13) 14 | 15 | * reduce requiremenents string checks because on arch it's not libfontconfig but fontconfig 16 | 17 | # 2.1.3 (2015-04-24) 18 | 19 | * enhance requirements checks, #26 20 | 21 | # 2.1.2 (2015-04-07) 22 | 23 | * pin all dependencies, moment broke with moment-duration-format 24 | 25 | # 2.1.1 (2015-01-17) 26 | 27 | * fix `to` duration (do not substract `from` from `to`) 28 | 29 | # 2.1.0 (2015-01-11) 30 | 31 | * add --text 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # ISC License 2 | 3 | Copyright (c) 2014-2016 Vincent Voyer and contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE FROM AUTHOR** 2 | 3 | I am no more using this project or providing any support to it, if you want to maintain it, ping me here: vincent.voyer@gmail.com. 4 | 5 | This project was created 4 years ago, there might be better ways to turn a video into a GIF now, use google! 6 | 7 | **/NOTE FROM AUTHOR** 8 | 9 | # gifify 10 | 11 | Convert any video file to an optimized animated GIF. Either in its full length or only a part of it. 12 | 13 | ## Demo time 14 | 15 | ![screencast](screencast.gif) 16 | 17 | This screencast was recorded with [lolilolicon/FFcast](https://github.com/lolilolicon/FFcast) then converted to a GIF with: 18 | 19 | ```shell 20 | gifify screencast.mkv -o screencast.gif --resize 800:-1 21 | ``` 22 | 23 | ## Features 24 | 25 | - command line interface 26 | - programmatic JavaScript ([Node.JS](http://nodejs.org/)) [stream](http://nodejs.org/api/stream.html) interface 27 | - unix friendly, supports `stdin` & `stdout` 28 | - optimized! uses [pornel/giflossy](https://github.com/pornel/giflossy) to generate light GIFS 29 | - lots of options: movie speed, fps, colors, compression, resize, reverse, from & to, subtitles 30 | - no temp files used, everything happens in memory 31 | - fast! Extracting a 5-second GIF from the middle of a 2-hour movie takes less than 20 seconds 32 | 33 | ## Requirements 34 | 35 | Before using gifify, please install: 36 | 37 | - [Node.js](https://nodejs.org) (`$ brew install node`) 38 | - [FFmpeg](http://ffmpeg.org/) [🐓🐓🐓🐓](http://en.wikipedia.org/wiki/FFmpeg#History) (`$ brew install ffmpeg`) 39 | - [convert](http://www.imagemagick.org/script/convert.php), the famous [ImageMagick](http://www.imagemagick.org/) (`$ brew install imagemagick`) 40 | - [pornel/giflossy](https://github.com/pornel/giflossy/releases), it's a [gifsicle](http://www.lcdf.org/gifsicle/) fork (waiting for [gifsicle#16](https://github.com/kohler/gifsicle/pull/16) to be merged) (`$ brew install giflossy`) 41 | 42 | You can also use the [gifify Docker image](https://github.com/maxogden/gifify-docker) which comes with everything installed. 43 | 44 | ## Installation 45 | 46 | ```shell 47 | npm install -g gifify 48 | ``` 49 | 50 | ## Command line usage 51 | 52 | ```shell 53 | > gifify -h 54 | 55 | Usage: gifify [options] [file] 56 | 57 | Options: 58 | 59 | -h, --help output usage information 60 | -V, --version output the version number 61 | --colors Number of colors, up to 255, defaults to 80 62 | --compress Compression (quality) level, from 0 (no compression) to 100, defaults to 40 63 | --from Start position, hh:mm:ss or seconds, defaults to 0 64 | --fps Frames Per Second, defaults to 10 65 | -o, --output Output file, defaults to stdout 66 | --resize Resize output, use -1 when specifying only width or height. `350:100`, `400:-1`, `-1:200` 67 | --reverse Reverses movie 68 | --speed Movie speed, defaults to 1 69 | --subtitles Subtitle filepath to burn to the GIF 70 | --text Add some text at the bottom of the movie 71 | --to End position, hh:mm:ss or seconds, defaults to end of movie 72 | --no-loop Will show every frame once without looping 73 | ``` 74 | 75 | ## Programmatic usage 76 | 77 | See the [example](./example). 78 | 79 | ```js 80 | var fs = require('fs'); 81 | var gifify = require('gifify'); 82 | var path = require('path'); 83 | 84 | var input = path.join(__dirname, 'movie.mp4'); 85 | var output = path.join(__dirname, 'movie.gif'); 86 | 87 | var gif = fs.createWriteStream(output); 88 | 89 | var options = { 90 | resize: '200:-1', 91 | from: 30, 92 | to: 35 93 | }; 94 | 95 | gifify(input, options).pipe(gif); 96 | ``` 97 | 98 | You can also pass a [readable stream](http://nodejs.org/api/stream.html#stream_class_stream_readable) to `gifify(stream, opts)`. 99 | 100 | ## Readable stream input performance 101 | 102 | Gifify supports streams both on command line (`cat movie.mp4 | gifify -o out.gif`) and in the programmatic API (`gifify(readableStream, opts).pipe(writableStream)`). 103 | 104 | While it's super useful in some cases, if you have the file on disk already, you better do `gifify movie.mp4 -o out.gif` or `gifify(filePath, opts).pipe(writableStream)`. 105 | 106 | Why? Because piping 3.4GB when you want to cut from 40:20 to 40:22 still takes a loooooot of time and does not give you any performance benefit. 107 | 108 | FFmpeg has to read from 0GB -> $START_BYTE_40:20 and discards it. But everything flows in your memory. 109 | 110 | When using direct file input from command line, we pass the `-i filename` option to FFmpeg and then it's super fast! 111 | 112 | Be careful when `|piping`. 113 | 114 | ## Adding some text 115 | 116 | You can burn some simple text into your GIF: 117 | 118 | ```shell 119 | gifify back.mp4 -o back.gif --from 01:48:23.200 --to 01:48:25.300 --text "What?..What?What?" 120 | ``` 121 | 122 | Result: 123 | 124 | ![back](back.gif) 125 | 126 | ## Subtitles 127 | 128 | You can burn subtitles into your GIF, it's that easy: 129 | 130 | ```shell 131 | gifify 22.mkv -o movie.gif --subtitles 22.ass --from 1995 --to 2002 --resize 600:-1 132 | ``` 133 | 134 | You must create new subtitles files, the timecodes for the complete film will not work for a five seconds GIF. 135 | 136 | Create subtitles using [aegisub](http://www.aegisub.org/) and augment the font size for a great effect! 137 | 138 | Here's the `22.ass` from the previous command, created with aegisub: 139 | 140 | ```ass 141 | [Script Info] 142 | ; Script generated by Aegisub 3.2.1 143 | ; http://www.aegisub.org/ 144 | Title: Default Aegisub file 145 | ScriptType: v4.00+ 146 | WrapStyle: 0 147 | ScaledBorderAndShadow: yes 148 | YCbCr Matrix: None 149 | 150 | [Aegisub Project Garbage] 151 | 152 | [V4+ Styles] 153 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 154 | Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 155 | 156 | [Events] 157 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 158 | Dialogue: 0,0:00:02.50,0:00:03.97,Default,,0,0,0,,{\fnLiberation Sans\fs40}Okay, okay. 159 | Dialogue: 0,0:00:05.00,0:00:06.90,Default,,0,0,0,,{\fnLiberation Sans\fs40}Okay. Okay. 160 | ``` 161 | 162 | Result extracting a GIF from [22 Jump Street](http://www.imdb.com/title/tt2294449/): 163 | 164 | ![22](22.gif) 165 | 166 | ## GIF Performance 167 | 168 | ``` 169 | On modern hardware GIF is the slowest and most expensive video codec. Can we please allow it to be obsoleted? 170 | ``` 171 | 172 | https://pornel.net/efficient-gifs#sec44 173 | 174 | YOLO! 175 | 176 | ## Giflossy 177 | 178 | [Giflossy](https://github.com/pornel/giflossy) is a fork of [gifsicle](http://www.lcdf.org/gifsicle/), gifsicle author is currently [working on](https://github.com/kohler/gifsicle/tree/lossy) integrating the lossy part in gifsicle. 179 | 180 | So in little time we will be able to directly use gifsicle and gifiscle packages. 181 | 182 | ## Thanks 183 | 184 | [jclem/gifify](https://github.com/jclem/gifify/) was a great source of inspiration. 185 | -------------------------------------------------------------------------------- /back.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/gifify/e4aff97fc3d31457a55ffa0d997ed78aac939efc/back.gif -------------------------------------------------------------------------------- /bin/gifify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var program = require('commander'); 6 | var Promise = require('promise'); 7 | var exec = require('child_process').exec; 8 | var util = require('util'); 9 | var colors = require('colors'); 10 | 11 | var gifify = require('../'); 12 | 13 | var options = 14 | program 15 | .version(require('../package.json').version) 16 | .usage('[options] [file]') 17 | .option('--colors ', 'Number of colors, up to 255, defaults to 80', parseFloat, 80) 18 | .option('--compress ', 'Compression (quality) level, from 0 (no compression) to 100, defaults to 40', 40) 19 | .option('--from ', 'Start position, hh:mm:ss or seconds, defaults to 0') 20 | .option('--fps ', 'Frames Per Second, defaults to 10', parseFloat, 10) 21 | .option('-o, --output ', 'Output file, defaults to stdout') 22 | .option('--resize ', 'Resize output, use -1 when specifying only width or height. `350:100`, `400:-1`, `-1:200`') 23 | .option('--reverse', 'Reverses movie') 24 | .option('--speed ', 'Movie speed, defaults to 1', parseFloat, 1) 25 | .option('--subtitles ', 'Subtitle filepath to burn to the GIF') 26 | .option('--text ', 'Add some text at the bottom of the movie') 27 | .option('--to ', 'End position, hh:mm:ss or seconds, defaults to end of movie') 28 | .option('--no-loop', 'Will show every frame once without looping') 29 | .parse(process.argv); 30 | 31 | // we can only deal with one gif at a time 32 | if (options.args.length > 1) { 33 | program.help(); 34 | } 35 | 36 | // show help if user only typed "gifify" without piping to it or specifying an input file 37 | if (process.stdin.isTTY && options.args.length === 0) { 38 | program.help(); 39 | } 40 | 41 | // main program logic 42 | checkRequirements() 43 | .then(encode) 44 | .catch(abort); 45 | 46 | function abort(err) { 47 | console.error(err); 48 | process.exit(1); 49 | } 50 | 51 | function encode() { 52 | var outputStream; 53 | if (options.output !== undefined) { 54 | outputStream = fs.createWriteStream(path.resolve(process.cwd(), options.output)); 55 | } else { 56 | outputStream = process.stdout; 57 | } 58 | 59 | var input; 60 | 61 | if (options.args[0] === undefined) { 62 | // we got piped! 63 | input = process.stdin; 64 | } else { 65 | input = path.resolve(process.cwd(), options.args[0]); 66 | if (!fs.existsSync(input)) { 67 | abort(new Error('Could not find: ' + input)); 68 | return; 69 | } 70 | } 71 | 72 | if(outputStream !== process.stdout) { 73 | process.stdout.write("Generating GIF, please wait...\n"); 74 | } 75 | 76 | var gifStream = gifify(input, options); 77 | gifStream.pipe(outputStream); 78 | gifStream.on('error', abort.bind(null)); 79 | outputStream.on('error', abort.bind(null)); 80 | } 81 | 82 | function notInstalledMessage(command) { 83 | var readMe = "https://github.com/vvo/gifify#requirements"; 84 | return colors.red(util.format("Could not find %s. Is it installed?\n\nMore info: %s", command, readMe)); 85 | } 86 | 87 | function missingDependency(command, dependencies) { 88 | return colors.red(util.format("%s needs %s", command, dependencies.join(", "))); 89 | } 90 | 91 | // This will check both if the required applications are installed, and if they 92 | // are installed with the correct dependencies. For example, to make subtitles 93 | // work, ffmpeg needs to be compiled with `enable-libass`. 94 | function checkRequirements() { 95 | var required = [ 96 | {'bin': 'ffmpeg', 'args' : '-version', 'needs': ['libass', 'fontconfig']}, 97 | {'bin': 'gifsicle', 'args': '-h', 'needs': ['lossy']} 98 | ]; 99 | 100 | if(process.platform !== 'win32') { 101 | // disabled because new homebrew doesn't allow custom builds 102 | // required.push({'bin': 'convert', 'args': '--version', 'needs': ['fontconfig']}); 103 | } else { 104 | required.push({'bin': 'convert', 'args': '--version', 'needs': []}); 105 | } 106 | 107 | return Promise.all(required.map(checkRequirement)); 108 | } 109 | 110 | // Takes a `obj` which should have three keys available: 111 | // bin - The name of the binary to execute 112 | // args - The arguments that are passed to the binary 113 | // needs - Strings to look for in the output of `bin` fed `args` 114 | // 115 | // Returns a Promise 116 | function checkRequirement(obj) { 117 | return new Promise(function find(resolve, reject) { 118 | exec([obj.bin, obj.args].join(' '), function(error, stdout, stderr) { 119 | if (error) return reject(notInstalledMessage(obj.bin)); 120 | 121 | var ok = (obj.needs || []).every(function(req) { 122 | return new RegExp(req, 'i').test(stdout); 123 | }); 124 | if (ok) { 125 | return resolve(); 126 | } else { 127 | return reject(missingDependency(obj.bin, obj.needs)); 128 | } 129 | }); 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gifify = require('../'); 3 | var path = require('path'); 4 | 5 | var input = path.join(__dirname, 'movie.mp4'); 6 | var output = path.join(__dirname, 'movie.gif'); 7 | 8 | var gif = fs.createWriteStream(output); 9 | 10 | var options = { 11 | resize: '500:-1', 12 | from: 30, 13 | to: 35, 14 | subtitles: path.join(__dirname, 'movie.ass') 15 | }; 16 | 17 | gifify(input, options).pipe(gif); 18 | 19 | gif.on('close', function end() { 20 | console.log('gifified ' + input + ' to ' + output); 21 | }); 22 | -------------------------------------------------------------------------------- /example/movie.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by Aegisub 3.2.1 3 | ; http://www.aegisub.org/ 4 | Title: Default Aegisub file 5 | ScriptType: v4.00+ 6 | WrapStyle: 0 7 | ScaledBorderAndShadow: yes 8 | YCbCr Matrix: None 9 | 10 | [Aegisub Project Garbage] 11 | 12 | [V4+ Styles] 13 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 14 | Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 15 | 16 | [Events] 17 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 18 | Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{\i1\fnLiberation Sans\fs26}water noise 19 | -------------------------------------------------------------------------------- /example/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/gifify/e4aff97fc3d31457a55ffa0d997ed78aac939efc/example/movie.mp4 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var duration = require('moment').duration; 2 | var spawn = require('child_process').spawn; 3 | 4 | require('moment-duration-format'); 5 | 6 | var debug = require('debug')('gifify'); 7 | 8 | module.exports = gifify; 9 | 10 | function gifify(streamOrFile, opts) { 11 | opts = opts || {}; 12 | if (typeof streamOrFile === 'string') { 13 | opts.inputFilePath = streamOrFile; 14 | } 15 | 16 | if (opts.fps === undefined) { 17 | opts.fps = 10; 18 | } 19 | 20 | if (opts.speed === undefined) { 21 | opts.speed = 1; 22 | } 23 | 24 | if (opts.colors === undefined) { 25 | opts.colors = 80; 26 | } 27 | 28 | if (opts.compress === undefined) { 29 | opts.compress = 40; 30 | } 31 | 32 | if (opts.from !== undefined && typeof opts.from === 'number' || 33 | typeof opts.from === 'string' && opts.from.indexOf(':') === -1) { 34 | opts.from = parseFloat(opts.from) * 1000; 35 | } 36 | 37 | if (opts.to !== undefined && typeof opts.to === 'number' || 38 | typeof opts.to === 'string' && opts.to.indexOf(':') === -1) { 39 | opts.to = parseFloat(opts.to) * 1000; 40 | } 41 | 42 | var ffmpegArgs = computeFFmpegArgs(opts); 43 | var convertArgs = computeConvertArgs(opts); 44 | var gifsicleArgs = computeGifsicleArgs(opts); 45 | 46 | var ffmpeg = spawn('ffmpeg', ffmpegArgs); 47 | var convert = spawn('convert', convertArgs); 48 | var gifsicle = spawn('gifsicle', gifsicleArgs); 49 | 50 | [ffmpeg, convert, gifsicle].forEach(function handleErrors(child) { 51 | child.on('error', gifsicle.emit.bind(gifsicle, 'error')); 52 | child.stderr.on('data', function gotSomeErrors(buf) { 53 | // emit errors on the resolved stream 54 | gifsicle.stdout.emit('error', buf.toString()); 55 | }); 56 | }); 57 | 58 | // https://github.com/joyent/node/issues/8652 59 | ffmpeg.stdin.on('error', function ignoreStdinError(){}); 60 | 61 | // ffmpeg.stdout.on('error', function() {}) 62 | // convert.stdin.on('error', function(){}); 63 | // convert.stdout.on('error', function() {}); 64 | // gifsicle.stdin.on('error', function() {}); 65 | // gifsicle.stdout.on('error', function() {}) 66 | 67 | if (!opts.inputFilePath) { 68 | streamOrFile.pipe(ffmpeg.stdin); 69 | } 70 | 71 | ffmpeg.stdout.pipe(convert.stdin); 72 | convert.stdout.pipe(gifsicle.stdin); 73 | return gifsicle.stdout; 74 | } 75 | 76 | function computeFFmpegArgs(opts) { 77 | var FFmpegTimeFormat = 'hh:mm:ss.SSS'; 78 | 79 | // FFmpeg options 80 | // https://www.ffmpeg.org/ffmpeg.html#Options 81 | var args = [ 82 | '-loglevel', 'panic' 83 | ]; 84 | 85 | // fast seek to opts.from - 500ms 86 | // see http://superuser.com/a/704118/35651 87 | if (opts.from !== undefined) { 88 | args.push('-ss', duration(opts.from).format(FFmpegTimeFormat, {trim: false})); 89 | } 90 | 91 | if (opts.inputFilePath) { 92 | args.push('-i', opts.inputFilePath); 93 | } else { 94 | // stdin as input 95 | // https://www.ffmpeg.org/ffmpeg-protocols.html#pipe 96 | args.push('-i', 'pipe:0'); 97 | } 98 | 99 | if (opts.to !== undefined) { 100 | args.push('-to', duration(opts.to).format(FFmpegTimeFormat, {trim: false})); 101 | } 102 | 103 | // framerate 104 | args.push('-r', opts.fps); 105 | 106 | if (opts.resize || opts.subtitles || opts.reverse) { 107 | // filters 108 | args.push('-vf'); 109 | 110 | var filters = []; 111 | // resize filter 112 | if (opts.resize) { 113 | filters.push('scale=' + opts.resize); 114 | } 115 | 116 | if (opts.subtitles !== undefined) { 117 | filters.push('subtitles=' + opts.subtitles); 118 | } 119 | 120 | if (opts.reverse !== undefined) { 121 | filters.push('reverse'); 122 | } 123 | 124 | args.push(filters.join(',')); 125 | } 126 | 127 | // encoding filter and codec 128 | args.push('-f', 'image2pipe', '-vcodec', 'ppm'); 129 | 130 | // force video sync so that even if nothing moves in the video, we get a constant frame rate 131 | // seems buggy, not what I want, still, some videos are failing to encode 132 | // args.push('-vsync', '1'); 133 | 134 | // write on stdout 135 | args.push('pipe:1'); 136 | 137 | debug('ffmpeg args: %j', args); 138 | 139 | return args; 140 | } 141 | 142 | function computeConvertArgs(opts) { 143 | // Convert options 144 | // http://www.imagemagick.rg/script/convert.php#options 145 | var args = [ 146 | '-', 147 | '+dither', 148 | '-layers', 'Optimize' 149 | ]; 150 | 151 | if (opts.text) { 152 | args.push( 153 | '-gravity', 'South', 154 | '-fill', 'white', 155 | '-stroke', 'black', 156 | '-strokewidth', '1', 157 | '-pointsize', '40', 158 | '-annotate', '+20+20', opts.text 159 | ); 160 | } 161 | 162 | args.push('gif:-'); 163 | 164 | debug('convert args: %j', args); 165 | 166 | return args; 167 | } 168 | 169 | function computeGifsicleArgs(opts) { 170 | // Gifsicle options 171 | // http://www.lcdf.org/gifsicle/man.html 172 | // --lossy is not yet into master, https://github.com/kohler/gifsicle/pull/16 173 | var args = [ 174 | '-O3', 175 | '--lossy=' + opts.compress * 2, 176 | '--colors=' + opts.colors, 177 | '--delay', Math.round(100 / opts.fps / opts.speed), 178 | '--no-warnings' 179 | ]; 180 | 181 | if (opts.loop === false) { 182 | args.push('--no-loopcount'); 183 | } 184 | 185 | debug('gifsicle args: %j', args); 186 | 187 | return args; 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gifify", 3 | "version": "2.4.3", 4 | "description": "Convert any video file to an optimized animated GIF", 5 | "main": "index.js", 6 | "bin": { 7 | "gifify": "bin/gifify" 8 | }, 9 | "scripts": { 10 | "example": "node ./example/index" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/vvo/gifify.git" 15 | }, 16 | "author": "Vincent Voyer ", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/vvo/gifify/issues" 20 | }, 21 | "homepage": "https://github.com/vvo/gifify", 22 | "dependencies": { 23 | "colors": "^1.1.2", 24 | "commander": "^2.9.0", 25 | "debug": "^2.5.2", 26 | "moment": "^2.17.1", 27 | "moment-duration-format": "^1.3.0", 28 | "promise": "^7.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvo/gifify/e4aff97fc3d31457a55ffa0d997ed78aac939efc/screencast.gif -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | asap@~2.0.3: 6 | version "2.0.5" 7 | resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" 8 | 9 | colors@^1.1.2: 10 | version "1.1.2" 11 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 12 | 13 | commander@^2.9.0: 14 | version "2.9.0" 15 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 16 | dependencies: 17 | graceful-readlink ">= 1.0.0" 18 | 19 | debug@^2.5.2: 20 | version "2.6.3" 21 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" 22 | dependencies: 23 | ms "0.7.2" 24 | 25 | "graceful-readlink@>= 1.0.0": 26 | version "1.0.1" 27 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 28 | 29 | moment-duration-format@^1.3.0: 30 | version "1.3.0" 31 | resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-1.3.0.tgz#541771b5f87a049cc65540475d3ad966737d6908" 32 | 33 | moment@^2.17.1: 34 | version "2.18.1" 35 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" 36 | 37 | ms@0.7.2: 38 | version "0.7.2" 39 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 40 | 41 | promise@^7.1.1: 42 | version "7.1.1" 43 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" 44 | dependencies: 45 | asap "~2.0.3" 46 | --------------------------------------------------------------------------------