├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── cli-parser.js ├── index.js └── url-to-image.js └── test └── test-functional.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 4 space indentation 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # Tab indentation (no size specified) 17 | [*.js] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | # Matches the exact files package.json and .travis.yml 22 | [{package.json,.travis.yml,Gruntfile.js}] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | [Makefile] 27 | indent_style = tab 28 | indent_size = 4 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.2 4 | - 4.1 5 | - 0.12 6 | - 0.11 7 | - 0.10 8 | notifications: 9 | email: 10 | - kimmo.brunfeldt+urltoimage@gmail.com 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | API changes across versions 4 | 5 | ## 0.2.0 -> 1.0.0 6 | 7 | * `.done` callback is now: `.then` 8 | 9 | Promise library has been changed to [bluebird](http://bluebirdjs.com/docs/api-reference.html). 10 | 11 | * `.fail` callback is now: `.catch` 12 | 13 | * `options.ignoreSslErrors` was removed. Use `options.phantomArguments` instead. 14 | 15 | Examples 16 | ``` 17 | { 18 | // Note: this is the new default for phantom arguments 19 | phantomArguments: '--ignore-ssl-errors=true' 20 | } 21 | ``` 22 | 23 | ``` 24 | urltoimage --phantom-arguments="--ignore-ssl-errors=true" google.com google.png 25 | ``` 26 | 27 | See also: http://phantomjs.org/api/command-line.html 28 | 29 | * `options.sslProtocol` was removed. Use `options.phantomArguments` instead. 30 | 31 | See above example. 32 | See also: http://phantomjs.org/api/command-line.html 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kimmo Brunfeldt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # url-to-image 2 | 3 | **DEPRECATED:** I recommend using implementations that use Headless Chrome underneath, for example: https://github.com/kimmobrunfeldt/squint or https://github.com/alvarcarto/url-to-pdf-api. You can still continue using this module but no updates will be applied. 4 | 5 | 6 | [![Build Status](https://travis-ci.org/kimmobrunfeldt/url-to-image.png?branch=master)](https://travis-ci.org/kimmobrunfeldt/url-to-image) 7 | [![Dependency Status](https://david-dm.org/kimmobrunfeldt/url-to-image.png?theme=shields.io)](https://david-dm.org/kimmobrunfeldt/url-to-image) 8 | [![devDependency Status](https://david-dm.org/kimmobrunfeldt/url-to-image/dev-status.png?theme=shields.io)](https://david-dm.org/kimmobrunfeldt/url-to-image#info=devDependencies) 9 | 10 | Takes screenshot of a given page. This module correctly handles pages which dynamically load content making AJAX requests. 11 | Instead of waiting fixed amount of time before rendering, we give a short time for the page to make additional requests. 12 | 13 | **Usage from code:** 14 | 15 | ```javascript 16 | const urlToImage = require('url-to-image'); 17 | urlToImage('http://google.com', 'google.png').then(function() { 18 | // now google.png exists and contains screenshot of google.com 19 | }).catch(function(err) { 20 | console.error(err); 21 | }); 22 | ``` 23 | 24 | **Usage from command line:** 25 | 26 | ```bash 27 | $ urltoimage http://google.com google.png 28 | ``` 29 | 30 | Sometimes it's useful to see requests, responses and page errors from PhantomJS: 31 | 32 | ```bash 33 | $ urltoimage http://google.com google.png --verbose 34 | -> GET http://google.com/ 35 | -> GET http://www.google.fi/?gfe_rd=cr&ei=xTYxVouuOeiA8QexyZ2QBw 36 | <- 302 http://google.com/ 37 | -> GET http://ssl.gstatic.com/gb/images/b_8d5afc09.png 38 | 39 | ... quite a lot of requests ... 40 | 41 | -> GET http://ssl.gstatic.com/gb/js/sem_32d9c4210965b8e7bfa34fa376864ce8.js 42 | <- 200 http://ssl.gstatic.com/gb/js/sem_32d9c4210965b8e7bfa34fa376864ce8.js 43 | Render screenshot.. 44 | Done. 45 | ``` 46 | 47 | 48 | For more options, see [CLI](#command-line-interface-cli) chapter. 49 | 50 | ## Install 51 | 52 | npm install url-to-image 53 | 54 | PhantomJS is installed by using [Medium/phantomjs NPM module](https://github.com/Medium/phantomjs). 55 | 56 | ## API 57 | 58 | ```javascript 59 | const urlToImage = require('url-to-image'); 60 | ``` 61 | 62 | #### urlToImage(url, filePath, options) 63 | 64 | This will run a PhantomJS script([url-to-image.js](./src/url-to-image.js)) which renders given url to an image. 65 | 66 | **Parameters** 67 | 68 | * `url` Url of the page which will be rendered as image. For example `http://google.com`. 69 | * `filePath` File path where to save rendered image. 70 | * `options` Options for page rendering. 71 | 72 | **Default values for options** 73 | 74 | ```javascript 75 | { 76 | // User agent width 77 | width: 1200, 78 | 79 | // User agent height 80 | height: 800, 81 | 82 | // The file type of the rendered image. By default, PhantomJS 83 | // sets the output format automatically based on the file extension. 84 | // Supported: PNG, GIF, JPEG, PDF 85 | fileType: 'jpeg', 86 | 87 | // The file quality of the rendered image, represented as a percentage. 88 | // This reduces the image size. By default, 100 percent is used. 89 | fileQuality: 100, 90 | 91 | // Sets the width of the final image (cropped from the User agent defined size) 92 | // By default, no cropping is done. 93 | cropWidth: false, 94 | 95 | // Sets the height of the final image (cropped from the User agent defined size) 96 | // By default, no cropping is done. 97 | cropHeight: false, 98 | 99 | //Sets the offset of where to begin the image cropping from the left margin 100 | // of the page 101 | cropOffsetLeft: 0, 102 | 103 | //Sets the offset of where to begin the image cropping from the top margin 104 | // of the page 105 | cropOffsetTop: 0, 106 | 107 | // How long in ms do we wait for additional requests 108 | // after all initial requests have gotten their response 109 | // Note: this does NOT limit the amount of time individual request 110 | // can take in time 111 | requestTimeout: 300, 112 | 113 | // How long in ms do we wait at maximum. The screenshot is 114 | // taken after this time even though resources are not loaded 115 | maxTimeout: 1000 * 10, 116 | 117 | // How long in ms do we wait for phantomjs process to finish. 118 | // If the process is running after this time, it is killed. 119 | killTimeout: 1000 * 60 * 2, 120 | 121 | // If true, phantomjs script will output requests and responses to stdout 122 | verbose: false, 123 | 124 | // String of of phantomjs arguments 125 | // You can separate arguments with spaces 126 | // See options in http://phantomjs.org/api/command-line.html 127 | phantomArguments: '--ignore-ssl-errors=true' 128 | } 129 | ``` 130 | 131 | **Returns** 132 | 133 | [Bluebird promise object](http://bluebirdjs.com/docs/api-reference.html). 134 | 135 | **Detailed example** 136 | 137 | ```javascript 138 | const urlToImage = require('url-to-image'); 139 | 140 | const options = { 141 | width: 600, 142 | height: 800, 143 | // Give a short time to load additional resources 144 | requestTimeout: 100 145 | } 146 | 147 | urlToImage('http://google.com', 'google.png', options) 148 | .then(function() { 149 | // do stuff with google.png 150 | }) 151 | .catch(function(err) { 152 | console.error(err); 153 | }); 154 | ``` 155 | 156 | ## Command line interface (CLI) 157 | 158 | The package also ships with a cli called `urltoimage`. 159 | 160 | ``` 161 | Usage: urltoimage [options] 162 | 163 | Url to take screenshot of 164 | File path where the screenshot is saved 165 | 166 | 167 | Options: 168 | --width Width of the viewport [string] [default: 1280] 169 | --height Height of the viewport [string] [default: 800] 170 | --file-type The file type of the rendered image. By default, 171 | PhantomJS sets the output format automatically based on 172 | the file extension. Supported: PNG, GIF, JPEG, PDF 173 | [string] [default: false] 174 | --file-quality The file quality of the rendered image, represented as a 175 | percentage. This reduces the image size. 176 | By default, 100 percent is used. 177 | [string] [default: 100] 178 | --crop-width Sets the width of the final image (cropped from the User 179 | agent defined size). 180 | [string] [default: false] 181 | --crop-height Sets the height of the final image (cropped from the User 182 | agent defined size). 183 | [string] [default: false] 184 | --cropoffset-left Sets the offset of where to begin the image cropping from 185 | the left margin of the page. 186 | [string] [default: false] 187 | --cropoffset-top Sets the offset of where to begin the image cropping from 188 | the top margin of the page. 189 | [string] [default: false] 190 | --request-timeout How long in ms do we wait for additional requests after 191 | all initial requests have gotten their response 192 | [string] [default: 300] 193 | --max-timeout How long in ms do we wait at maximum. The screenshot is 194 | taken after this time even though resources are not 195 | loaded [string] [default: 10000] 196 | --kill-timeout How long in ms do we wait for phantomjs process to 197 | finish. If the process is running after this time, it is 198 | killed. [string] [default: 120000] 199 | --phantom-arguments Command line arguments to be passed to phantomjs 200 | process.You must use the format 201 | --phantom-arguments="--version". 202 | [string] [default: "--ignore-ssl-errors=true"] 203 | --verbose If set, script will output additional information to 204 | stdout. [boolean] [default: false] 205 | -h, --help Show help [boolean] 206 | -v, --version Show version number [boolean] 207 | 208 | Examples: 209 | urltoimage http://google.com google.png 210 | ``` 211 | 212 | # Contributors 213 | 214 | ## Release 215 | 216 | * Commit all changes. 217 | * Use [releasor](https://github.com/kimmobrunfeldt/releasor) to automate the release: 218 | 219 | `releasor --bump patch` 220 | 221 | * Edit GitHub release notes. 222 | 223 | ## Test 224 | 225 | npm test 226 | 227 | ## Attribution 228 | 229 | This module was inspired by 230 | 231 | * [url-to-screenshot](https://github.com/juliangruber/url-to-screenshot) 232 | * https://gist.github.com/cjoudrey/1341747 233 | 234 | # License 235 | 236 | MIT 237 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-to-image", 3 | "version": "1.0.0", 4 | "description": "PhantomJS screenshotting done right", 5 | "main": "src/index.js", 6 | "bin": { 7 | "urltoimage": "./src/index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:kimmobrunfeldt/url-to-image.git" 12 | }, 13 | "keywords": [ 14 | "phantomjs", 15 | "screenshot", 16 | "picture", 17 | "webpage", 18 | "render" 19 | ], 20 | "author": "Kimmo Brunfeldt", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/kimmobrunfeldt/url-to-image/issues" 24 | }, 25 | "dependencies": { 26 | "bluebird": "^3.5.0", 27 | "lodash": "^4.17.4", 28 | "phantomjs-prebuilt": "^2.1.7", 29 | "yargs": "^8.0.2" 30 | }, 31 | "devDependencies": { 32 | "image-size": "^0.5.5", 33 | "mocha": "^3.4.2", 34 | "releasor": "^1.2.1" 35 | }, 36 | "scripts": { 37 | "test": "mocha" 38 | }, 39 | "engines": { 40 | "node": ">=7.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/cli-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const yargs = require('yargs'); 5 | 6 | const VERSION = require('../package.json').version; 7 | 8 | const defaultOpts = { 9 | width: 1280, 10 | height: 800, 11 | requestTimeout: 300, 12 | maxTimeout: 1000 * 10, 13 | killTimeout: 1000 * 60 * 2, 14 | verbose: false, 15 | fileType: false, 16 | fileQuality: false, 17 | cropWidth: false, 18 | cropHeight: false, 19 | cropOffsetLeft: 0, 20 | cropOffsetTop: 0, 21 | phantomArguments: '--ignore-ssl-errors=true' 22 | }; 23 | 24 | function getOpts(argv) { 25 | const userOpts = getUserOpts(); 26 | const opts = _.merge(defaultOpts, userOpts); 27 | return validateAndTransformOpts(opts); 28 | } 29 | 30 | function getUserOpts() { 31 | const userOpts = yargs 32 | .usage( 33 | 'Usage: $0 [options]\n\n' + 34 | ' Url to take screenshot of\n' + 35 | ' File path where the screenshot is saved\n' 36 | ) 37 | .example('$0 http://google.com google.png') 38 | .demand(2) 39 | .option('width', { 40 | describe: 'Width of the viewport', 41 | default: defaultOpts.width, 42 | type: 'string' 43 | }) 44 | .option('height', { 45 | describe: 'Height of the viewport', 46 | default: defaultOpts.height, 47 | type: 'string' 48 | }) 49 | .option('request-timeout', { 50 | describe: 'How long in ms do we wait for additional requests' + 51 | ' after all initial requests have gotten their response', 52 | default: defaultOpts.requestTimeout, 53 | type: 'string' 54 | }) 55 | .option('max-timeout', { 56 | describe: 'How long in ms do we wait at maximum. The screenshot is' + 57 | ' taken after this time even though resources are not loaded', 58 | default: defaultOpts.maxTimeout, 59 | type: 'string' 60 | }) 61 | .option('file-type', { 62 | describe: 'Defines the file type you want to create.', 63 | default: defaultOpts.fileType, 64 | type: 'string' 65 | }) 66 | .option('file-quality', { 67 | describe: 'Defines the quality of the file you want rendered' + 68 | 'as a percentage. Default is 100.', 69 | default: defaultOpts.fileQuality, 70 | type: 'string' 71 | }) 72 | .option('crop-width', { 73 | describe: 'The width of the final image which will be created', 74 | default: defaultOpts.cropWidth, 75 | type: 'string' 76 | }) 77 | .option('crop-height', { 78 | describe: 'The height of the final image which will be created', 79 | default: defaultOpts.cropHeight, 80 | type: 'string' 81 | }) 82 | .option('cropoffset-left', { 83 | describe: 'The position offset from the left of the screen from' + 84 | ' where to start the image crop', 85 | default: defaultOpts.cropOffsetLeft, 86 | type: 'string' 87 | }) 88 | .option('cropoffset-top', { 89 | describe: 'The position offset from the top of the screen from' + 90 | ' where to start the image crop', 91 | default: defaultOpts.cropOffsetTop, 92 | type: 'string' 93 | }) 94 | .option('kill-timeout', { 95 | describe: 'How long in ms do we wait for phantomjs process to finish.' + 96 | ' If the process is running after this time, it is killed.', 97 | default: defaultOpts.killTimeout, 98 | type: 'string' 99 | }) 100 | .option('phantom-arguments', { 101 | describe: 'Command line arguments to be passed to phantomjs process.' + 102 | 'You must use the format --phantom-arguments="--version".', 103 | default: defaultOpts.phantomArguments, 104 | type: 'string' 105 | }) 106 | .option('verbose', { 107 | describe: 'If set, script will output additional information to stdout.', 108 | default: defaultOpts.verbose, 109 | type: 'boolean' 110 | }) 111 | .help('h') 112 | .alias('h', 'help') 113 | .alias('v', 'version') 114 | .version(VERSION) 115 | .argv; 116 | 117 | userOpts.url = userOpts._[0]; 118 | userOpts.path = userOpts._[1]; 119 | return userOpts; 120 | } 121 | 122 | function validateAndTransformOpts(opts) { 123 | if (opts.width) { 124 | validateNumber(opts.width, 'Incorrect argument, width is not a number'); 125 | } 126 | 127 | if (opts.height) { 128 | validateNumber(opts.height, 'Incorrect argument, height is not a number'); 129 | } 130 | 131 | if (opts.requestTimeout) { 132 | validateNumber(opts.requestTimeout, 'Incorrect argument, request timeout is not a number'); 133 | } 134 | 135 | if (opts.maxTimeout) { 136 | validateNumber(opts.maxTimeout, 'Incorrect argument, max timeout is not a number'); 137 | } 138 | 139 | if (opts.killTimeout) { 140 | validateNumber(opts.killTimeout, 'Incorrect argument, kill timeout is not a number'); 141 | } 142 | 143 | if (opts.fileQuality) { 144 | validateNumber(opts.fileQuality, 'Incorrect argument, file quality is not a number'); 145 | } 146 | 147 | if (opts.cropWidth) { 148 | validateNumber(opts.cropWidth, 'Incorrect argument, crop width is not a number'); 149 | } 150 | 151 | if (opts.cropHeight) { 152 | validateNumber(opts.cropHeight, 'Incorrect argument, crop height is not a number'); 153 | } 154 | 155 | if (opts.cropOffsetLeft) { 156 | validateNumber(opts.killTimeout, 'Incorrect argument, crop offset left is not a number'); 157 | } 158 | 159 | if (opts.cropOffsetTop) { 160 | validateNumber(opts.killTimeout, 'Incorrect argument, crop offset top is not a number'); 161 | } 162 | 163 | return opts; 164 | } 165 | 166 | function validateNumber(val, message) { 167 | const number = Number(val); 168 | if (!_.isNumber(number)) { 169 | const err = message; 170 | err.argumentError = true; 171 | throw err; 172 | } 173 | } 174 | 175 | module.exports = { 176 | defaultOpts: defaultOpts, 177 | getOpts: getOpts 178 | }; 179 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const Promise = require('bluebird'); 5 | const _ = require('lodash'); 6 | const path = require('path'); 7 | const childProcess = require('child_process'); 8 | const phantomjs = require('phantomjs') 9 | const cliParser = require('./cli-parser'); 10 | 11 | function render(url, filePath, opts) { 12 | opts = _.extend(cliParser.defaultOpts, opts); 13 | 14 | let args = []; 15 | if (_.isString(opts.phantomArguments)) { 16 | args = opts.phantomArguments.split(' '); 17 | } 18 | 19 | if (!_.startsWith(url, 'http') && 20 | !_.startsWith(url, 'https') && 21 | !_.startsWith(url, 'file')) { 22 | url = 'http://' + url; 23 | } 24 | 25 | args = args.concat([ 26 | path.join(__dirname, 'url-to-image.js'), 27 | url, 28 | filePath, 29 | opts.width, 30 | opts.height, 31 | opts.requestTimeout, 32 | opts.maxTimeout, 33 | opts.verbose, 34 | opts.fileType, 35 | opts.fileQuality, 36 | opts.cropWidth, 37 | opts.cropHeight, 38 | opts.cropOffsetLeft, 39 | opts.cropOffsetTop 40 | ]); 41 | 42 | let execOpts = { 43 | maxBuffer: Infinity 44 | }; 45 | 46 | let killTimer; 47 | return new Promise(function(resolve, reject) { 48 | let child; 49 | killTimer = setTimeout(function() { 50 | killPhantom(opts, child); 51 | reject(new Error('Phantomjs process timeout')); 52 | }, opts.killTimeout); 53 | 54 | try { 55 | child = childProcess.spawn(phantomjs.path, args, { 56 | stdio: 'inherit' 57 | }); 58 | } catch (err) { 59 | return Promise.reject(err); 60 | } 61 | 62 | function errorHandler(err) { 63 | // Remove bound handlers after use 64 | child.removeListener('close', closeHandler); 65 | reject(err); 66 | } 67 | 68 | function closeHandler(exitCode) { 69 | child.removeListener('error', errorHandler); 70 | if (exitCode > 0) { 71 | let err; 72 | if (exitCode === 10) { 73 | err = new Error('Unable to load given url: ' + url); 74 | } 75 | reject(err); 76 | } else { 77 | resolve(exitCode); 78 | } 79 | } 80 | 81 | child.once('error', errorHandler); 82 | child.once('close', closeHandler); 83 | }) 84 | .finally(function() { 85 | if (killTimer) { 86 | clearTimeout(killTimer); 87 | } 88 | }); 89 | } 90 | 91 | function killPhantom(opts, child) { 92 | if (child) { 93 | const msg = 'Phantomjs process didn\'t finish in ' + 94 | opts.killTimeout + 'ms, killing it..'; 95 | console.error(msg); 96 | 97 | child.kill(); 98 | } 99 | } 100 | 101 | if (require.main === module) { 102 | let opts; 103 | try { 104 | opts = cliParser.getOpts(); 105 | } catch (err) { 106 | if (err.argumentError) { 107 | console.error(err.message); 108 | process.exit(1); 109 | } 110 | 111 | throw err; 112 | } 113 | 114 | render(opts.url, opts.path, opts) 115 | .catch(function(err) { 116 | console.error('\nTaking screenshot failed to error:'); 117 | if (err && err.message) { 118 | console.error(err.message); 119 | } else if (err) { 120 | console.error(err); 121 | } else { 122 | console.error('No error message available'); 123 | } 124 | 125 | process.exit(2); 126 | }); 127 | } 128 | 129 | module.exports = render; 130 | -------------------------------------------------------------------------------- /src/url-to-image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // PhantomJS script 4 | // Takes screeshot of a given page. This correctly handles pages which 5 | // dynamically load content making AJAX requests. 6 | 7 | // Instead of waiting fixed amount of time before rendering, we give a short 8 | // time for the page to make additional requests. 9 | 10 | // Phantom internals 11 | const system = require('system'); 12 | const webPage = require('webpage'); 13 | 14 | function main() { 15 | // I tried to use yargs as a nicer commandline option parser but 16 | // it doesn't run in phantomjs environment 17 | const args = system.args; 18 | const opts = { 19 | url: args[1], 20 | filePath: args[2], 21 | width: args[3], 22 | height: args[4], 23 | requestTimeout: args[5], 24 | maxTimeout: args[6], 25 | verbose: args[7] === 'true', 26 | fileType: args[8], 27 | fileQuality: args[9] ? args[9] : 100, 28 | cropWidth: args[10], 29 | cropHeight: args[11], 30 | cropOffsetLeft: args[12] ? args[12] : 0, 31 | cropOffsetTop: args[13] ? args[13] : 0 32 | }; 33 | 34 | renderPage(opts); 35 | } 36 | 37 | function renderPage(opts) { 38 | let requestCount = 0; 39 | let forceRenderTimeout; 40 | let dynamicRenderTimeout; 41 | 42 | const page = webPage.create(); 43 | page.viewportSize = { 44 | width: opts.width, 45 | height: opts.height 46 | }; 47 | // Silence confirmation messages and errors 48 | page.onConfirm = page.onPrompt = function noOp() {}; 49 | page.onError = function(err) { 50 | log('Page error:', err); 51 | }; 52 | 53 | page.onResourceRequested = function(request) { 54 | log('->', request.method, request.url); 55 | requestCount += 1; 56 | clearTimeout(dynamicRenderTimeout); 57 | }; 58 | 59 | page.onResourceReceived = function(response) { 60 | if (!response.stage || response.stage === 'end') { 61 | log('<-', response.status, response.url); 62 | requestCount -= 1; 63 | if (requestCount === 0) { 64 | dynamicRenderTimeout = setTimeout(renderAndExit, opts.requestTimeout); 65 | } 66 | } 67 | }; 68 | 69 | page.open(opts.url, function(status) { 70 | if (status !== 'success') { 71 | log('Unable to load url:', opts.url); 72 | phantom.exit(10); 73 | } else { 74 | forceRenderTimeout = setTimeout(renderAndExit, opts.maxTimeout); 75 | } 76 | }); 77 | 78 | function log() { 79 | // PhantomJS doesn't stringify objects very well, doing that manually 80 | if (opts.verbose) { 81 | let args = Array.prototype.slice.call(arguments); 82 | 83 | let str = ''; 84 | args.forEach(function(arg) { 85 | if (isString) { 86 | str += arg; 87 | } else { 88 | str += JSON.stringify(arg, null, 2); 89 | } 90 | 91 | str += ' ' 92 | }); 93 | 94 | console.log(str); 95 | } 96 | } 97 | 98 | function renderAndExit() { 99 | log('Render screenshot..'); 100 | if(opts.cropWidth && opts.cropHeight) { 101 | log("Cropping..."); 102 | page.clipRect = {top: opts.cropOffsetTop, left: opts.cropOffsetLeft, width: opts.cropWidth, height: opts.cropHeight}; 103 | } 104 | 105 | let renderOpts = { 106 | fileQuality: opts.fileQuality 107 | }; 108 | 109 | if(opts.fileType) { 110 | log("Adjusting File Type..."); 111 | renderOpts.fileType = opts.fileType; 112 | } 113 | 114 | page.render(opts.filePath, renderOpts); 115 | log('Done.'); 116 | phantom.exit(); 117 | } 118 | } 119 | 120 | function isString(value) { 121 | return typeof value === 'string' 122 | } 123 | 124 | main(); 125 | -------------------------------------------------------------------------------- /test/test-functional.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const fs = require('fs'); 5 | const assert = require('assert'); 6 | const sizeOf = require('image-size'); 7 | 8 | const urlToImage = require('../src/index'); 9 | 10 | describe('urlToImage', function() { 11 | 12 | const server = http.createServer(function(req, res) { 13 | res.end('test'); 14 | }); 15 | 16 | before(function(done) { 17 | server.listen(9000); 18 | server.on('listening', done); 19 | }); 20 | 21 | after(function(done) { 22 | server.close(); 23 | done(); 24 | }); 25 | 26 | describe('render', function() { 27 | this.timeout(20000); 28 | 29 | it('should render test image', function(done) { 30 | urlToImage('http://localhost:9000', 'localhost.png') 31 | .then(function() { 32 | const dimensions = sizeOf('localhost.png'); 33 | assert.equal(dimensions.width, 1280, 'default width is incorrect'); 34 | fs.unlinkSync('localhost.png'); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should render image in custom size', function(done) { 40 | urlToImage( 41 | 'http://localhost:9000', 42 | 'localhost.png', { 43 | width: 800, 44 | height: 600 45 | } 46 | ) 47 | .then(function() { 48 | const dimensions = sizeOf('localhost.png'); 49 | 50 | assert.equal(dimensions.width, 800, 'width is incorrect'); 51 | 52 | // The content of test page is so small, so viewport 53 | // is larger than content. If content were larger, 54 | // urlToImage's height could be bigger than viewport's width 55 | assert.equal(dimensions.height, 600, 'height is incorrect'); 56 | 57 | fs.unlinkSync('localhost.png'); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should fail to incorrect url', function(done) { 63 | this.timeout(5000); 64 | 65 | urlToImage( 66 | 'http://failure', 67 | 'localhost.png', { 68 | width: 800, 69 | height: 600 70 | } 71 | ) 72 | .catch(function(err) { 73 | done(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | --------------------------------------------------------------------------------