├── .gitignore ├── package.json ├── LICENSE ├── test.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules/ 4 | *.png 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-server-screenshot", 3 | "version": "0.2.4", 4 | "description": "A server-side component for taking screenshots of html", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/stefangab95/node-server-screenshot.git" 12 | }, 13 | "author": "Stefan-Gabriel Muscalu", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/stefangab95/node-server-screenshot/issues" 17 | }, 18 | "homepage": "https://github.com/stefangab95/node-server-screenshot#readme", 19 | "dependencies": { 20 | "nightmare": "^3.0.2" 21 | }, 22 | "engines": { 23 | "node": ">=4.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ștefan-Gabriel Muscalu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var app = require("./index"); 2 | var exec = require("child_process").exec; 3 | var fs = require('fs'); 4 | 5 | function assertPNG(buffer) { 6 | // ? P N G \r \n SUB \n 7 | var pngSignatureHeader = [137, 80, 78, 71, 13, 10, 26, 10]; 8 | 9 | for (var i = 0; i < pngSignatureHeader.length; i++) { 10 | if (buffer[i] !== pngSignatureHeader[i]) { 11 | throw new Error('Buffer has malformed PNG signature') 12 | } 13 | } 14 | } 15 | 16 | function assertPNGfile(file) { 17 | if (!fs.existsSync(file)) { 18 | throw new Error('File ' + file + ' not found!'); 19 | } 20 | 21 | assertPNG(fs.readFileSync(file)); 22 | } 23 | 24 | console.log('saving google.com into google.png'); 25 | app.fromURL("https://google.com", "google.png", { clip: { x: 490, y: 180, width: 100, height: 100 }, scale: 1 }, function (err) { 26 | if (err) { 27 | throw err; 28 | } 29 | 30 | assertPNGfile('google.png'); 31 | console.log('google.png looks ok'); 32 | 33 | console.log('saving wikipedia.png with injected html headlessly') 34 | app.fromHTML('This has been modified by injecting the HTML', "wiki.png", { 35 | inject: { 36 | url: "https://en.wikipedia.org/wiki/Main_Page", 37 | selector: { className: "mw-wiki-logo" } 38 | }, 39 | width: 500, 40 | height: 1000, 41 | show: false 42 | }, function () { 43 | if (err) { 44 | throw err; 45 | } 46 | assertPNGfile('wiki.png'); 47 | console.log('wiki.png looks ok'); 48 | 49 | 50 | console.log('checking that if provided path is null, it will output a buffer'); 51 | app.fromHTML("something something html", null, { 52 | clip: { 53 | width: 100, 54 | height: 100 55 | } 56 | }, function (err, buff) { 57 | if (err) { 58 | throw err; 59 | } 60 | 61 | assertPNG(buff); 62 | console.log('PNG buffer looks ok'); 63 | 64 | app.fromURL("https://bluecrate.com", "redirect.png",{waitMilliseconds: 5000}, function(err){ 65 | if (err) { 66 | throw err; 67 | } 68 | 69 | assertPNGfile('redirect.png'); 70 | console.log('redirect.png looks ok'); 71 | }); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-server-screenshot 2 | A server-side NodeJS component that takes screenshots of: 3 | * Webpage at a `URL` location 4 | * Pure `HTML` 5 | * `HTML` injected into a webpage at a `URL` 6 | 7 | ## API: 8 | 9 | **fromURL(url, destinationFile, options, callback)** 10 | 11 | This will navigate to the url, take a screenshot and save it to the destination file 12 | * Url: `String` with the URL 13 | * DestinationFile: `String` with the location of the image file that will be saved. 14 | If null is provided then the callback will return as 2nd parameter a PNG buffer. 15 | * Options: `Object[={}]` with options 16 | * width: `Number[=1280]` to set the width of the viewport 17 | * height: `Number[=720]` to set the height of the viewport 18 | * waitAfterSelector: `String[="html"]` to set the selector that will be awaited for completion before taking the screenshot 19 | * waitMilliseconds: `Number[=1000]` to set the waiting time after the selector is ready before taking the screenshot 20 | * clip: `Object[={}]` crops the image 21 | * x: `Number` 22 | * y: `Number` 23 | * width: `Number` 24 | * height: `Number` 25 | * scale: `Number[=1]` to set the scaling of the browser. 26 | * Callback: `Function` with an optional error argument, and an optional PNG buffer argument. 27 | 28 | #### Example: 29 | ```javascript 30 | var app = require("node-server-screenshot"); 31 | app.fromURL("https://google.com", "test.png", function(){ 32 | //an image of google.com has been saved at ./test.png 33 | }); 34 | ``` 35 | 36 | ___ 37 | 38 | **fromHTML(html, destinationFile, options, callback)** 39 | 40 | This will navigate to the url, take a screenshot and save it to the destination file 41 | * Url: `String` with the URL 42 | * DestinationFile: `String` with the location of the image file that will be saved. 43 | If null is provided then the callback will return as 2nd parameter a PNG buffer. 44 | * Options: `Object[={}]` with options 45 | * width: `Number[=1280]` to set the width of the viewport 46 | * height: `Number[=720]` to set the height of the viewport 47 | * waitAfterSelector: `String[="html"]` to set the selector that will be awaited for completion before taking the screenshot 48 | * waitMilliseconds: `Number[=1000]` to set the waiting time after the selector is ready before taking the screenshot 49 | * clip: `Object[={}]` crops the image 50 | * x: `Number` 51 | * y: `Number` 52 | * width: `Number` 53 | * height: `Number` 54 | * inject: `Object[={}]` 55 | * url: `String[="about:blank"]` the url that the html will be injected in 56 | * selector `String[="html"]`: selecting the nodes where the HTML will be injected 57 | * `String` 58 | * `{tag: String}` 59 | * `{id: String}` 60 | * `{className: String}` 61 | * `{jQuery: String}` - Note! JQuery must be embedded in the page already 62 | * Callback: `Function` with an optional error argument, and an optional PNG buffer argument. 63 | 64 | #### Example: 65 | ```javascript 66 | var app = require("node-server-screenshot"); 67 | app.fromHTML( 68 | 'This has been modified by injecting the HTML', 69 | "test.png", 70 | {inject: { 71 | url: "https://en.wikipedia.org/wiki/Main_Page", 72 | selector: {className: "mw-wiki-logo"} 73 | }}, 74 | function(){ 75 | //an image of the HTML has been saved at ./test.png 76 | } 77 | ); 78 | ``` 79 | ```javascript 80 | var app = require("node-server-screenshot"); 81 | app.fromHTML("Hello world!", "test.png", function(){ 82 | //an image of the HTML has been saved at ./test.png 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Nightmare = require("nightmare"); 2 | 3 | /** 4 | * Callback for taking a screenshot of a web page. 5 | * 6 | * @callback screenshotCallback 7 | * @param {Object} [error] - The error object if an error occurred, otherwise undefined. 8 | */ 9 | 10 | Nightmare.action('injectHTML', function (selector, html, done) { 11 | this.evaluate_now(function (selector, html) { 12 | 13 | function applyHTML() { 14 | "use strict"; 15 | for (var i = 0; i < this.length; i++) 16 | this[i].innerHTML = html; 17 | } 18 | 19 | try { 20 | if (typeof selector == "string") 21 | return applyHTML.call(document.getElementsByTagName(selector)); 22 | 23 | if (selector.tag) 24 | return applyHTML.call(document.getElementsByTagName(selector.tag)); 25 | 26 | if (selector.id) 27 | return document.getElementById(selector.id).innerHTML = html; 28 | 29 | if (selector.className) 30 | return applyHTML.call(document.getElementsByClassName(selector.className)); 31 | 32 | if (selector.jQuery) 33 | return applyHTML.call(($ || jQuery || window.jQuery)(selector.jQuery)); 34 | 35 | } catch (ex) { 36 | document.getElementsByTagName("html")[0].innerHTML = ex.stack; 37 | } 38 | }, done, selector, html) 39 | }); 40 | 41 | /** 42 | * Navigates to a url and takes a screenshot 43 | * @param {String} url 44 | * @param {String|null} path - screenshot path 45 | * @param {Object} options 46 | * @param {Number=} options.scale 47 | * @param {Number=} options.width 48 | * @param {Number=} options.height 49 | * @param {String=} options.waitAfterSelector 50 | * @param {Number=} options.waitMilliseconds 51 | * @param {Object=} options.clip 52 | * @param {Number} options.clip.x 53 | * @param {Number} options.clip.y 54 | * @param {Number} options.clip.width 55 | * @param {Number} options.clip.height 56 | * 57 | * @param {screenshotCallback} callback 58 | */ 59 | module.exports.fromURL = function (url, path, options, callback) { 60 | "use strict"; 61 | 62 | if(typeof options == "function") { 63 | callback = options; 64 | options = null; 65 | } 66 | options = options || {}; 67 | callback = callback || function(){}; 68 | if (options.clip) { 69 | if (typeof options.clip.x !== 'number') { 70 | options.clip.x = 0; 71 | } 72 | if (typeof options.clip.y !== 'number') { 73 | options.clip.y = 0; 74 | } 75 | } 76 | 77 | var n = Nightmare({ 78 | switches: { 'ignore-certificate-errors': true, 'force-device-scale-factor': options.scale ? options.scale.toString() : '1' }, 79 | show: typeof options.show === 'boolean' ? options.show : true, 80 | width: options.width || 1280, 81 | height: options.height || 720, 82 | frame: false 83 | }); 84 | 85 | n 86 | .viewport(options.width || 1280, options.height || 720) 87 | .goto(url) 88 | .wait(options.waitAfterSelector || "html") 89 | .wait(options.waitMilliseconds || 1000) 90 | .screenshot( 91 | path || undefined, 92 | options.clip || undefined 93 | ) 94 | .then(function (buff) { 95 | callback(null, buff); 96 | }) 97 | .catch(function (err) { 98 | return n 99 | .screenshot(path || undefined, options.clip || undefined) 100 | .then(function (buff) { 101 | callback(null, buff); 102 | }) 103 | .catch(function(){ callback(err) }); 104 | }) 105 | .finally(function() { 106 | return n.end(); 107 | }); 108 | }; 109 | 110 | /** 111 | * Navigates to a url and takes a screenshot 112 | * @param {String} html 113 | * @param {String|null} path - screenshot path 114 | * @param {Object} options 115 | * @param {Number=} options.scale 116 | * @param {Number=} options.width 117 | * @param {Number=} options.height 118 | * @param {String=} options.waitAfterSelector 119 | * @param {Number=} options.waitMilliseconds 120 | * @param {Object=} options.clip 121 | * @param {Number} options.clip.x 122 | * @param {Number} options.clip.y 123 | * @param {Number} options.clip.width 124 | * @param {Number} options.clip.height 125 | * @param {Object=} options.inject 126 | * @param {Number} options.inject.url 127 | * @param {String|{tag: String}|{id: String}|{className: String}|{jQuery: String}} options.inject.selector 128 | * 129 | * @param {screenshotCallback} callback 130 | */ 131 | module.exports.fromHTML = function (html, path, options, callback) { 132 | "use strict"; 133 | if(typeof options == "function") { 134 | callback = options; 135 | options = null; 136 | } 137 | 138 | options = options || {}; 139 | callback = callback || function(){}; 140 | options.inject = options.inject || {}; 141 | if (options.clip) { 142 | if (typeof options.clip.x !== 'number') { 143 | options.clip.x = 0; 144 | } 145 | if (typeof options.clip.y !== 'number') { 146 | options.clip.y = 0; 147 | } 148 | } 149 | 150 | var n = Nightmare({ 151 | switches: { 'force-device-scale-factor': options.scale ? options.scale.toString() : '1' }, 152 | show: typeof options.show === 'boolean' ? options.show : true, 153 | width: options.width || 1280, 154 | height: options.height || 720, 155 | frame: false 156 | }); 157 | 158 | n 159 | .viewport(options.width || 1280, options.height || 720) 160 | .goto(options.inject.url || "about:blank") 161 | .wait(options.waitAfterSelector || "html") 162 | .wait(options.waitMilliseconds || 1000) 163 | .injectHTML(options.inject.selector || "html", html) 164 | .wait(options.waitAfterSelector || "html") 165 | .wait(options.waitMilliseconds || 1000) 166 | .screenshot( 167 | path || undefined, 168 | options.clip || undefined 169 | ) 170 | .then(function (buff) { 171 | callback(null, buff); 172 | }) 173 | .catch(function (err) { 174 | callback(err); 175 | }); 176 | n.end(); 177 | }; 178 | --------------------------------------------------------------------------------