Skip to content

Commit 4d8b36b

Browse files
Issue 132 (#133)
* Added event observer and argument mutation * refactoring, removed window references in events because it felt wrong and there is no use case for it yet #132 * fixed documentation in README #132
1 parent 75bf7eb commit 4d8b36b

File tree

3 files changed

+245
-53
lines changed

3 files changed

+245
-53
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,32 @@ In your application, at the point which the view is ready for rendering
143143
document.body.dispatchEvent(new Event('view-ready'))
144144
```
145145

146+
#### Observing your own event
147+
148+
If the page you are rending is under your control, and you wish to modify the behavior
149+
of the rendering process you can use a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent)
150+
and an observer that will be triggered after the view is ready but before it is captured.
151+
152+
##### your-page.html
153+
154+
```javascript
155+
document.body.dispatchEvent(new CustomEvent('view-ready', { detail: {layout: landscape} }))
156+
```
157+
158+
##### your-exporter.js
159+
As an example, suppose you wanted to change the orientation of the PDF
160+
161+
```javascript
162+
job.observeReadyEvent( (detail) => {
163+
return new Promise( (resolve,reject) => {
164+
if( detail && detail.landscape ){
165+
job.changeArgValue('landscape', true)
166+
}
167+
resolve()
168+
})
169+
})
170+
```
171+
146172
All Available Options
147173
-----
148174

lib/exportJob.js

Lines changed: 217 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const HTML_DPI = 96
2222
const MICRONS_INCH_RATIO = 25400
2323
const MAX_EVENT_WAIT = 10000
2424

25+
const DEFAULT_OPTIONS = {
26+
closeWindow: true
27+
}
28+
2529
class ExportJob extends EventEmitter {
2630

2731
/**
@@ -31,17 +35,23 @@ class ExportJob extends EventEmitter {
3135
* @param output The name of the file to export to. If the extension is
3236
* '.png' then a PNG image will be generated instead of a PDF.
3337
* @param args {Object} the minimist arg object
38+
* @param options {Object} electron-pdf options
39+
* @param options.closeWindow default:true - If set to false, the window will
40+
* not be closed when the job is complete. This can be useful if you wish
41+
* to reuse a window by passing it to the render function.
3442
*
35-
* @fires ExportJob#pdf-complete after each PDF is available on the
36-
* filesystem
37-
* @fires ExportJob#job-complete after all PDFs are available on the
43+
* @fires ExportJob#export-complete after each export is available on the
3844
* filesystem
45+
* @fires ExportJob#job-complete after all export resources are available on
46+
* the filesystem
3947
*/
40-
constructor (input, output, args) {
48+
constructor (input, output, args, options) {
4149
super()
4250
this.input = _.isArray(input) ? input : [input]
4351
this.output = output
4452
this.args = args
53+
this.options = _.extend({}, DEFAULT_OPTIONS, options)
54+
logger('job options:', this.options)
4555

4656
if (_.startsWith(this.args.pageSize, '{')) {
4757
this.args.pageSize = JSON.parse(this.args.pageSize)
@@ -55,31 +65,60 @@ class ExportJob extends EventEmitter {
5565
/**
5666
* Render markdown or html to pdf
5767
*/
58-
render () {
68+
render (window) {
69+
logger('render starting...')
5970
const args = this.args
6071

6172
const win = this._launchBrowserWindow(args)
73+
this.window = win
74+
6275
// TODO: Check for different domains, this is meant to support only a single origin
6376
const firstUrl = this.input[0]
6477
this._setSessionCookies(args.cookies, firstUrl, win.webContents.session.cookies)
6578

6679
const windowEvents = []
6780
this.input.forEach((uriPath, i) => {
6881
windowEvents.push((pageDone) => {
69-
this._loadURL(win, uriPath, args)
7082
const targetFile = this._getTargetFile(i)
7183
const generateFunction = this._generateOutput.bind(this, win, targetFile, args, pageDone)
7284
const waitFunction = this._waitForPage.bind(this, win, generateFunction, args.outputWait)
85+
7386
win.webContents.removeAllListeners('did-finish-load')
7487
win.webContents.on('did-finish-load', waitFunction)
88+
win.webContents.on('did-fail-load', (r) => {
89+
// http://electron.atom.io/docs/api/web-contents/#event-did-fail-load
90+
logger('load failure!')
91+
})
92+
win.webContents.on('did-start-loading', (r) => {
93+
// logger('loading!')
94+
})
95+
win.webContents.on('dom-ready', (r) => {
96+
logger('dom ready!')
97+
})
98+
win.webContents.on('did-get-response-details',
99+
function (event,
100+
status,
101+
newURL,
102+
originalURL,
103+
httpResponseCode,
104+
requestMethod,
105+
referrer,
106+
headers,
107+
resourceType) {
108+
// logger('resource complete:', httpResponseCode, newURL)
109+
})
110+
111+
this._loadURL(win, uriPath, args)
75112
})
76113
})
77114

78115
async.series(windowEvents, (err, results) => {
79-
win.close()
116+
if (this.options.closeWindow) {
117+
win.close()
118+
}
80119
/**
81120
* PDF Generation Event - fires when all PDFs have been persisted to disk
82-
* @event PDFExporter#jobj-complete
121+
* @event PDFExporter#job-complete
83122
* @type {object}
84123
* @property {String} results - array of generated pdf file locations
85124
* @property {Object} error - If an error occurred, null otherwise
@@ -88,6 +127,41 @@ class ExportJob extends EventEmitter {
88127
})
89128
}
90129

130+
/**
131+
* If the html page requested emits a CustomEvent
132+
* (https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent)
133+
* you may want to act upon the information it contains.
134+
*
135+
* Use this method to register your own observer.
136+
*
137+
* @param handler {function} A callback that is passed the following:
138+
* args[0]: the details object from CustomEvent
139+
*
140+
* @fires PDFExporter#window.observer.start when the observer is invoked
141+
* @fires PDFExporter#window.observer.timeout when the promise is not observed by
142+
* the maximum wait time (default: 10 seconds). The process will continue on and
143+
* capture the page, it is up to the caller to handle this event accordingly.
144+
* @fires PDFExporter#window.observer.end when the observer fulfills the promise
145+
*/
146+
observeReadyEvent (handler) {
147+
this.readyEventObserver = handler
148+
}
149+
150+
/**
151+
* Change one of the arguments provided in the constructor.
152+
* Intended to be used with observeReadyEvent
153+
*
154+
* Note that electron-pdf uses the fully named arguments and none of the
155+
* aliases (i.e. 'landscape' and not 'l'). Even if you used an alias during
156+
* initialization make sure you pass the named argument here.
157+
*
158+
* @param arg The full name of the argument (i.e 'landscape')
159+
* @param value The new value
160+
*/
161+
changeArgValue (arg, value) {
162+
this.args[arg] = value
163+
}
164+
91165
// ***************************************************************************
92166
// ************************* Private Functions *******************************
93167
// ***************************************************************************
@@ -245,80 +319,172 @@ class ExportJob extends EventEmitter {
245319
this._executeJSListener(eventName, generateFunction, window)
246320
}
247321

322+
/**
323+
* responsible for executing JS in the browser that will wait for the page
324+
* to emit an event before capturing the page.
325+
*
326+
* @param eventName
327+
* @param generateFunction
328+
* @param window
329+
* @private
330+
*/
248331
_executeJSListener (eventName, generateFunction, window) {
249332
const cmd = `var ipc = require('electron').ipcRenderer
250-
var body = document.body
251-
body.addEventListener('${eventName}', () => ipc.send('READY_TO_RENDER'))`
333+
var body = document.body
334+
body.addEventListener('${eventName}',
335+
event => {
336+
//Detail will only exist if a CustomEvent was emitted
337+
ipc.send('READY_TO_RENDER', event.detail)
338+
}
339+
)`
252340

253341
// Don't let things hang forever
254342
const timeout = setTimeout(() => {
255343
this.emit('window.event.wait.timeout', {eventName: eventName})
256344
electron.ipcMain.removeAllListeners('READY_TO_RENDER')
257345
generateFunction()
258346
}, this.args.outputWait > 0 ? this.args.outputWait : MAX_EVENT_WAIT)
259-
this.once('window.capture.start', () => clearTimeout(timeout))
347+
348+
this.once('window.event.wait.end', () => clearTimeout(timeout))
260349

261350
window.webContents.executeJavaScript(cmd)
262351
}
263352

