class PDFExporter extends EventEmitter {
// ------------------------------------------------------------------
// ------------------ Public API ------------------------------------
// ------------------------------------------------------------------
/**
* @constructor
* @param opts
* @param {boolean} [opts.resilient=false] set to true to catch and
* log all uncaught exception but leave things running
* @param {object} [opts.loggers] Allows client to use its own logging implementation
*/
constructor (opts) {
super()
this.options = opts || {}
this.reslientMode = this.options.resilient || false
setLogger(this.options.loggers)
}
/**
* Starts the electron app
*
* @fires PDFExporter#charged
*/
start () {
electronApp = electron.app
electronApp.once('ready', () => {
this.isReady = true
/**
* emitted when the application is ready to process exports
* @event PDFExporter#charged
*/
this.emit('charged')
})
electronApp.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
// electronApp.quit()
}
})
// Stop Electron on SIG*
process.on('exit', code => electronApp.exit(code))
// Passthrough error handler to silence Electron GUI prompt
process.on('uncaughtException', err => {
if (this.reslientMode) {
console.log('Something unexpectedly bad happened ' +
'(but electron-pdf was initialized in resilient mode ' +
'and will remain operational):', err)
} else {
throw err
}
})
}
stop () {
console.log('Shutting down...')
electronApp.quit()
}
/**
* Load one or more HTML pages inside of a new window which is closed
* as soon as the PDFs are rendered.
*
* @param input {String} URL for filepath
* @param output {String} Filename
* @param args {array|Object} command line args - Can be an array of any
* supported args, or an object that is the result of running minimist.
* @param options {Object} export args - see ExportJob for list of options.
* These are options only supported by the API and not by the CLI
*/
createJob (input, output, args, options) {
if (!this.isReady) {
const msg = 'Electron is not ready, make sure to register an event listener for "charged" and invoke start()'
throw msg
}
// charge.js interprets the args, but this method should also support raw args
if (args instanceof Array) {
args = minimist(args, argOptions)
}
return new Promise((resolve, reject) => {
source.resolve(input, args).then(sources => {
resolve(new ExportJob(sources, output, args, options))
})
})
}
}
n/a
function info() { loggers[LEVELS.info](...arguments) }
n/a
function encode(args) { assert.strictEqual(typeof args, 'object', 'args must be an object') // stringify the args args = args ? encodeURIComponent(JSON.stringify(args)) : '' return args }
n/a
function urlWithArgs(urlOrFile, args) { args = encode(args) var u if (urlOrFile.indexOf('http') === 0) { var urlData = url.parse(urlOrFile) var hash = urlData.hash || args ? args : undefined u = url.format(assign(urlData, { hash: hash })) } else { // presumably a file url u = url.format({ protocol: 'file', pathname: path.resolve(urlOrFile), slashes: true, hash: args }) } return u }
...
*/
_loadURL (window, url) {
const loadOpts = {}
if (this.args.disableCache) {
loadOpts.extraHeaders += 'pragma: no-cache\n'
}
this.emit(`${RENDER_EVENT_PREFIX}loadurl`, { url: url })
window.loadURL(wargs.urlWithArgs(url, {}), loadOpts)
}
// Page Load & Rendering
/**
* Injects a wait if defined before calling the generateFunction
* Electron will apply the javascript we provide after the page is loaded,
...
function info() { loggers[LEVELS.info](...arguments) }
n/a
function debug() { loggers[LEVELS.debug](...arguments) }
n/a
function error() { loggers[LEVELS.error](...arguments) }
n/a
function info() { loggers[LEVELS.info](...arguments) }
n/a
function set(loggerObj) { if (loggerObj) { loggers[LEVELS.error] = loggerObj.error || loggers[LEVELS.error] loggers[LEVELS.info] = loggerObj.info || loggers[LEVELS.info] loggers[LEVELS.debug] = loggerObj.debug || loggers[LEVELS.debug] } }
...
cookies.split(';').forEach(function (c) {
const nameValue = c.split('=')
const cookie = {
url: urlObj.protocol + '//' + urlObj.host,
name: nameValue[0],
value: nameValue[1]
}
windowSessionCookies.set(cookie, function (err) {
if (err) {
errorLogger(err)
}
})
})
}
}
...
cleanupHungWindows(threshold) { const now = Date.now() const th = threshold || HUNG_WINDOW_THRESHOLD const hungWindows = _.filter(windowCache, e => now - e.lastUsed >= th) debugLogger(`checking hung windows-> ` + `total windows: ${_.size(windowCache)}, ` + `hung windows: ${_.size(hungWindows)}, ` + `threshold: ${th}`) _.forEach(hungWindows, e => { infoLogger('destroying hung window: ', e.id) const destroyable = e.job && e.window && !e.window.isDestroyed() if (destroyable) { const windowContext = { id: e.window.id, lifespan: now - e.lastUsed } e.job.emit('window.termination', windowContext) delete windowCache[e.id] e.job.destroy() } else { errorLogger('a window was left in the cache that was already destroyed, do proper cleanup') delete windowCache[e.id] } }) }
n/a
registerOpenWindow(exportJob) { const w = exportJob.window windowCache[w.id] = {id: w.id, job: exportJob, window: w, lastUsed: Date.now()} }
...
/**
* Render markdown or html to pdf
*/
render () {
this.emit(`${RENDER_EVENT_PREFIX}start`)
this._launchBrowserWindow()
const win = this.window
WindowMaid.registerOpenWindow(this)
// TODO: Check for different domains, this is meant to support only a single origin
const firstUrl = this.input[0]
this._setSessionCookies(this.args.cookies, firstUrl, win.webContents.session.cookies)
const windowEvents = []
// The same listeners can be used for each resource
...
removeWindow(id) { delete windowCache[id] }
...
* Resources managed:
* - this.window
*/
destroy () {
if (this.window) {
try {
logger(`destroying job with window: ${this.window.id}`)
WindowMaid.removeWindow(this.window.id)
this.window.close()
} finally {
this.window = undefined
}
}
}
...
touchWindow(id) { windowCache[id].lastUsed = Date.now() }
...
// Browser Setup
/**
*
* @private
*/
_initializeWindowForResource (landscape) {
WindowMaid.touchWindow(this.window.id)
// Reset the generated flag for each input URL because this same job/window
// can be reused in this scenario
this.generated = false
// args can be modified by the client, restore them for each resource
this.args = _.cloneDeep(this.originalArgs)
const dim = WindowTailor.setWindowDimensions(this.window, this.args.pageSize, landscape)
...
windowCount() { return _.size(windowCache) }
n/a
getPageDimensions(pageSize, landscape) { function pdfToPixels (inches) { return Math.floor(inches * HTML_DPI) } const pageDimensions = { 'A3': {x: pdfToPixels(11.7), y: pdfToPixels(16.5)}, 'A4': {x: pdfToPixels(8.3), y: pdfToPixels(11.7)}, 'A5': {x: pdfToPixels(5.8), y: pdfToPixels(8.3)}, 'Letter': {x: pdfToPixels(8.5), y: pdfToPixels(11)}, 'Legal': {x: pdfToPixels(8.5), y: pdfToPixels(14)}, 'Tabloid': {x: pdfToPixels(11), y: pdfToPixels(17)} } let pageDim if (typeof pageSize === 'object') { const xInches = pageSize.width / MICRONS_INCH_RATIO const yInches = pageSize.height / MICRONS_INCH_RATIO pageDim = { x: pdfToPixels(xInches), y: pdfToPixels(yInches) } } else { pageDim = pageDimensions[pageSize] if (landscape && pageDim.x < pageDim.y) { pageDim = {x: pageDim.y, y: pageDim.x} } } return pageDim }
...
* see
* http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions
* @param args
* @returns {Object} for BrowserWindow constructor
* @private
*/
_getBrowserConfiguration (args) {
const pageDim = WindowTailor.getPageDimensions(args.pageSize, args.landscape)
const defaultOpts = {
width: pageDim.x,
height: pageDim.y,
enableLargerThanScreen: true,
show: false,
center: true, // Display in center of screen,
...
setWindowDimensions(window, pageSize, landscape) { const pageDim = this.getPageDimensions(pageSize, landscape) var size = window.getSize() if (size[0] !== pageDim.x || size[1] !== pageDim.y) { window.setSize(pageDim.x, pageDim.y) return {dimensions: pageDim} } }
...
WindowMaid.touchWindow(this.window.id)
// Reset the generated flag for each input URL because this same job/window
// can be reused in this scenario
this.generated = false
// args can be modified by the client, restore them for each resource
this.args = _.cloneDeep(this.originalArgs)
const dim = WindowTailor.setWindowDimensions(this.window, this.args.pageSize, landscape
)
dim && this.emit('window.resize', dim)
}
/**
*
* @param {String} cookies - ';' delimited cookies, '=' delimited name/value
* pairs
...