├── .gitignore ├── index.js ├── circle.yml ├── test ├── fixtures │ ├── video.mp4 │ └── image.jpeg ├── test.js └── thumbbot.test.js ├── Makefile ├── lib ├── util.js ├── render.js └── thumbbot.js ├── package.json └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/thumbbot'); 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.11.14 4 | -------------------------------------------------------------------------------- /test/fixtures/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/thumbbot/HEAD/test/fixtures/video.mp4 -------------------------------------------------------------------------------- /test/fixtures/image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/thumbbot/HEAD/test/fixtures/image.jpeg -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('6to5/register')({ 2 | blacklist: ['generators'] 3 | }); 4 | 5 | require('./thumbbot.test'); 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard lib/*.js) 2 | DEST = $(SRC:lib/%.js=build/%.js) 3 | 4 | build: $(DEST) 5 | build/%.js: lib/%.js 6 | mkdir -p $(@D) 7 | ./node_modules/.bin/6to5 -b generators $< -o $@ 8 | 9 | clean: 10 | rm -rf build 11 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module exports 3 | */ 4 | 5 | var exports = module.exports; 6 | 7 | exports.clone = clone; 8 | exports.tmpPath = tmpPath; 9 | exports.parseExtension = parseExtension; 10 | 11 | /** 12 | * Utilities 13 | */ 14 | 15 | function clone (obj) { 16 | return JSON.parse(JSON.stringify(obj)); 17 | } 18 | 19 | function tmpPath (format) { 20 | if (!format) format = 'jpg'; 21 | 22 | var filename = Math.random().toString(36).substring(7); 23 | 24 | return '/tmp/' + filename + '.' + format; 25 | } 26 | 27 | function parseExtension (path) { 28 | return /\.([a-z0-9]{3,5})$/i.exec(path)[1]; 29 | } -------------------------------------------------------------------------------- /lib/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var system = require('system'); 6 | 7 | 8 | // arguments 9 | var params = JSON.parse(system.args[1]); 10 | 11 | var page = require('webpage').create(); 12 | 13 | // features 14 | var window = params.window; 15 | var disable = params.disable; 16 | var crop = params.crop; 17 | 18 | if (window) { 19 | page.viewportSize = params.window; 20 | } 21 | 22 | if (crop) { 23 | page.clipRect = { 24 | top: crop.y, 25 | left: crop.x, 26 | width: crop.width, 27 | height: crop.height 28 | }; 29 | } 30 | 31 | if (disable) { 32 | if (disable.javascript) { 33 | page.settings.javascriptEnabled = false; 34 | } 35 | 36 | if (disable.images) { 37 | page.settings.loadImages = false; 38 | } 39 | } 40 | 41 | var url = decodeURIComponent(params.url); 42 | page.open(url, function() { 43 | page.render(params.destPath); 44 | phantom.exit(); 45 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thumbbot", 3 | "version": "0.4.1", 4 | "description": "Thumbnails for video, images and web pages.", 5 | "keywords": [ 6 | "thumb", 7 | "thumbnail", 8 | "video", 9 | "image", 10 | "imagemagick", 11 | "magick", 12 | "ffmpeg", 13 | "phantom", 14 | "phantomjs", 15 | "resize", 16 | "crop", 17 | "co-image", 18 | "co-thumbnail", 19 | "co-video", 20 | "co", 21 | "es6" 22 | ], 23 | "author": "Vadim Demedes ", 24 | "main": "./index.js", 25 | "scripts": { 26 | "test": "./node_modules/.bin/mocha --harmony test/test.js" 27 | }, 28 | "license": "MIT", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/vdemedes/thumbbot" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/vdemedes/thumbbot/issues" 35 | }, 36 | "engines": { 37 | "node": ">= 0.11.x" 38 | }, 39 | "dependencies": { 40 | "co-exec": "^1.1.0", 41 | "co-request": "^0.2.0", 42 | "magician": "^0.2.0" 43 | }, 44 | "devDependencies": { 45 | "6to5": "^2.7.2", 46 | "chai": "^1.10.0", 47 | "co": "^4.0.0", 48 | "mocha": "^2.0.1", 49 | "mocha-generators": "^1.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Thumbbot 2 | 3 | Create thumbnails from images, videos and web pages. 4 | 5 | [![Circle CI](https://circleci.com/gh/vdemedes/thumbbot.svg?style=svg)](https://circleci.com/gh/vdemedes/thumbbot) 6 | 7 | ## Installation 8 | 9 | ```npm install thumbbot --save``` 10 | 11 | ## Requirements 12 | 13 | - PhantomJS - web page snapshots 14 | - ImageMagick - image thumbnails 15 | - ffmpeg - video snapshots 16 | 17 | ## Usage 18 | 19 | ```javascript 20 | var Thumbbot = require('thumbbot'); 21 | ``` 22 | 23 | ### Images 24 | 25 | #### Resize 26 | 27 | ```javascript 28 | var image = new Thumbbot('image.png'); 29 | image.resize(200, 200); // width, height 30 | 31 | // or 32 | 33 | image.width(200); 34 | .height(200); 35 | 36 | var thumbnail = yield image.save(); 37 | ``` 38 | 39 | #### Crop 40 | 41 | ```javascript 42 | var image = new Thumbbot('image.png'); 43 | image.crop(0, 0, 200, 200); // x, y, width, height 44 | 45 | var thumbnail = yield image.save(); 46 | ``` 47 | 48 | ### Videos 49 | 50 | ```javascript 51 | var video = new Thumbbot('video.mp4'); 52 | video.seek('00:01:24'); // take a snapshot at 01:24 53 | 54 | var thumbnail = yield video.save(); 55 | ``` 56 | 57 | ### Web pages 58 | 59 | ```javascript 60 | var page = new Thumbbot('http://smashingmagazine.com'); 61 | page.window(1024, 768) // specify browser window size, optional 62 | .crop(100, 100, 400, 400) // specify an area to capture, x, y, width & height, optional 63 | .disable('javascript') // disable javascript, optional 64 | .disable('images'); // disable loading images, optional 65 | 66 | var thumbnail = yield page.save(); 67 | ``` 68 | 69 | ## Tests 70 | 71 | To run tests execute: 72 | 73 | ```npm test``` 74 | 75 | ## License 76 | 77 | Thumbbot is released under the MIT License. 78 | -------------------------------------------------------------------------------- /test/thumbbot.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test dependencies 3 | */ 4 | 5 | require('chai').should(); 6 | require('mocha-generators')(); 7 | 8 | var Thumbbot = require('../'); 9 | 10 | 11 | /** 12 | * Tests 13 | */ 14 | 15 | describe ('Thumbbot', function () { 16 | describe ('Images', function () { 17 | it ('should take a thumbnail from image using .crop()', function *() { 18 | let image = new Thumbbot(__dirname + '/fixtures/image.jpeg'); 19 | image.crop(100, 200, 200, 200); 20 | 21 | let thumbnail = yield image.save(); 22 | let meta = yield thumbnail.meta(); 23 | meta.width.should.equal(200); 24 | meta.height.should.equal(200); 25 | meta.format.should.equal('JPEG'); 26 | }); 27 | 28 | it ('should take a thumbnail from image using .resize()', function *() { 29 | let image = new Thumbbot(__dirname + '/fixtures/image.jpeg'); 30 | image.resize(200, 200); 31 | 32 | let thumbnail = yield image.save(); 33 | let meta = yield thumbnail.meta(); 34 | meta.width.should.equal(200); 35 | meta.height.should.equal(200); 36 | meta.format.should.equal('JPEG'); 37 | }); 38 | }); 39 | 40 | describe ('Video', function () { 41 | it ('should take a thumbnail from video', function *() { 42 | let video = new Thumbbot(__dirname + '/fixtures/video.mp4'); 43 | 44 | let thumbnail = yield video.save(); 45 | let meta = yield thumbnail.meta(); 46 | meta.width.should.equal(640); 47 | meta.height.should.equal(360); 48 | meta.format.should.equal('JPEG'); 49 | }); 50 | }); 51 | 52 | describe ('Web Pages', function () { 53 | it ('should take a screenshot of a web page', function *() { 54 | this.timeout(10000); 55 | 56 | let page = new Thumbbot('http://smashingmagazine.com'); 57 | 58 | let thumbnail = yield page.save(); 59 | let meta = yield thumbnail.meta(); 60 | meta.width.should.equal(400); 61 | meta.height.should.be.above(8000); 62 | meta.format.should.equal('JPEG'); 63 | }); 64 | 65 | it ('should take a screenshot of a web page while setting window size', function *() { 66 | this.timeout(10000); 67 | 68 | let page = new Thumbbot('http://smashingmagazine.com'); 69 | page.window(1024, 768); 70 | 71 | let thumbnail = yield page.save(); 72 | let meta = yield thumbnail.meta(); 73 | meta.width.should.equal(1024); 74 | meta.height.should.be.above(4000); 75 | meta.format.should.equal('JPEG'); 76 | }); 77 | 78 | it ('should take a screenshot of web page\'s selected area', function *() { 79 | this.timeout(10000); 80 | 81 | let page = new Thumbbot('http://smashingmagazine.com'); 82 | page.crop(0, 0, 300, 300) 83 | .window(1024, 768); 84 | 85 | let thumbnail = yield page.save(); 86 | let meta = yield thumbnail.meta(); 87 | meta.width.should.equal(300); 88 | meta.height.should.equal(300); 89 | meta.format.should.equal('JPEG'); 90 | }); 91 | 92 | it ('should take a screenshot of a web page with javascript disabled', function *() { 93 | this.timeout(10000); 94 | 95 | let page = new Thumbbot('http://smashingmagazine.com'); 96 | page.window(1024, 768) 97 | .disable('javascript'); 98 | 99 | let thumbnail = yield page.save(); 100 | let meta = yield thumbnail.meta(); 101 | meta.width.should.equal(1024); 102 | meta.height.should.be.above(4000); 103 | meta.format.should.equal('JPEG'); 104 | }); 105 | 106 | it ('should take a screenshot of a web page with images disabled', function *() { 107 | this.timeout(10000); 108 | 109 | let page = new Thumbbot('http://smashingmagazine.com'); 110 | page.window(1024, 768) 111 | .disable('images'); 112 | 113 | var thumbnail = yield page.save(); 114 | var meta = yield thumbnail.meta(); 115 | meta.width.should.equal(1024); 116 | meta.height.should.be.above(4000); 117 | meta.format.should.equal('JPEG'); 118 | }); 119 | }); 120 | }); -------------------------------------------------------------------------------- /lib/thumbbot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var request = require('co-request'); 6 | var Image = require('magician'); 7 | var exec = require('co-exec'); 8 | 9 | var util = require('./util'); 10 | 11 | 12 | /** 13 | * Thumbbot 14 | */ 15 | 16 | class Thumbbot { 17 | constructor (path) { 18 | this.path = path; 19 | this.options = {}; 20 | } 21 | 22 | format (format) { 23 | this.options.format = format; 24 | 25 | return this; 26 | } 27 | 28 | width (width) { 29 | if (!this.options.resize) this.options.resize = {}; 30 | 31 | this.options.resize.width = width; 32 | 33 | return this; 34 | } 35 | 36 | height (height) { 37 | if (!this.options.resize) this.options.resize = {}; 38 | 39 | this.options.resize.height = height; 40 | 41 | return this; 42 | } 43 | 44 | resize (width, height) { 45 | this.options.resize = { width, height }; 46 | 47 | return this; 48 | } 49 | 50 | window (width, height) { 51 | this.options.window = { width, height }; 52 | 53 | return this; 54 | } 55 | 56 | seek (position) { 57 | this.options.seek = position; 58 | 59 | return this; 60 | } 61 | 62 | crop (x, y, width, height) { 63 | this.options.crop = { x, y, width, height }; 64 | 65 | return this; 66 | } 67 | 68 | disable (feature) { 69 | if (!this.options.disable) this.options.disable = {}; 70 | 71 | this.options.disable[feature] = true; 72 | } 73 | 74 | type () { 75 | let extension = util.parseExtension(this.path); 76 | 77 | switch (extension) { 78 | case 'mp4': 79 | case '3gp': 80 | case 'mov': 81 | case 'avi': 82 | return 'video'; 83 | 84 | case 'jpeg': 85 | case 'jpg': 86 | case 'png': 87 | case 'gif': 88 | case 'tiff': 89 | return 'image'; 90 | 91 | default: 92 | return 'page'; 93 | } 94 | } 95 | 96 | saveVideo (destPath) { 97 | var seek = this.options.seek || '00:00:01'; 98 | 99 | var command = `ffmpeg -i "${ this.path }" -ss "${ seek }" -vframes 1 "${ destPath }"`; 100 | 101 | return exec(command); 102 | } 103 | 104 | savePage (destPath) { 105 | let options = Object.assign({}, this.options); 106 | 107 | options.url = encodeURIComponent(this.path); 108 | options.destPath = destPath; 109 | options = JSON.stringify(options); 110 | 111 | let command = ['phantomjs', '--disk-cache=true', 112 | '--ignore-ssl-errors=true', 113 | '--web-security=false', 114 | '--ssl-protocol=TLSv1', 115 | __dirname + '/render.js', 116 | `'${ options }'`].join(' '); 117 | 118 | return exec(command); 119 | } 120 | 121 | saveImage (destPath) { 122 | let image = new Image(this.path); 123 | 124 | let options = this.options; 125 | let { resize, crop } = options; 126 | 127 | if (resize) { 128 | image.resize(resize.width, resize.height); 129 | image.fit(options.fit || 'clip'); 130 | } 131 | 132 | if (crop) { 133 | image.crop(crop.x, crop.y, crop.width, crop.height); 134 | } 135 | 136 | return image.save(destPath); 137 | } 138 | 139 | * save (destPath) { 140 | let type = this.type(); 141 | 142 | if (!destPath) { 143 | destPath = util.tmpPath(this.options.format); 144 | } 145 | 146 | switch (type) { 147 | case 'video': 148 | yield this.saveVideo(destPath); 149 | break; 150 | 151 | case 'image': 152 | yield this.saveImage(destPath); 153 | break; 154 | 155 | case 'page': 156 | yield this.savePage(destPath); 157 | break; 158 | 159 | default: 160 | throw new Error('Uknown file type given to Thumbbot'); 161 | } 162 | 163 | return new Image(destPath); 164 | } 165 | } 166 | 167 | 168 | /** 169 | * Module exports 170 | */ 171 | 172 | module.exports = Thumbbot; 173 | --------------------------------------------------------------------------------