353+
/**
354+
* Listen for the browser to emit the READY_TO_RENDER event and when it does
355+
* emit our own event so the max load timer is removed.
356+
*
357+
* @param eventName this is whatever the client provided
358+
* @param generateFunction _generateOutput with all of its arguments bound
359+
* @private
360+
*/
264361
_attachIPCListener (eventName, generateFunction) {
265362
this.emit('window.event.wait.start', {eventName: eventName})
266-
electron.ipcMain.once('READY_TO_RENDER', generateFunction)
363+
364+
electron.ipcMain.once('READY_TO_RENDER', (name, customEventDetail) => {
365+
this.emit('window.event.wait.end', {})
366+
367+
if (this.readyEventObserver) {
368+
this._triggerReadyEventObserver(customEventDetail, generateFunction)
369+
} else {
370+
generateFunction()
371+
}
372+
})
373+
}
374+
375+
/**
376+
* If an event observer was set it is invoked before the generateFunction.
377+
*
378+
* This function must ensure that the observer does not hang.
379+
*
380+
* @param customEventDetail detail from the DOMs CustomEvent
381+
* @param generateFunction callback function to capture the page
382+
* @private
383+
*/
384+
_triggerReadyEventObserver (customEventDetail, generateFunction) {
385+
/**
386+
* fires right before a readyEventObserver is invoked
387+
* @event PDFExporter#window.observer.start
388+
* @type {object}
389+
* @property {String} detail - The CustomEvent detail
390+
*/
391+
this.emit('window.observer.start', {detail: customEventDetail})
392+
393+
const timeout = setTimeout(() => {
394+
/**
395+
* Fires when an observer times out
396+
* @event PDFExporter#window.observer.start
397+
* @type {object}
398+
*/
399+
this.emit('window.observer.timeout', {})
400+
generateFunction()
401+
}, MAX_EVENT_WAIT)
402+
403+
this.readyEventObserver(customEventDetail).then(() => {
404+
/**
405+
* Fires when an observer fulfills it's promise
406+
* @event PDFExporter#window.observer.end
407+
* @type {object}
408+
*/
409+
this.emit('window.observer.end', {})
410+
clearTimeout(timeout)
411+
generateFunction()
412+
})
267413
}
268414

