Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
*.pdf
!example/businesscard.pdf
!examples/businesscard.pdf
node_modules
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# node-html-pdf
## HTML to PDF converter that uses phantomjs
![image](example/businesscard.png)
[Example Business Card](example/businesscard.pdf)
-> [and its Source file](example/businesscard.html)
![image](examples/businesscard.png)
[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)

Expand Down Expand Up @@ -102,6 +102,7 @@ config = {
"left": "1.5in"
},

paginationOffset: 1, // Override the initial pagination number
"header": {
"height": "45mm",
"contents": '<div style="text-align: center;">Author: Marc Bachmann</div>'
Expand Down Expand Up @@ -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
}
]

}
```

Expand Down
Binary file removed example/businesscard.pdf
Binary file not shown.
File renamed without changes.
File renamed without changes
File renamed without changes
19 changes: 19 additions & 0 deletions examples/serve-http/index.js
Original file line number Diff line number Diff line change
@@ -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)
})
89 changes: 50 additions & 39 deletions lib/pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Expand All @@ -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()
}
73 changes: 50 additions & 23 deletions lib/scripts/pdf_a4_portrait.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
})
}
}
Expand Down
30 changes: 30 additions & 0 deletions test/callback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", sans-serif;
background: #F0F0F0;
color: #333;
}

* {
border: 0;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="pageHeader">Header</div>
<div id="pageContent"></div>
<div id="pageFooter">Footer</div>

<script>
setTimeout(function () {
var res = window.callPhantom({success: 'true'})
document.getElementById('pageContent').innerHTML = JSON.stringify(res)
}, 1000)
</script>
</body>
</html>
Loading