├── .travis.yml ├── .gitignore ├── package.json ├── child.js ├── render.js ├── tests └── pdf.js ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.9" 5 | 6 | services: phantomjs 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | google.pdf 2 | yahoo.pdf 3 | tests/shoflo.js 4 | shoflo.js 5 | google2.pdf 6 | html.pdf 7 | node_modules 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "TJ Krusinski", 4 | "email": "tjkrus@gmail.com" 5 | }, 6 | "contributors": [ 7 | "Steffen Persch <@n3o77>" 8 | ], 9 | "name": "nodepdf", 10 | "description": "Down and dirty PDF rendering in Node.js", 11 | "main": "./index.js", 12 | "version": "1.3.2", 13 | "keywords": [ 14 | "phantomjs", 15 | "pdf", 16 | "make a pdf" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/TJkrusinski/NodePDF" 21 | }, 22 | "homepage": "https://github.com/TJkrusinski/NodePDF", 23 | "dependencies": { 24 | "shell-quote": "1.4.x" 25 | }, 26 | "devDependencies": { 27 | "mocha": "*" 28 | }, 29 | "scripts": { 30 | "test":"mocha tests" 31 | }, 32 | "optionalDependencies": {}, 33 | "engines": { 34 | "node": "*" 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /child.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var child = require('child_process'); 4 | var shq = require('shell-quote').quote; 5 | var which = process.platform == 'win32' ? 'where' : 'which'; 6 | var fs = require('fs'); 7 | 8 | /** 9 | * Execute the command 10 | * 11 | * @param {String} url 12 | * @param {String} filename 13 | * @param {Object} options 14 | * @param {Function} [cb] 15 | */ 16 | exports.exec = function(url, filename, options, cb){ 17 | var key; 18 | var stdin = ['phantomjs']; 19 | var optsFile = __dirname + '/' + Date.now() + '.js'; 20 | 21 | stdin.push(options.args); 22 | stdin.push(shq([ 23 | __dirname+'/render.js', 24 | url, 25 | filename, 26 | optsFile, 27 | ])); 28 | 29 | fs.writeFile(optsFile, 'module.exports='+JSON.stringify(options), 'UTF-8', function(err) { 30 | if(err) { 31 | return console.log(err); 32 | } 33 | 34 | var c = child.exec(stdin.join(' ')); 35 | c.on('exit', function () { 36 | fs.unlinkSync(optsFile); 37 | }); 38 | 39 | c.on('error', function () { 40 | fs.unlinkSync(optsFile); 41 | }); 42 | cb(c); 43 | }); 44 | }; 45 | 46 | /** 47 | * Check to see if the environment has a command 48 | */ 49 | 50 | exports.supports = function(cb, cmd) { 51 | var stream = child.exec(which+' '+(cmd || 'phantomjs'), function(err, stdo, stde){ 52 | return cb(!!stdo.toString()); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | var system = require('system'); 3 | 4 | // ADDRESS CHANGE IN PARAMETERS HANDLING FOR PHANTOMJS 2.x 5 | var phantomargs; 6 | if(phantom.version.major >= 2) { 7 | // remove the script name from the arguments 8 | phantomargs = system.args.slice(1,system.args.length); 9 | } else { 10 | phantomargs = phantom.args; 11 | } 12 | 13 | var contentsCb = function(pobj) { 14 | if (!pobj || !pobj.contents) return; 15 | var contentStr = pobj.contents; 16 | pobj.contents = phantom.callback(function(currentPage, pages) { 17 | return contentStr.replace(/\{currentPage\}/g, currentPage).replace(/\{pages\}/g, pages); 18 | }); 19 | }; 20 | 21 | var args; 22 | 23 | if (phantom.args) { 24 | args = phantom.args 25 | } else { 26 | args = require('system').args; 27 | } 28 | 29 | if (args.length < 2) { 30 | console.log('11'); 31 | console.log('incorrect args'); 32 | phantom.exit(); 33 | } else { 34 | var optionsFile = args[3]; 35 | var options = require(optionsFile); 36 | 37 | contentsCb(options.paperSize.header); 38 | contentsCb(options.paperSize.footer); 39 | 40 | if (options.cookies) { 41 | options.cookies.forEach(page.addCookie); 42 | delete options.cookies; 43 | } 44 | 45 | for (var key in options) { 46 | if (options.hasOwnProperty(key) && page.hasOwnProperty(key)) { 47 | page[key] = options[key]; 48 | } 49 | } 50 | 51 | if (!options.content) page.open(args[0]); 52 | 53 | page.onLoadFinished = function(status) { 54 | if(status !== 'success'){ 55 | console.log('error: ' + status); 56 | console.log('unable to load the address!'); 57 | phantom.exit(); 58 | } else { 59 | window.setTimeout(function(){ 60 | page.render(args[1], { format: 'pdf', quality: options.outputQuality || '80' }); 61 | console.log('success'); 62 | phantom.exit(); 63 | }, options.captureDelay || 400); 64 | }; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /tests/pdf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | var Pdf = require('../index.js'); 6 | var child = require('../child.js'); 7 | var FP = process.env.PWD || process.cwd() || __dirname; 8 | 9 | describe('child#supports()', function(){ 10 | it('checks to see if phantomjs is installed', function(d){ 11 | child.supports(function(exists){ 12 | assert.ok(exists); 13 | d(); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('child#supports()', function(){ 19 | it('checks to see if asdfhijk is installed', function(d){ 20 | child.supports(function(exists){ 21 | assert.ok(!exists); 22 | d(); 23 | }, 'asdfhijk'); 24 | }); 25 | }); 26 | 27 | // google vs yahoo 28 | 29 | describe('pdf#done() 1', function(){ 30 | it('fires done when ', function(d){ 31 | this.timeout(20000); 32 | var pdf1 = new Pdf('http://www.google.com', 'google.pdf'); 33 | pdf1.on('done', function(msg){ 34 | assert.ok(msg); 35 | assert.equal(FP + '/google.pdf', msg); 36 | d(); 37 | }); 38 | pdf1.on('error', function(msg){ 39 | assert.ok(false); 40 | d(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('pdf#content', function() { 46 | it('fires done when content is loaded', function(d) { 47 | this.timeout(20000); 48 | var pdf1 = new Pdf(null, 'html.pdf', { 49 | 'content': 'Test' 50 | }); 51 | pdf1.on('done', function(msg){ 52 | assert.ok(msg); 53 | assert.equal(FP + '/html.pdf', msg); 54 | d(); 55 | }); 56 | pdf1.on('error', function(msg){ 57 | assert.ok(false); 58 | d(); 59 | }); 60 | }) 61 | }); 62 | 63 | describe('pdf#done() 2', function(){ 64 | it('fires done when ', function(d){ 65 | this.timeout(20000); 66 | var pdf2 = new Pdf('http://yahoo.com', 'yahoo.pdf', { 67 | 'viewportSize': { 68 | 'width': 3000, 69 | 'height': 9000 70 | }, 71 | 'paperSize': { 72 | 'pageFormat': 'A4', 73 | 'margin': { 74 | 'top': '2cm' 75 | }, 76 | 'header': { 77 | 'height': '4cm', 78 | 'contents': 'HEADER {currentPage} / {pages}' 79 | }, 80 | 'footer': { 81 | 'height': '4cm', 82 | 'contents': 'FOOTER {currentPage} / {pages}' 83 | } 84 | }, 85 | 'zoomFactor': 1.1, 86 | 'cookies': [ 87 | { 88 | 'name': 'Valid-Cookie-Name 1', /* required property */ 89 | 'value': 'Valid-Cookie-Value 1', /* required property */ 90 | 'domain': 'localhost', /* required property */ 91 | 'path': '/foo', 92 | 'httponly': true, 93 | 'secure': false, 94 | 'expires': (new Date()).getTime() + (1000 * 60 * 60) /* <-- expires in 1 hour */ 95 | }, 96 | { 97 | 'name': 'Valid-Cookie-Name 2', 98 | 'value': 'Valid-Cookie-Value 2', 99 | 'domain': 'localhost' 100 | } 101 | ] 102 | }); 103 | pdf2.on('done', function(msg){ 104 | assert.ok(msg); 105 | assert.equal(FP + '/yahoo.pdf', msg); 106 | d(); 107 | }); 108 | pdf2.on('error', function(msg){ 109 | assert.ok(false); 110 | d(); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('pdf#render()', function(){ 116 | it('renders a pdf with a callback style', function(d){ 117 | this.timeout(20000); 118 | Pdf.render('http://www.google.com', 'google2.pdf', function(err, file){ 119 | assert.equal(err, null); 120 | assert.equal(FP + '/google2.pdf', file); 121 | d(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var child = require('./child.js'); 4 | var Emitter = require('events').EventEmitter; 5 | 6 | var defaults = { 7 | 'viewportSize': { 8 | 'width': 2880, 9 | 'height': 1440 10 | }, 11 | 'paperSize': { 12 | 'format': 'A4', 13 | 'orientation': 'portrait', 14 | 'margin': { 15 | 'top': '1cm', 16 | 'right': '1cm', 17 | 'bottom': '1cm', 18 | 'left': '1cm' 19 | } 20 | }, 21 | 'outputQuality': '80', //0 to 100 22 | 'zoomFactor': 1, 23 | 'args': '', 24 | 'captureDelay': 400 25 | }; 26 | 27 | //code from https://github.com/rxaviers/cldr 28 | var merge = function() { 29 | var destination = {}, 30 | 31 | sources = [].slice.call(arguments, 0); 32 | sources.forEach(function(source) { 33 | var prop; 34 | 35 | for (prop in source) { 36 | if (prop in destination && Array.isArray(destination[prop])) { 37 | // Concat Arrays 38 | destination[prop] = destination[prop].concat(source[prop]); 39 | } else if (prop in destination && typeof destination[prop] === "object") { 40 | // Merge Objects 41 | destination[prop] = merge(destination[prop], source[prop] ); 42 | } else { 43 | // Set new values 44 | destination[prop] = source[prop]; 45 | }; 46 | }; 47 | }); 48 | 49 | return destination; 50 | }; 51 | 52 | /** 53 | * Constructor interface 54 | */ 55 | 56 | var exports = module.exports = Pdf; 57 | 58 | function Pdf (url, fileName, opts){ 59 | var self = this; 60 | 61 | this.url = url; 62 | this.fileName = fileName; 63 | this.filePath = process.env.PWD || process.cwd() || __dirname; 64 | this.options = merge(defaults, opts); 65 | 66 | child.supports(function(support){ 67 | if (!support) self.emit('error', 'PhantomJS not installed'); 68 | if (support) self.run(); 69 | }); 70 | 71 | return this; 72 | }; 73 | 74 | /** 75 | * Inherit the event emitter 76 | */ 77 | 78 | Pdf.prototype = Object.create(Emitter.prototype); 79 | 80 | /** 81 | * Run the process 82 | * 83 | * @method run 84 | */ 85 | 86 | Pdf.prototype.run = function() { 87 | var self = this; 88 | child.exec(this.url, this.fileName, this.options, function (ps) { 89 | ps.on('exit', function(c, d){ 90 | if (c != 0) return self.emit('error', 'PDF conversion failed with exit of '+c); 91 | 92 | var targetFilePath = self.fileName; 93 | 94 | if (targetFilePath[0] != '/') { 95 | targetFilePath = self.filePath + '/' + targetFilePath; 96 | }; 97 | 98 | self.emit('done', targetFilePath); 99 | }); 100 | 101 | ps.stdout.on('data', function(std){ 102 | self.emit('stdout', std); 103 | }); 104 | 105 | ps.stderr.on('data', function(std){ 106 | self.emit('stderr', std); 107 | }); 108 | }); 109 | }; 110 | 111 | /** 112 | * Use callback style rendering 113 | * 114 | * @param {String} address 115 | * @param {String} file 116 | * @param {Options} address 117 | * @param {Function} callback 118 | */ 119 | 120 | exports.render = function(address, file, options, callback) { 121 | var filePath = process.env.PWD || process.cwd() || __dirname; 122 | 123 | if (typeof options == 'function') { 124 | callback = options; 125 | options = defaults; 126 | }; 127 | 128 | options = merge(defaults, options); 129 | 130 | child.supports(function(support){ 131 | if (!support) callback(new Error('PhantomJS not installed')); 132 | 133 | child.exec(address, file, options, function (ps) { 134 | ps.on('exit', function(c, d){ 135 | if (c) return callback(new Error('Conversion failed with exit of '+c)); 136 | 137 | var targetFilePath = file; 138 | 139 | if (targetFilePath[0] != '/') 140 | targetFilePath = filePath + '/' + targetFilePath; 141 | 142 | return callback(null, targetFilePath); 143 | }); 144 | }); 145 | }); 146 | }; 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodePDF 2 | 3 | Down and dirty PDF rendering in Node.js 4 | 5 | [![Build Status](https://travis-ci.org/TJkrusinski/NodePDF.png?branch=master)](https://travis-ci.org/TJkrusinski/NodePDF) 6 | 7 | ## Installation 8 | 9 | ```` 10 | npm install nodepdf 11 | ```` 12 | 13 | ## Dependencies 14 | 15 | 1. PhantomJS 16 | 17 | ## Contsructor API 18 | 19 | You can use NodePDF two ways, one is using a contstructor that returns an instance of `EventEmitter`. 20 | 21 | ```` javascript 22 | var NodePDF = require('nodepdf'); 23 | 24 | // last argument is optional, sets the width and height for the viewport to render the pdf from. (see additional options) 25 | var pdf = new NodePDF('http://www.google.com', 'google.pdf', { 26 | 'viewportSize': { 27 | 'width': 1440, 28 | 'height': 900 29 | }, 30 | 'args': '--debug=true' 31 | }); 32 | 33 | pdf.on('error', function(msg){ 34 | console.log(msg); 35 | }); 36 | 37 | pdf.on('done', function(pathToFile){ 38 | console.log(pathToFile); 39 | }); 40 | 41 | // listen for stdout from phantomjs 42 | pdf.on('stdout', function(stdout){ 43 | // handle 44 | }); 45 | 46 | // listen for stderr from phantomjs 47 | pdf.on('stderr', function(stderr){ 48 | // handle 49 | }); 50 | 51 | ```` 52 | Or set the content directly instead of using a URL: 53 | ```` javascript 54 | var pdf = new NodePDF(null, 'google.pdf', { 55 | 'content': 'google', 56 | 'viewportSize': { 57 | 'width': 1440, 58 | 'height': 900 59 | }, 60 | }); 61 | ```` 62 | 63 | 64 | You can set the header and footer contents as well: 65 | ```` javascript 66 | var NodePDF = require('nodepdf'); 67 | var pdf = new NodePDF('http://yahoo.com', 'yahoo.pdf', { 68 | 'viewportSize': { 69 | 'width': 3000, 70 | 'height': 9000 71 | }, 72 | 'paperSize': { 73 | 'pageFormat': 'A4', 74 | 'margin': { 75 | 'top': '2cm' 76 | }, 77 | 'header': { 78 | 'height': '1cm', 79 | 'contents': 'HEADER {currentPage} / {pages}' // If you have 2 pages the result looks like this: HEADER 1 / 2 80 | }, 81 | 'footer': { 82 | 'height': '1cm', 83 | 'contents': 'FOOTER {currentPage} / {pages}' 84 | } 85 | }, 86 | 'outputQuality': '80', 87 | 'zoomFactor': 1.1 88 | }); 89 | ```` 90 | 91 | ## Callback API 92 | 93 | The callback API follows node standard callback signatures using the `render()` method. 94 | 95 | ```` javascript 96 | var NodePDF = require('nodepdf'); 97 | 98 | // options is optional, sets the width and height for the viewport to render the pdf from. (see additional options) 99 | NodePDF.render('http://www.google.com', 'google.pdf', options, function(err, filePath){ 100 | // handle error and filepath 101 | }); 102 | 103 | // use default options 104 | NodePDF.render('http://www.google.com', 'google.pdf', function(err, filePath){ 105 | // handle error and filepath 106 | }); 107 | 108 | ```` 109 | 110 | As soon the content option is set, the URL is ignored even if you set one. 111 | 112 | ## Options + Defaults 113 | ```` javascript 114 | { 115 | 'viewportSize': { 116 | 'width': 2880, 117 | 'height': 1440 118 | }, 119 | 'paperSize': { 120 | 'format': 'A4', 121 | 'orientation': 'portrait', 122 | 'margin': { 123 | 'top': '1cm', 124 | 'right': '1cm', 125 | 'bottom': '1cm', 126 | 'left': '1cm' 127 | } 128 | }, 129 | 'outputQuality': '80', //set embedded image quality 0 - 100 130 | 'zoomFactor': 1, 131 | 'args': '', 132 | 'captureDelay': 400 133 | 134 | } 135 | ```` 136 | 137 | You can set all the properties from here: http://phantomjs.org/api/webpage/ 138 | 139 | ## Cookies 140 | 141 | ```` javascript 142 | var NodePDF = require('nodepdf'); 143 | var pdf = new Pdf('http://yahoo.com', 'yahoo.pdf', { 144 | 'cookies': [ 145 | { 146 | 'name': 'Valid-Cookie-Name 1', /* required property */ 147 | 'value': 'Valid-Cookie-Value 1', /* required property */ 148 | 'domain': 'localhost', /* required property */ 149 | 'path': '/foo', 150 | 'httponly': true, 151 | 'secure': false, 152 | 'expires': (new Date()).getTime() + (1000 * 60 * 60) /* <-- expires in 1 hour */ 153 | }, 154 | { 155 | 'name': 'Valid-Cookie-Name 2', 156 | 'value': 'Valid-Cookie-Value 2', 157 | 'domain': 'localhost' 158 | } 159 | ] 160 | }); 161 | ```` 162 | 163 | PhantomJS Cookie Object description: http://phantomjs.org/api/webpage/property/cookies.html 164 | 165 | ## License 166 | 167 | (The MIT License) 168 | 169 | Copyright (c) 2013 TJ Krusinski <tj@shoflo.tv> 170 | 171 | Permission is hereby granted, free of charge, to any person obtaining 172 | a copy of this software and associated documentation files (the 173 | 'Software'), to deal in the Software without restriction, including 174 | without limitation the rights to use, copy, modify, merge, publish, 175 | distribute, sublicense, and/or sell copies of the Software, and to 176 | permit persons to whom the Software is furnished to do so, subject to 177 | the following conditions: 178 | 179 | The above copyright notice and this permission notice shall be 180 | included in all copies or substantial portions of the Software. 181 | 182 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 183 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 184 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 185 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 186 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 187 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 188 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 189 | --------------------------------------------------------------------------------