jsreport-phantom-pdf
Version:
jsreport recipe which is rendering pdf from html using phantomjs
284 lines (233 loc) • 10.4 kB
JavaScript
/*!
* Copyright(c) 2017 Jan Blaha
*
* Recipe rendering pdf files using phantomjs.
*/
var Promise = require('bluebird')
var Conversion = require('phantom-html-to-pdf')
var toArrayAsync = Promise.promisify(require('stream-to-array'))
var fs = require('fs')
var path = require('path')
var conversion
var readdirAsync = Promise.promisify(fs.readdir)
var defaultPhantomjsVersion = '1.9.8'
var Phantom = function (reporter, definition) {
this.reporter = reporter
this.definition = definition
this.phantomjsCrawled = false
this.allowLocalFilesAccess = definition.options.hasOwnProperty('allowLocalFilesAccess') ? definition.options.allowLocalFilesAccess : false
reporter.extensionsManager.recipes.push({
name: 'phantom-pdf',
execute: Phantom.prototype.execute.bind(this)
})
reporter.documentStore.registerComplexType('PhantomType', {
margin: { type: 'Edm.String', schema: { type: 'object' } },
header: { type: 'Edm.String', document: { extension: 'html', engine: true } },
headerHeight: { type: 'Edm.String' },
footer: { type: 'Edm.String', document: { extension: 'html', engine: true } },
footerHeight: { type: 'Edm.String' },
orientation: { type: 'Edm.String' },
format: { type: 'Edm.String' },
width: { type: 'Edm.String' },
height: { type: 'Edm.String' },
printDelay: { type: 'Edm.Int32' },
resourceTimeout: { type: 'Edm.Int32' },
phantomjsVersion: { type: 'Edm.String' },
customPhantomJS: { type: 'Edm.Boolean' },
blockJavaScript: { type: 'Edm.Boolean' },
waitForJS: { type: 'Edm.Boolean' },
fitToPage: { type: 'Edm.Boolean' }
})
if (reporter.documentStore.model.entityTypes['TemplateType']) {
reporter.documentStore.model.entityTypes['TemplateType'].phantom = { type: 'jsreport.PhantomType' }
}
}
Phantom.prototype.init = function () {
var npm = path.join(this.reporter.options.rootDirectory, 'node_modules')
var self = this
self.reporter.initializeListeners.add(self.definition.name, () => {
if (self.reporter.express) {
self.reporter.express.exposeOptionsToApi(self.definition.name, {
phantoms: self.definition.options.phantoms
})
}
})
if (this.reporter.execution) {
var phantomjsPackage
try {
phantomjsPackage = require(this.reporter.execution.resourcePath('phantomjsPackage.json'))
} catch (e) {
// in case of error when trying to read the resource then just fall back to normal fs crawling
}
if (phantomjsPackage) {
var names = phantomjsPackage.name.split('phantomjs-exact-')
this.definition.options.phantoms = [{
version: names.length === 2 ? names[1].replace(/-/g, '.') : defaultPhantomjsVersion
}]
return Promise.resolve()
}
}
function crawlAvailablePhantomVersions () {
return readdirAsync(npm).then(function (files) {
return files.filter(function (f) {
return f.indexOf('phantomjs-exact-') !== -1
}).map(function (f) {
return {
path: path.join(npm, f),
version: f.replace('phantomjs-exact-', '').replace(/-/g, '.')
}
})
})
}
return crawlAvailablePhantomVersions().then(function (versions) {
self.phantomjsCrawled = true
self.definition.options.phantoms = versions
self.definition.options.phantoms.splice(0, 0, {
version: defaultPhantomjsVersion
})
if (self.definition.options.defaultPhantomjsVersion && self.definition.options.defaultPhantomjsVersion !== defaultPhantomjsVersion) {
var defPhantomInstance
self.definition.options.phantoms = self.definition.options.phantoms.filter(function (p) {
if (p.version === self.definition.options.defaultPhantomjsVersion) {
defPhantomInstance = p
return false
} else {
return true
}
})
if (!defPhantomInstance) {
throw new Error('defaultPhantomjsVersion ' + self.definition.options.defaultPhantomjsVersion + ' was not found')
}
self.definition.options.phantoms.splice(0, 0, defPhantomInstance)
}
}).then(function () {
if (self.reporter.compilation) {
if (self.definition.options.defaultPhantomjsVersion && self.definition.options.defaultPhantomjsVersion !== defaultPhantomjsVersion) {
self.reporter.compilation.resourceInTemp('phantomjs.exe', require(self.definition.options.phantoms[0].path).path)
self.reporter.compilation.resource('phantomjsPackage.json', path.join(path.dirname(require.resolve(self.definition.options.phantoms[0].path)), 'package.json'))
} else {
self.reporter.compilation.resourceInTemp('phantomjs.exe', require('phantomjs').path)
self.reporter.compilation.resource('phantomjsPackage.json', path.join(path.dirname(require.resolve('phantomjs')), '../package.json'))
}
}
})
}
Phantom.prototype.execute = function (request, response) {
var self = this
var reporter = self.reporter
var margin
request.template.phantom = request.template.phantom || {}
margin = request.template.phantom.margin
if (margin) {
if (typeof margin === 'string') {
try {
// margin value should always be a string or object, never a number
margin = isNaN(parseInt(margin, 10)) ? JSON.parse(margin) : String(JSON.parse(margin))
} catch (e) {}
}
}
request.template.phantom.paperSize = {
width: request.template.phantom.width,
height: request.template.phantom.height,
headerHeight: request.template.phantom.headerHeight,
footerHeight: request.template.phantom.footerHeight,
format: request.template.phantom.format,
orientation: request.template.phantom.orientation,
margin: margin
}
request.template.phantom.allowLocalFilesAccess = self.allowLocalFilesAccess
request.template.phantom.settings = {
javascriptEnabled: request.template.phantom.blockJavaScript !== true,
resourceTimeout: request.template.phantom.resourceTimeout
}
if (request.template.phantom.waitForJS) {
request.template.phantom.waitForJS = JSON.parse(request.template.phantom.waitForJS)
}
request.template.phantom.fitToPage = request.template.phantom.fitToPage != null ? request.template.phantom.fitToPage : false
request.template.phantom.waitForJSVarName = 'JSREPORT_READY_TO_START'
request.template.phantom.html = response.content.toString()
request.template.phantom.timeout = reporter.getAvailableRenderTimeout(request, self.definition.options.timeout)
if (reporter.execution) {
if (self.phantomjsCrawled) {
request.template.phantom.phantomPath = require('phantomjs').path
} else {
request.template.phantom.phantomPath = reporter.execution.resourceTempPath('phantomjs.exe')
}
} else {
if (request.template.phantom.customPhantomJS === true) {
request.template.phantom.phantomPath = require.main.require('phantomjs-prebuilt').path
}
if (request.template.phantom.phantomjsVersion) {
var phantom = self.definition.options.phantoms.filter(function (p) {
return p.version === request.template.phantom.phantomjsVersion
})
// default doesn't have a path
if (phantom.length === 1 && phantom[0].path) {
request.template.phantom.phantomPath = require(phantom[0].path).path
}
} else {
if (self.definition.options.defaultPhantomjsVersion && self.definition.options.defaultPhantomjsVersion !== defaultPhantomjsVersion) {
request.template.phantom.phantomPath = require(self.definition.options.phantoms[0].path).path
}
}
}
function processPart (options, req, type) {
if (!options[type]) {
return Promise.resolve()
}
reporter.logger.debug('Starting child request to render pdf ' + type, req)
// do an anonymous render
const template = {
content: options[type],
engine: req.template.engine,
recipe: 'html',
helpers: req.template.helpers
}
return reporter.render({
template
}, req).then(function (res) {
options[type] = res.content.toString()
})
}
return processPart(request.template.phantom, request, 'header').then(function () {
return processPart(request.template.phantom, request, 'footer')
}).then(function () {
return Promise.promisify(conversion)(request.template.phantom)
}).then(function (res) {
res.logs.forEach(function (m) {
reporter.logger[m.level](m.message, { timestamp: m.timestamp.getTime(), ...request })
})
response.meta.contentType = 'application/pdf'
response.meta.fileExtension = 'pdf'
response.meta.numberOfPages = res.numberOfPages
return toArrayAsync(res.stream).then(function (arr) {
response.content = Buffer.concat(arr)
reporter.logger.debug('phantom-pdf recipe finished with ' + res.numberOfPages + ' pages generated', request)
})
})
}
module.exports = function (reporter, definition) {
definition.options = Object.assign({}, reporter.options.phantom, definition.options)
definition.options.tmpDir = reporter.options.tempAutoCleanupDirectory
definition.options.strategy = definition.options.strategy || 'dedicated-process'
var timeoutProp
if (definition.options.timeout != null) {
if (reporter.options.phantom && reporter.options.phantom.timeout != null) {
timeoutProp = 'phantom.timeout'
} else {
timeoutProp = 'extensions.phantom-pdf.timeout'
}
}
if (definition.options.timeout != null && reporter.options.reportTimeout != null) {
reporter.logger.warn('"' + timeoutProp + '" configuration is ignored when "reportTimeout" is set')
} else if (definition.options.timeout != null) {
reporter.logger.warn('"' + timeoutProp + '" configuration is deprecated and will be removed in the future, please use "reportTimeout" instead')
}
conversion = Conversion(definition.options)
reporter[definition.name] = new Phantom(reporter, definition)
Object.defineProperty(reporter[definition.name], 'conversion', {
get: function () { return conversion },
set: function (val) { conversion = val }
})
return reporter[definition.name].init()
}