Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
419e1a9
Apply standard style to all files
marcbachmann Feb 1, 2017
3bd5552
Merge pull request #230 from marcbachmann/lint-all-files
marcbachmann Feb 1, 2017
eb1d4c3
Remove CoffeScript dependency
chris-kobrzak Feb 14, 2017
e5eff67
Merge pull request #236 from chris-kobrzak/coffee-cleanup
marcbachmann Feb 14, 2017
543a918
Add missing comma in example config in README (#262)
Sorgel Mar 22, 2017
f5fe191
Move the example directory to examples
marcbachmann Sep 26, 2017
b672aa8
Improve error handling and pipe stdout to process.stdout
marcbachmann Sep 26, 2017
ef03a37
Add cliOptions option and pass it to child process
zmedgyes Jun 19, 2017
9c38449
Update README.md
zmedgyes Jul 10, 2017
4a29881
Added pagination start page
mdelorimier Jan 27, 2017
5d0a30c
Simplify paginationStartPage and rename to paginationOffset
marcbachmann Mar 21, 2017
81b38e4
Add child process close event handler
marcbachmann Sep 26, 2017
027bfb9
Add callback to unlink functions to get rid of deprecation messages
marcbachmann Sep 26, 2017
63a8d28
Option for waiting for event to trigger
alexbor May 13, 2016
91b359c
Improve `renderDelay` option and add to README.
marcbachmann Mar 21, 2017
7dbc346
Add renderDelay option
marcbachmann Sep 26, 2017
42e2dfb
Add test to ensure that renderDelay will render after a specific time
chriskinsman Mar 22, 2017
6830076
Add `manual` renderDelay support using `window.callPhantom`
marcbachmann Sep 27, 2017
84ac0a2
Add http cookie support
seyfert Feb 3, 2017
f1f78a5
Add test for http cookie support
marcbachmann Mar 22, 2017
52f2e2d
Rename cliOptions to childProcessOptions
marcbachmann Sep 27, 2017
3127891
Version 2.2.0
marcbachmann Sep 27, 2017
b0018c4
Fix two of three broken links
Jason-Cooke Oct 3, 2017
63ba98f
Re-add business card example pdf
marcbachmann Oct 3, 2017
89a41e3
Extract business card test into separate file
marcbachmann Oct 3, 2017
9e14ef5
Fix issue with last header appearing on all pages
manishbhatt94 May 15, 2019
a0f4500
A better way for handling PhantomJS exits
Apr 10, 2018
9349b6f
Added null checker
Apr 10, 2018
4e15719
Satisfying test for TravisCI
micahbule Jun 21, 2018
36a551c
Fixed error handling
micahbule Jun 21, 2018
85e2470
chore: Add package-lock.json
marcbachmann Apr 20, 2021
236a297
fix: Prevent local file access by default using the `localUrlAccess: …
marcbachmann Apr 20, 2021
296313e
chore: Update circleci config
marcbachmann Apr 20, 2021
13b438c
3.0.0
marcbachmann Apr 20, 2021
7f054b6
Fix options.base example path to avoid #508
tymekg Oct 4, 2019
c12d697
Invert localUrlAccess to fix https://www.npmjs.com/advisories/1095
fakelag May 7, 2021
bac0f69
3.0.1
marcbachmann May 7, 2021
7e8c02a
chore: Upgrade tap devDependencies
marcbachmann May 7, 2021
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
10 changes: 10 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2.1

orbs:
node: circleci/[email protected]

workflows:
matrix-tests:
jobs:
- node/test:
version: 15.3.0
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
44 changes: 35 additions & 9 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/businesscard.png)
[Example Business Card](examples/businesscard/businesscard.pdf)
-> [and its Source file](examples/businesscard/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 All @@ -110,35 +111,60 @@ config = {
"height": "28mm",
"contents": {
first: 'Cover page',
2: 'Second page' // Any page number is working. 1-based index
2: 'Second page', // Any page number is working. 1-based index
default: '<span style="color: #444;">{{page}}</span>/<span>{{pages}}</span>', // fallback value
last: 'Last Page'
}
},


// Rendering options
"base": "file:///home/www/your-asset-path", // Base path that's used to load files (images, css, js) when they aren't referenced using a host
"base": "file:///home/www/your-asset-path/", // Base path that's used to load files (images, css, js) when they aren't referenced using a host

// Zooming option, can be used to scale images if `options.type` is not pdf
"zoomFactor": "1", // default is 1

// File options
"type": "pdf", // allowed file types: png, jpeg, pdf
"quality": "75", // only used for types png & jpeg
"type": "pdf", // allowed file types: png, jpeg, pdf
"quality": "75", // only used for types png & jpeg

// Script options
"phantomPath": "./node_modules/phantomjs/bin/phantomjs", // PhantomJS binary which should get downloaded automatically
"phantomArgs": [], // array of strings used as phantomjs args e.g. ["--ignore-ssl-errors=yes"]
"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
"localUrlAccess": false, // Prevent local file:// access by passing '--local-url-access=false' to phantomjs
// For security reasons you should keep the default value if you render arbritary html/js.
"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
3 changes: 0 additions & 3 deletions circle.yml

This file was deleted.

File renamed without changes.
Binary file not shown.
File renamed without changes
File renamed without changes
28 changes: 28 additions & 0 deletions examples/businesscard/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var test = require('tape')
var pdf = require('../../')
var path = require('path')
var fs = require('fs')

test('allows custom html and css', function (t) {
t.plan(3)

var template = path.join(__dirname, 'businesscard.html')
var filename = template.replace('.html', '.pdf')
var templateHtml = fs.readFileSync(template, 'utf8')

var image = path.join('file://', __dirname, 'image.png')
templateHtml = templateHtml.replace('{{image}}', image)

var options = {
width: '50mm',
height: '90mm'
}

pdf
.create(templateHtml, options)
.toFile(filename, function (err, pdf) {
t.error(err)
t.assert(pdf.filename, 'Returns the filename')
t.assert(fs.existsSync(pdf.filename), 'Saves the file to the desired destination')
})
})
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)
})
104 changes: 65 additions & 39 deletions lib/pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ function PDF (html, options) {
if (this.options.filename) this.options.filename = path.resolve(this.options.filename)
if (!this.options.phantomPath) this.options.phantomPath = phantomjs && phantomjs.path
this.options.phantomArgs = this.options.phantomArgs || []

if (!this.options.localUrlAccess) this.options.phantomArgs.push('--local-url-access=false')
assert(this.options.phantomPath, "html-pdf: Failed to load PhantomJS module. You have to set the path to the PhantomJS binary using 'options.phantomPath'")
assert(typeof this.html === 'string' && this.html.length, "html-pdf: Can't create a pdf without an html string")
this.options.timeout = parseInt(this.options.timeout) || 30000
this.options.timeout = parseInt(this.options.timeout, 10) || 30000
}

PDF.prototype.toBuffer = function PdfToBuffer (callback) {
Expand All @@ -63,7 +65,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 +86,78 @@ 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)
// If we don't have an exit code, we kill the process, ignore stderr after this point
if (code === null) kill(child, onData, onError)

// Since code has a truthy/falsy value of either 0 or 1, check for existence first.
// Ignore if code has a value of 0 since that means PhantomJS has executed and exited successfully.
// Also, as per your script and standards, having a code value of 1 means one can always assume that
// an error occured.
if (((typeof code !== 'undefined' && code !== null) && code !== 0) || err) {
var error = null

if (err) {
// Rudimentary checking if err is an instance of the Error class
error = err instanceof Error ? err : new Error(err)
} else {
// This is to catch the edge case of having a exit code value of 1 but having no error
error = new Error('html-pdf: Unknown Error')
}
return exit(null, data)

// Append anything caught from the stderr
var postfix = stderr.length ? '\n' + Buffer.concat(stderr).toString() : ''
if (postfix) error.message += postfix

return callback(error)
}
})

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 (pageNumFinal === 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
Loading