Skip to content

Commit dbe77d5

Browse files
Issue 143 in memory output, ava and electron version bumps (#146)
* Added a job option to only hold the PDF in memory and not persist to disk, fixes #143 * got rid of bluebird and bumped electron version #143 * factored out common events #143 * added a destroy method to cleanup windows left open to address seg fault when streaming buffer objects from the client #143
1 parent 44deb4c commit dbe77d5

File tree

7 files changed

+158
-54
lines changed

7 files changed

+158
-54
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,18 @@ exporter.start()
6868
```javascript
6969
app.post('/pdfexport', function(req,res){
7070
// derive job arguments from request here
71-
exporter.createJob(source, target, options).then( job => {
71+
//
72+
const jobOptions = {
73+
/**
74+
r.results[] will contain the following based on inMemory
75+
false: the fully qualified path to a PDF file on disk
76+
true: The Buffer Object as returned by Electron
77+
78+
Note: the default is false, this can not be set using the CLI
79+
*/
80+
inMemory: false
81+
}
82+
exporter.createJob(source, target, options, jobOptions).then( job => {
7283
job.on('job-complete', (r) => {
7384
console.log('pdf files:', r.results)
7485
// Process the PDF file(s) here
@@ -78,6 +89,25 @@ app.post('/pdfexport', function(req,res){
7889
})
7990
```
8091

92+
#### Using an in memory Buffer
93+
94+
If you set the `inMemory` setting to true, you must also set `closeWindow=true`
95+
or you will get a segmentation fault anytime the window is closed before the buffer
96+
is sent on the response. You then need to invoke `job.destroy` to close the window.
97+
98+
Sample Code:
99+
```javascript
100+
const jobOptions = { inMemory: true, closeWindow: false }
101+
exporter.createJob(source, target, options, jobOptions).then( job => {
102+
job.on('job-complete', (r) => {
103+
//Send the Buffer here
104+
process.nextTick(() => {job.destroy()})
105+
})
106+
})
107+
```
108+
109+
## Events
110+
81111
The API is designed to emit noteworthy events rather than use callbacks.
82112
Full documentation of all events is a work in progress.
83113

lib/exportJob.js

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const url = require('url')
1010
const _ = require('lodash')
1111
const EventEmitter = require('eventemitter2').EventEmitter2
1212
const electron = require('electron')
13-
var uuid = require('uuid')
13+
const uuid = require('uuid')
1414

1515
// Logging
1616
const debug = require('debug')
@@ -24,7 +24,8 @@ const MICRONS_INCH_RATIO = 25400
2424
const MAX_EVENT_WAIT = 10000
2525

2626
const DEFAULT_OPTIONS = {
27-
closeWindow: true
27+
closeWindow: true,
28+
inMemory: false
2829
}
2930

3031
class ExportJob extends EventEmitter {
@@ -33,18 +34,22 @@ class ExportJob extends EventEmitter {
3334
*
3435
* @param {Array} input The path to the HTML or url, or a markdown file
3536
* with 'md' or 'markdown' extension.
37+
*
3638
* @param output The name of the file to export to. If the extension is
3739
* '.png' then a PNG image will be generated instead of a PDF.
40+
*
3841
* @param args {Object} the minimist arg object
42+
*
3943
* @param options {Object} electron-pdf options
4044
* @param options.closeWindow default:true - If set to false, the window will
4145
* not be closed when the job is complete. This can be useful if you wish
4246
* to reuse a window by passing it to the render function.
47+
* @param options.inMemory default:false - If set to true then `output` will be
48+
* ignored and the results array will contain the Buffer object of the PDF
4349
*
44-
* @fires ExportJob#export-complete after each export is available on the
45-
* filesystem
46-
* @fires ExportJob#job-complete after all export resources are available on
47-
* the filesystem
50+
* @fires ExportJob#window.capture.end after each resource is captured (use this with inMemory)
51+
* @fires ExportJob#export-complete after each resource is available in memory or on the filesystem
52+
* @fires ExportJob#job-complete after all export resources are available on the filesystem
4853
*/
4954
constructor (input, output, args, options) {
5055
super()
@@ -139,10 +144,12 @@ class ExportJob extends EventEmitter {
139144
* args[0]: the details object from CustomEvent
140145
*
141146
* @fires PDFExporter#window.observer.start when the observer is invoked
142-
* @fires PDFExporter#window.observer.timeout when the promise is not observed by
143-
* the maximum wait time (default: 10 seconds). The process will continue on and
144-
* capture the page, it is up to the caller to handle this event accordingly.
145-
* @fires PDFExporter#window.observer.end when the observer fulfills the promise
147+
* @fires PDFExporter#window.observer.timeout when the promise is not
148+
* observed by the maximum wait time (default: 10 seconds). The process
149+
* will continue on and capture the page, it is up to the caller to handle
150+
* this event accordingly.
151+
* @fires PDFExporter#window.observer.end when the observer fulfills the
152+
* promise
146153
*/
147154
observeReadyEvent (handler) {
148155
this.readyEventObserver = handler
@@ -163,6 +170,18 @@ class ExportJob extends EventEmitter {
163170
this.args[arg] = value
164171
}
165172

173+
/**
174+
* Invoke this method to ensure that any allocated resources are destroyed
175+
* Resources managed:
176+
* - this.window
177+
*/
178+
destroy () {
179+
logger('job is tearing down')
180+
if (this.window) {
181+
this.window.close()
182+
}
183+
}
184+
166185
// ***************************************************************************
167186
// ************************* Private Functions *******************************
168187
// ***************************************************************************
@@ -464,23 +483,23 @@ class ExportJob extends EventEmitter {
464483
window.webContents.executeJavaScript('document.documentElement.outerHTML', result => {
465484
const target = path.resolve(outputFile)
466485
fs.writeFile(target, result, function (err) {
467-
this.emit('window.capture.end', {file: target, error: err})
468-
this.emit('export-complete', {file: target})
469-
done(err, target)
486+
this._emitResourceEvents(err, target, done)
470487
}.bind(this))
471488
})
472489
}
473490

474491
_captureImage (window, outputFile, done) {
475492
window.webContents.capturePage(image => {
476-
const target = path.resolve(outputFile)
477-
fs.writeFile(target, image.toPNG(), function (err) {
478-
this.emit('window.capture.end', {file: target, error: err})
479-
this.emit('export-complete', {file: target})
480-
// REMOVE pdf-complete in 2.0 - keeping for backwards compatibility
481-
this.emit('pdf-complete', {file: target})
482-
done(err, target)
483-
}.bind(this))
493+
// http://electron.atom.io/docs/api/native-image/#imagetopng
494+
const pngBuffer = image.toPNG()
495+
if (this.options.inMemory) {
496+
this._emitResourceEvents(undefined, pngBuffer, done)
497+
} else {
498+
const target = path.resolve(outputFile)
499+
fs.writeFile(target, pngBuffer, function (err) {
500+
this._emitResourceEvents(err, target, done)
501+
}.bind(this))
502+
}
484503
})
485504
}
486505

@@ -493,31 +512,55 @@ class ExportJob extends EventEmitter {
493512
pageSize: args.pageSize,
494513
landscape: args.landscape
495514
}
515+
window.webContents.printToPDF(pdfOptions, this._handlePDF.bind(this, outputFile, done))
516+
}
496517

497-
window.webContents.printToPDF(pdfOptions, (err, data) => {
498-
if (err) {
499-
this.emit('window.capture.end', {error: err})
500-
done(err)
501-
} else {
502-
const target = path.resolve(outputFile)
503-
fs.writeFile(target, data, (err) => {
504-
if (!err) {
505-
// REMOVE in 2.0 - keeping for backwards compatibility
506-
this.emit('pdf-complete', {file: target})
507-
/**
508-
* Generation Event - fires when an export has be persisted to
509-
* disk
510-
* @event PDFExporter#export-complete
511-
* @type {object}
512-
* @property {String} file - Path to the File
513-
*/
514-
this.emit('export-complete', {file: target})
515-
}
516-
this.emit('window.capture.end', {file: target, error: err})
517-
done(err, target)
518-
})
519-
}
520-
})
518+
/**
519+
* The callback function for when printToPDF is complete
520+
* @param err
521+
* @param data
522+
* @private
523+
*/
524+
_handlePDF (outputFile, done, err, data) {
525+
if (this.options.inMemory || err) {
526+
this._emitResourceEvents(err, data, done)
527+
} else {
528+
const target = path.resolve(outputFile)
529+
fs.writeFile(target, data, (fileWriteErr) => {
530+
// REMOVE in 2.0 - keeping for backwards compatibility
531+
this.emit('pdf-complete', {file: target, error: fileWriteErr})
532+
this._emitResourceEvents(fileWriteErr, target, done)
533+
})
534+
}
535+
}
536+
537+
/**
538+
* Emits events when a resource has been captured or an error has occurred
539+
* while attempting the capture.
540+
*
541+
* @param err
542+
* @param data
543+
* @param done
544+
* @private
545+
*/
546+
_emitResourceEvents (err, data, done) {
547+
/**
548+
* Window Event - fires when an export has captured the window (succesfully
549+
* or not)
550+
* @event PDFExporter#export-complete
551+
* @type {object}
552+
* @property {Buffer} data - The Buffer holding the PDF file
553+
* @property {Object} error - If an error occurred, undefined otherwise
554+
*/
555+
this.emit('window.capture.end', {data: data, error: err})
556+
/**
557+
* Generation Event - fires when an export has be persisted to disk
558+
* @event PDFExporter#export-complete
559+
* @type {object}
560+
* @property {String} file - Path to the File
561+
*/
562+
this.emit('export-complete', {data: data})
563+
done(err, data)
521564
}
522565

523566
/**

lib/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict'
22

33
var EventEmitter = require('events').EventEmitter
4-
var Promise = require('bluebird')
54

65
var electron = require('electron')
76
var minimist = require('minimist')
@@ -65,7 +64,8 @@ class PDFExporter extends EventEmitter {
6564
* @param output {String} Filename
6665
* @param args {array|Object} command line args - Can be an array of any
6766
* supported args, or an object that is the result of running minimist.
68-
*
67+
* @param options {Object} export args - see ExportJob for list of options.
68+
* These are options only supported by the API and not by the CLI
6969
*/
7070
createJob (input, output, args, options) {
7171
if (!this.isReady) {

lib/source.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ var _ = require('lodash')
44
var path = require('path')
55
// var wargs = require('./args')
66
var markdownToHTMLPath = require('./markdown')
7-
var Promise = require('bluebird')
87

98
// Logging
109
const debug = require('debug')

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
},
3636
"homepage": "https://github.com/fraserxu/electron-pdf",
3737
"devDependencies": {
38-
"ava": "^0.16.0",
38+
"ava": "^0.17.0",
3939
"jasmine": "^2.5.2",
4040
"standard": "^8.4.0",
4141
"tap-diff": "^0.1.1",
@@ -44,9 +44,8 @@
4444
},
4545
"dependencies": {
4646
"async": "^2.0.1",
47-
"bluebird": "^3.4.6",
4847
"debug": "^2.3.2",
49-
"electron": "^1.4.6",
48+
"electron": "^1.4.12",
5049
"eventemitter2": "^2.1.3",
5150
"github-markdown-css": "^2.0.9",
5251
"highlight.js": "^9.0.0",

test/exportJob-test.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ const micronDims = {
99
height: 228600
1010
}
1111

12+
const args = {}
1213
const options = {
1314
pageSize: JSON.stringify(micronDims)
1415
}
1516

16-
let job = new ExportJob(['input'], 'output.pdf', options)
17+
let job = new ExportJob(['input'], 'output.pdf', args, options)
1718

1819
// Construction
1920
/**
@@ -94,6 +95,38 @@ test('setSessionCookie_multiple', t => {
9495
})
9596
})
9697

98+
// PDF Completion Tests
99+
100+
test.cb('handlePDF_electronError', t => {
101+
const cb = (e, d) => {
102+
t.is(e, 'error occurred')
103+
t.end()
104+
}
105+
job._handlePDF('output.pdf', cb, 'error occurred', undefined)
106+
})
107+
108+
test.cb('handlePDF_inMemory', t => {
109+
// Arrange
110+
const opts = _.extend({}, options, {inMemory: true})
111+
const inMemJob = new ExportJob(['input'], 'output.pdf', {}, opts)
112+
let windowEventData
113+
inMemJob.on('window.capture.end', (event) => {
114+
windowEventData = event.data
115+
})
116+
const err = undefined
117+
const data = 'binaryPDFDataWouldGoHere'
118+
119+
// Assert
120+
const cb = (e, d) => {
121+
t.is(d, windowEventData) // the window.capture.end event was emitted with data
122+
t.is(d, data) // The raw data was returned and not a filepath
123+
t.end()
124+
}
125+
126+
// Act
127+
inMemJob._handlePDF('output.pdf', cb, err, data)
128+
})
129+
97130
// Support Functions
98131
/**
99132
* Stubs the windows.sessionn.cookies object and provides access

test/source-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test('resolve() handles arrays of strings', t => {
1616
})
1717

1818
test('resolve() converts markdown to html', t => {
19-
source.resolve(['../README.md'], {}).then(result => {
19+
source.resolve(['./README.md'], {}).then(result => {
2020
t.is(result.length, 1)
2121
t.truthy(result[0].endsWith('.html'))
2222
})

0 commit comments

Comments
 (0)