269415
// Output
270416

271417
/**
272-
* Create the PDF or PNG file
418+
* Create the PDF or PNG file.
419+
*
420+
* Because of timeouts and promises being resolved this function
421+
* is implemented to be idempotent
422+
*
273423
* @param window
274424
* @param outputFile
275425
*
276426
* @private
277427
*/
278428
_generateOutput (window, outputFile, args, done) {
279-
this.emit('window.capture.start', {})
429+
if (!this.generated) {
430+
this.generated = true
431+
this.emit('window.capture.start', {})
432+
433+
if (outputFile.toLowerCase().endsWith('.png')) {
434+
this._captureImage(window, outputFile, done)
435+
} else {
436+
this._capturePDF(args, window, done, outputFile)
437+
}
438+
}
439+
}
280440

281-
const png = outputFile.toLowerCase().endsWith('.png')
282-
// Image (PNG)
283-
if (png) {
284-
window.capturePage(function (image) {
441+
_captureImage (window, outputFile, done) {
442+
window.webContents.capturePage(image => {
443+
const target = path.resolve(outputFile)
444+
fs.writeFile(target, image.toPNG(), function (err) {
445+
this.emit('window.capture.end', {file: target, error: err})
446+
this.emit('export-complete', {file: target})
447+
// REMOVE pdf-complete in 2.0 - keeping for backwards compatibility
448+
this.emit('pdf-complete', {file: target})
449+
done(err, target)
450+
}.bind(this))
451+
})
452+
}
453+
454+
_capturePDF (args, window, done, outputFile) {
455+
// TODO: Validate these because if they're wrong a non-obvious error will occur
456+
const pdfOptions = {
457+
marginsType: args.marginsType,
458+
printBackground: args.printBackground,
459+
printSelectionOnly: args.printSelectionOnly,
460+
pageSize: args.pageSize,
461+
landscape: args.landscape
462+
}
463+
464+
window.webContents.printToPDF(pdfOptions, (err, data) => {
465+
if (err) {
466+
this.emit('window.capture.end', {error: err})
467+
done(err)
468+
} else {
285469
const target = path.resolve(outputFile)
286-
fs.writeFile(target, image.toPNG(), function (err) {
470+
fs.writeFile(target, data, (err) => {
471+
if (!err) {
472+
// REMOVE in 2.0 - keeping for backwards compatibility
473+
this.emit('pdf-complete', {file: target})
474+
/**
475+
* Generation Event - fires when an export has be persisted to
476+
* disk
477+
* @event PDFExporter#export-complete
478+
* @type {object}
479+
* @property {String} file - Path to the File
480+
*/
481+
this.emit('export-complete', {file: target})
482+
}
287483
this.emit('window.capture.end', {file: target, error: err})
484+
done(err, target)
288485
})
289-
})
290-
} else { // PDF
291-
const pdfOptions = {
292-
marginsType: args.marginsType,
293-
printBackground: args.printBackground,
294-
printSelectionOnly: args.printSelectionOnly,
295-
pageSize: args.pageSize,
296-
landscape: args.landscape
297486
}
298-
299-
window.webContents.printToPDF(pdfOptions, (err, data) => {
300-
if (err) {
301-
this.emit('window.capture.end', {error: err})
302-
done(err)
303-
} else {
304-
const target = path.resolve(outputFile)
305-
fs.writeFile(target, data, (err) => {
306-
if (!err) {
307-
/**
308-
* PDF Generation Event - fires when a PDF has be persisted to
309-
* disk
310-
* @event PDFExporter#pdf-complete
311-
* @type {object}
312-
* @property {String} file - Path to the PDF File
313-
*/
314-
this.emit('pdf-complete', {file: target})
315-
}
316-
this.emit('window.capture.end', {file: target, error: err})
317-
done(err, target)
318-
})
319-
}
320-
})
321-
}
487+
})
322488
}
323489

324490
/**

0 commit comments

Comments
 (0)