diff --git a/.gitignore b/.gitignore
index 0e69430..135cfd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
*.pdf
-!example/businesscard.pdf
+!examples/businesscard.pdf
node_modules
diff --git a/README.md b/README.md
index fcaaef0..3f3a1d3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# node-html-pdf
## HTML to PDF converter that uses phantomjs
-
-[Example Business Card](example/businesscard.pdf)
- -> [and its Source file](example/businesscard.html)
+
+[Example Business Card](examples/businesscard.pdf)
+ -> [and its Source file](examples/businesscard.html)
[Example Receipt](http://imgr-static.s3-eu-west-1.amazonaws.com/order.pdf)
@@ -102,6 +102,7 @@ config = {
"left": "1.5in"
},
+ paginationOffset: 1, // Override the initial pagination number
"header": {
"height": "45mm",
"contents": '
Author: Marc Bachmann
'
@@ -133,12 +134,35 @@ config = {
"script": '/url', // Absolute path to a custom phantomjs script, use the file in lib/scripts as example
"timeout": 30000, // Timeout that will cancel phantomjs, in milliseconds
+ // Time we should wait after window load
+ // accepted values are 'manual', some delay in milliseconds or undefined to wait for a render event
+ "renderDelay": 1000,
+
// HTTP Headers that are used for requests
"httpHeaders": {
// e.g.
"Authorization": "Bearer ACEFAD8C-4B4D-4042-AB30-6C735F5BAC8B"
+ },
+
+ // To run Node application as Windows service
+ "childProcessOptions": {
+ "detached": true
}
+ // HTTP Cookies that are used for requests
+ "httpCookies": [
+ // e.g.
+ {
+ "name": "Valid-Cookie-Name", // required
+ "value": "Valid-Cookie-Value", // required
+ "domain": "localhost",
+ "path": "/foo", // required
+ "httponly": true,
+ "secure": false,
+ "expires": (new Date()).getTime() + (1000 * 60 * 60) // e.g. expires in 1 hour
+ }
+ ]
+
}
```
diff --git a/example/businesscard.pdf b/example/businesscard.pdf
deleted file mode 100644
index a6f8752..0000000
Binary files a/example/businesscard.pdf and /dev/null differ
diff --git a/example/businesscard.html b/examples/businesscard/businesscard.html
similarity index 100%
rename from example/businesscard.html
rename to examples/businesscard/businesscard.html
diff --git a/example/businesscard.png b/examples/businesscard/businesscard.png
similarity index 100%
rename from example/businesscard.png
rename to examples/businesscard/businesscard.png
diff --git a/example/image.png b/examples/businesscard/image.png
similarity index 100%
rename from example/image.png
rename to examples/businesscard/image.png
diff --git a/examples/serve-http/index.js b/examples/serve-http/index.js
new file mode 100644
index 0000000..2af62e6
--- /dev/null
+++ b/examples/serve-http/index.js
@@ -0,0 +1,19 @@
+const fs = require('fs')
+const http = require('http')
+const pdf = require('../../')
+const tmpl = fs.readFileSync(require.resolve('../businesscard/businesscard.html'), 'utf8')
+
+const server = http.createServer(function (req, res) {
+ if (req.url === '/favicon.ico') return res.end('404')
+ const html = tmpl.replace('{{image}}', `file://${require.resolve('../businesscard/image.png')}`)
+ pdf.create(html, {width: '50mm', height: '90mm'}).toStream((err, stream) => {
+ if (err) return res.end(err.stack)
+ res.setHeader('Content-type', 'application/pdf')
+ stream.pipe(res)
+ })
+})
+
+server.listen(8080, function (err) {
+ if (err) throw err
+ console.log('Listening on http://localhost:%s', server.address().port)
+})
diff --git a/lib/pdf.js b/lib/pdf.js
index 7211d0f..4ca1afd 100644
--- a/lib/pdf.js
+++ b/lib/pdf.js
@@ -63,7 +63,7 @@ PDF.prototype.toStream = function PdfToStream (callback) {
}
stream.on('end', function () {
- fs.unlink(res.filename, function (err) {
+ fs.unlink(res.filename, function unlinkPdfFile (err) {
if (err) console.log('html-pdf:', err)
})
})
@@ -84,54 +84,65 @@ PDF.prototype.toFile = function PdfToFile (filename, callback) {
}
PDF.prototype.exec = function PdfExec (callback) {
- var callbacked = false
- var child = childprocess.spawn(this.options.phantomPath, [].concat(this.options.phantomArgs, [this.script]))
- var stdout = []
+ var child = childprocess.spawn(this.options.phantomPath, [].concat(this.options.phantomArgs, [this.script]), this.options.childProcessOptions)
var stderr = []
+
var timeout = setTimeout(function execTimeout () {
- child.stdin.end()
- child.kill()
- if (!stderr.length) {
- stderr = [new Buffer('html-pdf: PDF generation timeout. Phantom.js script did not exit.')]
- }
+ respond(null, new Error('html-pdf: PDF generation timeout. Phantom.js script did not exit.'))
}, this.options.timeout)
- child.stdout.on('data', function (buffer) {
- return stdout.push(buffer)
- })
-
- child.stderr.on('data', function (buffer) {
+ function onError (buffer) {
stderr.push(buffer)
- child.stdin.end()
- return child.kill()
- })
+ }
+
+ function onData (buffer) {
+ var result
+ try {
+ var json = buffer.toString().trim()
+ if (json) result = JSON.parse(json)
+ } catch (err) {
+ // Proxy for debugging purposes
+ process.stdout.write(buffer)
+ }
+
+ if (result) respond(null, null, result)
+ }
- function exit (err, data) {
+ var callbacked = false
+ function respond (code, err, data) {
if (callbacked) return
callbacked = true
clearTimeout(timeout)
- if (err) return callback(err)
- return callback(null, data)
- }
- child.on('error', exit)
-
- child.on('exit', function (code) {
- if (code || stderr.length) {
- var err = new Error(Buffer.concat(stderr).toString() || 'html-pdf: Unknown Error')
- return exit(err)
- } else {
- try {
- var buff = Buffer.concat(stdout).toString()
- var data = (buff) != null ? buff.trim() : undefined
- data = JSON.parse(data)
- } catch (err) {
- return exit(err)
- }
- return exit(null, data)
+ // If we don't have an exit code, we kill the process, ignore stderr after this point
+ if (code === null) kill(child, onData, onError)
+
+ if (!data) {
+ if (!err && code) err = new Error("html-pdf: Received the exit code '" + code + "'")
+ else if (!err) err = new Error('html-pdf: Unknown Error')
+
+ var postfix = stderr.length ? '\n' + Buffer.concat(stderr).toString() : ''
+ if (postfix) err.message += postfix
+ return callback(err, null)
}
- })
- var res = JSON.stringify({html: this.html, options: this.options})
- return child.stdin.write(res + '\n', 'utf8')
+ callback(null, data)
+ }
+
+ child.stdout.on('data', onData)
+ child.stderr.on('data', onError)
+ child.on('error', function onError (err) { respond(null, err) })
+
+ // An exit event is most likely an error because we didn't get any data at this point
+ child.on('close', respond)
+ child.on('exit', respond)
+
+ var config = JSON.stringify({html: this.html, options: this.options})
+ child.stdin.write(config + '\n', 'utf8')
+ child.stdin.end()
+}
+
+function kill (child, onData, onError) {
+ child.stdin.end()
+ child.kill()
}
diff --git a/lib/scripts/pdf_a4_portrait.js b/lib/scripts/pdf_a4_portrait.js
index a9197c4..3542baa 100755
--- a/lib/scripts/pdf_a4_portrait.js
+++ b/lib/scripts/pdf_a4_portrait.js
@@ -33,6 +33,46 @@ if (!json.html || !json.html.trim()) exit('Did not receive any html')
var options = json.options
var page = webpage.create()
+// Completely load page & end process
+// ----------------------------------
+var rendered = false
+var renderTimeout
+
+// If renderDelay is manual, then listen for an event and don't automatically render
+if (options.renderDelay === 'manual') {
+ page.onCallback = function (message) {
+ setTimeout(renderNow, 0)
+ return message
+ }
+}
+
+page.onLoadFinished = function () {
+ if (options.renderDelay === 'manual') return
+ renderTimeout = setTimeout(renderNow, Math.floor(options.renderDelay) || 0)
+}
+
+function renderNow () {
+ if (rendered) return
+ rendered = true
+ clearTimeout(renderTimeout)
+ page.paperSize = definePaperSize(getContent(page), options)
+
+ var fileOptions = {
+ type: options.type || 'pdf',
+ quality: options.quality || 75
+ }
+
+ var filename = options.filename || (options.directory || '/tmp') + '/html-pdf-' + system.pid + '.' + fileOptions.type
+ page.render(filename, fileOptions)
+
+ // Output to parent process
+ system.stdout.write(JSON.stringify({filename: filename}))
+ exit(null)
+}
+
+// Set Content and begin loading
+// -----------------------------
+if (options.httpCookies) page.cookies = options.httpCookies
if (options.httpHeaders) page.customHeaders = options.httpHeaders
if (options.viewportSize) page.viewportSize = options.viewportSize
if (options.zoomFactor) page.zoomFactor = options.zoomFactor
@@ -51,25 +91,6 @@ setTimeout(function () {
exit('Force timeout')
}, timeout)
-// Completely load page & end process
-// ----------------------------------
-page.onLoadFinished = function (status) {
- // The paperSize object must be set at once
- page.paperSize = definePaperSize(getContent(page), options)
-
- // Output to parent process
- var fileOptions = {
- type: options.type || 'pdf',
- quality: options.quality || 75
- }
-
- var filename = options.filename || (options.directory || '/tmp') + '/html-pdf-' + system.pid + '.' + fileOptions.type
- page.render(filename, fileOptions)
- system.stdout.write(JSON.stringify({filename: filename}))
-
- exit(null)
-}
-
// Returns a hash of HTML content
// ------------------------------
function getContent (page) {
@@ -147,17 +168,23 @@ function createSection (section, content, options) {
options = options[section] || {}
var c = content[section] || {}
var o = options.contents
+ var paginationOffset = Math.floor(options.paginationOffset) || 0
+
if (typeof o !== 'object') o = {default: o}
return {
height: options.height,
contents: phantom.callback(function (pageNum, numPages) {
var html = o[pageNum] || c[pageNum]
- if (pageNum === 1 && !html) html = o.first || c.first
- if (pageNum === numPages && !html) html = o.last || c.last
+
+ var pageNumFinal = pageNum + paginationOffset
+ var numPagesFinal = numPages + paginationOffset
+
+ if (pageNumFinal === 1 && !html) html = o.first || c.first
+ if (numPagesFinal === numPages && !html) html = o.last || c.last
return (html || o.default || c.default || '')
- .replace(/{{page}}/g, pageNum)
- .replace(/{{pages}}/g, numPages) + content.styles
+ .replace(/{{page}}/g, pageNumFinal)
+ .replace(/{{pages}}/g, numPagesFinal) + content.styles
})
}
}
diff --git a/test/callback.html b/test/callback.html
new file mode 100644
index 0000000..5684a35
--- /dev/null
+++ b/test/callback.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/index.js b/test/index.js
index bc8841f..a48b42e 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,5 +1,7 @@
var test = require('tape')
var tapSpec = require('tap-spec')
+function noop (err) { if (err) throw err }
+
test.createStream()
.pipe(tapSpec())
.pipe(process.stdout)
@@ -42,7 +44,7 @@ test('pdf.create(html[, options]).toFile([filename, ]callback)', function (t) {
pdf.create(html).toFile(function (err, pdf) {
t.error(err)
t.assert(typeof pdf.filename === 'string', `toFile(callback) returns {filename: '${pdf.filename}'} as second cb argument`)
- fs.unlink(pdf.filename)
+ fs.unlink(pdf.filename, noop)
})
var file = path.join(__dirname, 'simple.pdf')
@@ -72,6 +74,32 @@ test('pdf.create(html, {directory: "/tmp"}).toBuffer(callback)', function (t) {
})
})
+test('pdf.create(html, {renderDelay: 1000}).toBuffer(callback)', function (t) {
+ t.plan(2)
+
+ pdf.create(html, {renderDelay: 1000}).toBuffer(function (err, pdf) {
+ t.error(err)
+ t.assert(Buffer.isBuffer(pdf), 'still returns after renderDelay')
+ })
+})
+
+test('window.callPhantom renders page', function (t) {
+ t.plan(3)
+
+ var callbackHtml = fs.readFileSync(path.join(__dirname, 'callback.html'), 'utf8')
+ var file = path.join(__dirname, 'callback.pdf')
+ var startTime = new Date().getTime()
+
+ pdf.create(callbackHtml, {renderDelay: 'manual'}).toFile(file, function (err, pdf) {
+ var endTime = new Date().getTime()
+ t.error(err)
+
+ var time = endTime - startTime
+ t.assert(time > 1000 && time < 2000, 'rendered in response to callPhantom')
+ t.assert(fs.existsSync(file), 'writes the file to the given destination')
+ })
+})
+
test('pdf.create(html[, options]).toStream(callback)', function (t) {
t.plan(3)
@@ -82,7 +110,7 @@ test('pdf.create(html[, options]).toStream(callback)', function (t) {
stream.pipe(fs.createWriteStream(destination))
stream.on('end', function () {
t.assert(fs.existsSync(destination), 'toStream returns a working readable stream')
- fs.unlink(destination)
+ fs.unlink(destination, noop)
})
})
})
@@ -93,11 +121,11 @@ test('pdf.create(html[, options]).toStream(callback)', function (t) {
test('allows custom html and css', function (t) {
t.plan(3)
- var template = path.join(__dirname, '../example/businesscard.html')
+ var template = path.join(__dirname, '../examples/businesscard/businesscard.html')
var filename = template.replace('.html', '.pdf')
var templateHtml = fs.readFileSync(template, 'utf8')
- var image = path.join('file://', __dirname, '../example/image.png')
+ var image = path.join('file://', __dirname, '../examples/businesscard/image.png')
templateHtml = templateHtml.replace('{{image}}', image)
var options = {
@@ -194,3 +222,36 @@ test('load external js', function (t) {
t.assert(fs.existsSync(pdf.filename), 'Saves the pdf with a custom page size and footer')
})
})
+
+test('load with cookies js', function (t) {
+ t.plan(3)
+
+ var server = require('http').createServer(function (req, res) {
+ res.write(req.headers.cookie)
+ res.end()
+ })
+
+ server.listen(0, function (err) {
+ t.error(err, 'http server for iframe started')
+
+ var port = server.address().port
+ var filename = path.join(__dirname, 'cookies.pdf')
+ pdf.create(`
+ here is an iframe which receives the cookies
+
+
+ `, {
+ httpCookies: [{
+ name: 'Valid-Cookie-Name',
+ value: 'Valid-Cookie-Value',
+ domain: 'localhost',
+ path: '/'
+ }]
+ })
+ .toFile(filename, function (error, pdf) {
+ server.close()
+ t.error(error, 'There must be no render error')
+ t.assert(fs.existsSync(pdf.filename), 'Saves the pdf')
+ })
+ })
+})