Repository: fraserxu/electron-pdf
Pull request #251 (2019-07-16 16:44:37)

Added support for changing the exportType to PNG at runtime
Also bumped electron to 4.2.6

mattsgarlata: +1

Patch:
diff --git a/README.md b/README.md
index e99dccd..d812dcf 100644
--- a/README.md
+++ b/README.md
@@ -226,19 +226,31 @@ document.body.dispatchEvent(new CustomEvent('view-ready', { detail: {layout: lan
 ```
 
 ##### your-exporter.js
-As an example, suppose you wanted to change the orientation of the PDF
+You are required to provide a function that accepts the `detail` object from 
+the CustomEvent and returns a Promise.  You may optionally fulfill the promise with 
+and object that will amend/override any of the contextual attributes assigned to resource (url)
+currently being exported.
+
+As an example, suppose you wanted to change the orientation of the PDF,
+and capture the output as PNG instead of a PDF.
 
 ```javascript
 job.observeReadyEvent( (detail) => {
     return new Promise( (resolve,reject) => {
+      const context = {}
       if( detail && detail.landscape ){
         job.changeArgValue('landscape', true)
+        context.type = 'png'
       }
-      resolve()
+      resolve(context)
     })
 })
 ```
 
+Note: Future versions of the library will only allow you to provide context overrides, 
+and not allow you to change job level attributes.
+
+
 All Available Options
 -----
 
@@ -293,7 +305,8 @@ available from the Electron API.  See the following options for usage.
     -s | --printSelectionOnly  Boolean - Whether to print selection only
                                  false - default
                                  
-    -t | --trustRemoteContent  Boolean - Whether to trust remote content loaded in the Electron webview.  False by default.                               
+    -t | --trustRemoteContent  Boolean - Whether to trust remote content loaded in the Electron webview.  False by default.
+    --type                     String - The type of export, will dictate the output file type.  'png': PNG image, anything else: PDF File
     
     -w | --outputWait          Integer – Time to wait (in MS) between page load and PDF creation.  
                                          If used in conjunction with -e this will override the default timeout of 10 seconds    
diff --git a/lib/exportJob.js b/lib/exportJob.js
index b677847..5d1b295 100644
--- a/lib/exportJob.js
+++ b/lib/exportJob.js
@@ -139,8 +139,16 @@ class ExportJob extends EventEmitter {
    *
    * Use this method to register your own observer.
    *
-   * @param handler {function} A callback that is passed the following:
-   *  args[0]: the details object from CustomEvent
+   * @param handler {Function<Promise>}
+   *  A callback that is passed the following:
+   *  - args[0]: the details object from CustomEvent
+   *
+   *  Can optionally fulfill the promise with an object
+   *  that will be assigned to the context object for the current export.
+   *
+   *  Keys that are currently supported:
+   *  {String} type - a valid export type, for example 'png' can be used to change
+   *  the output from 'pdf' to a 'png' at runtime.
    *
    * @fires PDFExporter#window.observer.start when the observer is invoked
    * @fires PDFExporter#window.observer.timeout when the promise is not
@@ -380,13 +388,16 @@ class ExportJob extends EventEmitter {
   // Page Load & Rendering
 
   _load (win) {
+    // Do not run printToPDF, useful if the page downloads a file that needs captured instead of a PDF
+    // Currently only supports a single import url
     if (this.args.noprint) {
       // It's the responsibility of the window being loaded to initiate a download itself
       this._saveDownload(win)
     } else {
-      this._renderAndCollectOutput(win, (win, inputIndex, outputDoneFn) => {
-        const targetFile = this._getTargetFile(inputIndex)
-        this._generateOutput(win, targetFile, outputDoneFn)
+      this._renderAndCollectOutput(win, (context, outputDoneFn, observerContext) => {
+        const ctx = _.extend({}, observerContext, context)
+        const targetFile = this._getTargetFile(ctx)
+        this._generateOutput(ctx, targetFile, outputDoneFn)
       })
     }
   }
@@ -411,11 +422,23 @@ class ExportJob extends EventEmitter {
     })
   }
 
+  /**
+   * Processes each of the url requests synchronously (in series)
+   *
+   * @param {Window} win The Window object
+   * @param {Function} generateFunc
+   * @private
+   */
   _renderAndCollectOutput (win, generateFunc) {
     const windowEvents = []
 
-    this.input.forEach((uriPath, i) => {
-      windowEvents.push((outputDoneFn) => this._captureWindow(uriPath, win, generateFunc, outputDoneFn, i))
+    this.input.forEach((uriPath, inputIndex) => {
+      const context = {
+        uriPath,
+        inputIndex,
+        window: win
+      }
+      windowEvents.push((outputDoneFn) => this._captureWindow(context, generateFunc, outputDoneFn))
     })
 
     async.series(windowEvents, (err, results) => {
@@ -438,21 +461,23 @@ class ExportJob extends EventEmitter {
   /**
    * Loads a Uri and prepares the window for export based on the options provided.
    *
-   * @param uriPath
-   * @param {Electron.BrowserWindow} win The browser window
+   * @param {Electron.BrowserWindow} context.window The browser window
+   * @param {String} context.uriPath
+   * @param {Number} context.inputIndex If multiple input uris are being generated, this is the index of the current one.
    * @param {Function} generateFn A callback that acts on a window ready to be captured.
    * @param {Function} outputDoneFn A call back invoked after generateFn has finished creating the output
-   * @param {Number} [inputIndex=0] If multiple input uris are being generated, this is the index of the current one.
    * @private
    */
-  _captureWindow (uriPath, win, generateFn, outputDoneFn, inputIndex = 0) {
-    this._initializeWindowForResource(this.orientations[inputIndex] === 'landscape')
-    const generateFunction = generateFn.bind(this, win, inputIndex, outputDoneFn)
-    const waitFunction = this._waitForPage.bind(this, win, generateFunction, this.args.outputWait)
+  _captureWindow (context, generateFn, outputDoneFn) {
+    const { window, uriPath } = context
+    const orientation = this.orientations[context.inputIndex]
+    this._initializeWindowForResource(orientation === 'landscape')
+    const generateFunction = generateFn.bind(this, context, outputDoneFn)
+    const waitFunction = this._waitForPage.bind(this, window, generateFunction, this.args.outputWait)
     // Need a new listener to generate the resource
-    win.webContents.removeAllListeners('did-finish-load')
-    win.webContents.on('did-finish-load', waitFunction)
-    this._loadURL(win, uriPath)
+    window.webContents.removeAllListeners('did-finish-load')
+    window.webContents.on('did-finish-load', waitFunction)
+    this._loadURL(window, uriPath)
   }
 
   /**
@@ -587,7 +612,7 @@ class ExportJob extends EventEmitter {
       generateFunction()
     }, MAX_READY_EVENT_WAIT)
 
-    this.readyEventObserver(customEventDetail).then(() => {
+    this.readyEventObserver(customEventDetail).then(context => {
       /**
        * Fires when an observer fulfills it's promise
        * @event PDFExporter#window.observer.end
@@ -595,7 +620,7 @@ class ExportJob extends EventEmitter {
        */
       this.emit('window.observer.end', {})
       clearTimeout(timeout)
-      generateFunction()
+      generateFunction(context)
     })
   }
 
@@ -607,13 +632,15 @@ class ExportJob extends EventEmitter {
    * Because of timeouts and promises being resolved this function
    * is implemented to be idempotent
    *
-   * @param window
+   * @param context.window
+   * @param context.type
    * @param outputFile
-   * @param {Function} done callback that is invoked when the output file is complete (or errored)
+   * @param {Function} outputDoneFn callback that is invoked when the output file is complete (or errored)
    *
    * @private
    */
-  _generateOutput (window, outputFile, done) {
+  _generateOutput (context, outputFile, outputDoneFn) {
+    const { window } = context
     if (!this.generated) {
       this.generated = true
       // Multi-resource jobs can have different orientations, so resize the
@@ -625,11 +652,11 @@ class ExportJob extends EventEmitter {
       this.emit('window.capture.start', {})
 
       if (outputFile.toLowerCase().endsWith('.png')) {
-        this._captureImage(window, outputFile, done)
+        this._captureImage(window, outputFile, outputDoneFn)
       } else if (outputFile.toLowerCase().endsWith('.html')) {
-        this._captureHtml(window, outputFile, done)
+        this._captureHtml(window, outputFile, outputDoneFn)
       } else {
-        this._capturePDF(this.args, window, done, outputFile)
+        this._capturePDF(this.args, window, outputDoneFn, outputFile)
       }
     }
   }
@@ -643,7 +670,7 @@ class ExportJob extends EventEmitter {
     })
   }
 
-  _captureImage (window, outputFile, done) {
+  _captureImage (window, outputFile, outputDoneFn) {
     // We need a short timeout here or the image may not be captured fully
     // https://github.com/electron/electron/issues/6622
     setTimeout(() => {
@@ -651,11 +678,11 @@ class ExportJob extends EventEmitter {
         // http://electron.atom.io/docs/api/native-image/#imagetopng
         const pngBuffer = image.toPNG()
         if (this.options.inMemory) {
-          this._emitResourceEvents(undefined, pngBuffer, done)
+          this._emitResourceEvents(undefined, pngBuffer, outputDoneFn)
         } else {
           const target = path.resolve(outputFile)
           fs.writeFile(target, pngBuffer, function (err) {
-            this._emitResourceEvents(err, target, done)
+            this._emitResourceEvents(err, target, outputDoneFn)
           }.bind(this))
         }
       })
@@ -700,10 +727,10 @@ class ExportJob extends EventEmitter {
    *
    * @param err
    * @param data
-   * @param done
+   * @param outputDoneFn
    * @private
    */
-  _emitResourceEvents (err, data, done) {
+  _emitResourceEvents (err, data, outputDoneFn) {
     /**
      * Window Event - fires when an export has captured the window (succesfully
      * or not)
@@ -720,20 +747,23 @@ class ExportJob extends EventEmitter {
      * @property {String} file - Path to the File
      */
     this.emit('export-complete', { data: data })
-    done(err, data)
+    outputDoneFn(err, data)
   }
 
   /**
-   * @param zeroBasedIndex Index of the input being processed
+   * @param {Number} context.inputIndex Index of the input being processed
    * @returns {String} the pdf output file name that should be used.
    * @private
    */
-  _getTargetFile (zeroBasedIndex) {
+  _getTargetFile (context) {
+    const zeroBasedIndex = context.inputIndex
+    const ext = path.extname(this.output)
+    const extIndex = this.output.lastIndexOf(ext)
+    const actualExt = context.type === 'png' ? '.png' : ext
     if (_.size(this.input) > 1) {
-      const ext = path.extname(this.output)
-      return _.replace(this.output, ext, `_${zeroBasedIndex + 1}${ext}`)
+      return this.output.substring(0, extIndex) + `_${zeroBasedIndex + 1}${actualExt}`
     }
-    return this.output
+    return this.output.substring(0, extIndex) + actualExt
   }
 }
 
diff --git a/lib/options.js b/lib/options.js
index 09e2175..43af7f9 100644
--- a/lib/options.js
+++ b/lib/options.js
@@ -31,6 +31,7 @@ var options = {
     'printBackground': 'b',
     'printSelectionOnly': 's',
     'trustRemoteContent': 't',
+    'type': ['type'],
     'version': 'v',
     'waitForJSEvent': 'e'
   },
diff --git a/package-lock.json b/package-lock.json
index c6afd5c..0775b5a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "electron-pdf",
-  "version": "4.0.6",
+  "version": "4.0.7",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -146,9 +146,9 @@
       }
     },
     "@types/node": {
-      "version": "10.14.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz",
-      "integrity": "sha512-DT25xX/YgyPKiHFOpNuANIQIVvYEwCWXgK2jYYwqgaMrYE6+tq+DtmMwlD3drl6DJbUwtlIDnn0d7tIn/EbXBg=="
+      "version": "10.14.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.12.tgz",
+      "integrity": "sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg=="
     },
     "@types/stack-trace": {
       "version": "0.0.29",
@@ -2034,11 +2034,11 @@
       }
     },
     "electron": {
-      "version": "4.1.4",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-4.1.4.tgz",
-      "integrity": "sha512-MelOjntJvd33izEjR6H4N/Uii7y535z/b2BuYXJGLNSHL6o1IlyhUQmfiT87kWABayERgeuYERgvsyf956OOFw==",
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-4.2.6.tgz",
+      "integrity": "sha512-qnaQXQcRKugXoQ7/4J4lYKzyU/RJMK/qrIGyC8KsgGG/IVVagolNPYy7QrsGpJ7YwpQHzNjwtDNaJ/UI7VTCyA==",
       "requires": {
-        "@types/node": "10.14.4",
+        "@types/node": "10.14.12",
         "electron-download": "4.1.1",
         "extract-zip": "1.6.5"
       },
@@ -2048,7 +2048,7 @@
           "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
           "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
           "requires": {
-            "ms": "2.1.1"
+            "ms": "2.1.2"
           }
         },
         "electron-download": {
@@ -2086,9 +2086,9 @@
           }
         },
         "ms": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
-          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
         "path-exists": {
           "version": "3.0.0",
diff --git a/package.json b/package.json
index 10fc637..d90093a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "electron-pdf",
-  "version": "4.0.6",
+  "version": "4.0.7",
   "description": "A command line tool to generate PDF from URL, HTML or Markdown files",
   "main": "lib/index.js",
   "scripts": {
@@ -45,7 +45,7 @@
     "@sentry/electron": "^0.17.1",
     "async": "^2.0.1",
     "debug": "^2.3.2",
-    "electron": "4.1.4",
+    "electron": "4.2.6",
     "eventemitter2": "^2.1.3",
     "github-markdown-css": "^2.0.9",
     "highlight.js": "^9.0.0",
diff --git a/usage.txt b/usage.txt
index 10eb02d..944e8b6 100644
--- a/usage.txt
+++ b/usage.txt
@@ -25,6 +25,7 @@ Options
                                'view-ready' - default
   -t | --trustRemoteContent  Boolean - Trust remote content loaded in the Electron webview.
                                false - default
+  --type                     String - The type of export, will dictate the output file type.  'png': PNG image, anything else: PDF File
 
 Usage
   $ electron-pdf <input> <output>


Test patch:
diff --git a/test/exportJob-test.js b/test/exportJob-test.js
index 4bdc10f..dea8330 100644
--- a/test/exportJob-test.js
+++ b/test/exportJob-test.js
@@ -65,16 +65,28 @@ test('getBrowserConfiguration_SessionPartitionWithoutCookies', t => {
 
 // File Generation
 test('getTargetFile', t => {
-  const fileName = job._getTargetFile(2)
+  const fileName = job._getTargetFile({ inputIndex: 2 })
   t.is(fileName, 'output.pdf')
 })
 
 test('getTargetFile_multiple_inputs', t => {
   job = new ExportJob(['input1', 'input2'], 'output.pdf', options)
-  const fileName = job._getTargetFile(1)
+  const fileName = job._getTargetFile({ inputIndex: 1 })
   t.is(fileName, 'output_2.pdf')
 })
 
+test('getTargetFile_png_override', t => {
+  job = new ExportJob(['input1', 'input2'], 'output.pdf', options)
+  const fileName = job._getTargetFile({ inputIndex: 1, type: 'png' })
+  t.is(fileName, 'output_2.png')
+})
+
+test('getTargetFile_png_override_single', t => {
+  job = new ExportJob(['input1'], 'output.pdf', options)
+  const fileName = job._getTargetFile({ inputIndex: 1, type: 'png' })
+  t.is(fileName, 'output.png')
+})
+
 // Cookie Tests
 test('setSessionCookie_single', t => {
   const cs = setupCookieStub()
@@ -99,7 +111,7 @@ test('setSessionCookie_multiple', t => {
 })
 
 // Header Tests
-test.only('_getHeaders all possible headers', t => {
+test('_getHeaders all possible headers', t => {
   args.acceptLanguage = 'en'
   args.disableCache = true
   args.requestHeaders = `{"H1": "V1", "H2": "V2"}`