@@ -22,6 +22,10 @@ const HTML_DPI = 96
2222const MICRONS_INCH_RATIO = 25400
2323const MAX_EVENT_WAIT = 10000
2424
25+ const DEFAULT_OPTIONS = {
26+ closeWindow : true
27+ }
28+
2529class 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