Browse Source

feat: add more options to printToPDF (#21906)

Shelley Vohr 5 years ago
parent
commit
548b290ea7
4 changed files with 165 additions and 47 deletions
  1. 17 6
      docs/api/web-contents.md
  2. 10 3
      docs/api/webview-tag.md
  3. 111 34
      lib/browser/api/web-contents.js
  4. 27 4
      spec-main/api-web-contents-spec.ts

+ 17 - 6
docs/api/web-contents.md

@@ -1273,9 +1273,11 @@ Returns [`PrinterInfo[]`](structures/printer-info.md)
   * `pagesPerSheet` Number (optional) - The number of pages to print per page sheet.
   * `collate` Boolean (optional) - Whether the web page should be collated.
   * `copies` Number (optional) - The number of copies of the web page to print.
-  * `pageRanges` Record<string, number> (optional) - The page range to print. Should have two keys: `from` and `to`.
+  * `pageRanges` Record<string, number> (optional) - The page range to print.
+    * `from` Number - the start page.
+    * `to` Number - the end page.
   * `duplexMode` String (optional) - Set the duplex mode of the printed web page. Can be `simplex`, `shortEdge`, or `longEdge`.
-  * `dpi` Object (optional)
+  * `dpi` Record<string, number> (optional)
     * `horizontal` Number (optional) - The horizontal dpi.
     * `vertical` Number (optional) - The vertical dpi.
   * `header` String (optional) - String to be printed as page header.
@@ -1301,14 +1303,21 @@ win.webContents.print(options, (success, errorType) => {
 #### `contents.printToPDF(options)`
 
 * `options` Object
+  * `headerFooter` Record<string, string> (optional) - the header and footer for the PDF.
+    * `title` String - The title for the PDF header.
+    * `url` String - the url for the PDF footer.
+  * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait.
   * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for
     default margin, 1 for no margin, and 2 for minimum margin.
-  * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`,
-    `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`
     and `width` in microns.
+  * `scaleFactor` Number (optional) - The scale factor of the web page. Can range from 0 to 100.
+  * `pageRanges` Record<string, number> (optional) - The page range to print.
+    * `from` Number - the first page to print.
+    * `to` Number - the last page to print (inclusive).
+  * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`,
+  `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`
   * `printBackground` Boolean (optional) - Whether to print CSS backgrounds.
   * `printSelectionOnly` Boolean (optional) - Whether to print selection only.
-  * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait.
 
 Returns `Promise<Buffer>` - Resolves with the generated PDF data.
 
@@ -1324,7 +1333,9 @@ By default, an empty `options` will be regarded as:
   marginsType: 0,
   printBackground: false,
   printSelectionOnly: false,
-  landscape: false
+  landscape: false,
+  pageSize: 'A4',
+  scaleFactor: 100
 }
 ```
 

+ 10 - 3
docs/api/webview-tag.md

@@ -556,14 +556,21 @@ Prints `webview`'s web page. Same as `webContents.print([options])`.
 ### `<webview>.printToPDF(options)`
 
 * `options` Object
+  * `headerFooter` Record<string, string> (optional) - the header and footer for the PDF.
+    * `title` String - The title for the PDF header.
+    * `url` String - the url for the PDF footer.
+  * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait.
   * `marginsType` Integer (optional) - Specifies the type of margins to use. Uses 0 for
     default margin, 1 for no margin, and 2 for minimum margin.
-  * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`,
-    `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`
     and `width` in microns.
+  * `scaleFactor` Number (optional) - The scale factor of the web page. Can range from 0 to 100.
+  * `pageRanges` Record<string, number> (optional) - The page range to print.
+    * `from` Number - the first page to print.
+    * `to` Number - the last page to print (inclusive).
+  * `pageSize` String | Size (optional) - Specify page size of the generated PDF. Can be `A3`,
+  `A4`, `A5`, `Legal`, `Letter`, `Tabloid` or an Object containing `height`
   * `printBackground` Boolean (optional) - Whether to print CSS backgrounds.
   * `printSelectionOnly` Boolean (optional) - Whether to print selection only.
-  * `landscape` Boolean (optional) - `true` for landscape, `false` for portrait.
 
 Returns `Promise<Uint8Array>` - Resolves with the generated PDF data.
 

+ 111 - 34
lib/browser/api/web-contents.js

@@ -65,32 +65,34 @@ const PDFPageSizes = {
 
 // Default printing setting
 const defaultPrintingSetting = {
-  pageRage: [],
+  // Customizable.
+  pageRange: [],
   mediaSize: {},
   landscape: false,
-  color: 2,
   headerFooterEnabled: false,
   marginsType: 0,
-  isFirstRequest: false,
-  previewUIID: 0,
-  previewModifiable: true,
-  printToPDF: true,
+  scaleFactor: 100,
+  shouldPrintBackgrounds: false,
+  shouldPrintSelectionOnly: false,
+  // Non-customizable.
   printWithCloudPrint: false,
   printWithPrivet: false,
   printWithExtension: false,
   pagesPerSheet: 1,
+  isFirstRequest: false,
+  previewUIID: 0,
+  previewModifiable: true,
+  printToPDF: true,
   deviceName: 'Save as PDF',
   generateDraftData: true,
-  fitToPageEnabled: false,
-  scaleFactor: 100,
   dpiHorizontal: 72,
   dpiVertical: 72,
   rasterizePDF: false,
   duplex: 0,
   copies: 1,
-  collate: true,
-  shouldPrintBackgrounds: false,
-  shouldPrintSelectionOnly: false
+  // 2 = color - see ColorModel in //printing/print_job_constants.h
+  color: 2,
+  collate: true
 }
 
 // JavaScript implementations of WebContents.
@@ -206,60 +208,135 @@ WebContents.prototype.executeJavaScriptInIsolatedWorld = async function (code, h
 
 // Translate the options of printToPDF.
 WebContents.prototype.printToPDF = function (options) {
-  const printingSetting = {
+  const printSettings = {
     ...defaultPrintingSetting,
     requestID: getNextId()
   }
-  if (options.landscape) {
-    printingSetting.landscape = options.landscape
+
+  if (options.landscape !== undefined) {
+    if (typeof options.landscape !== 'boolean') {
+      const error = new Error('landscape must be a Boolean')
+      return Promise.reject(error)
+    }
+    printSettings.landscape = options.landscape
   }
-  if (options.fitToPageEnabled) {
-    printingSetting.fitToPageEnabled = options.fitToPageEnabled
+
+  if (options.scaleFactor !== undefined) {
+    if (typeof options.scaleFactor !== 'number') {
+      const error = new Error('scaleFactor must be a Number')
+      return Promise.reject(error)
+    }
+    printSettings.scaleFactor = options.scaleFactor
   }
-  if (options.scaleFactor) {
-    printingSetting.scaleFactor = options.scaleFactor
+
+  if (options.marginsType !== undefined) {
+    if (typeof options.marginsType !== 'number') {
+      const error = new Error('marginsType must be a Number')
+      return Promise.reject(error)
+    }
+    printSettings.marginsType = options.marginsType
   }
-  if (options.marginsType) {
-    printingSetting.marginsType = options.marginsType
+
+  if (options.printSelectionOnly !== undefined) {
+    if (typeof options.printSelectionOnly !== 'boolean') {
+      const error = new Error('printSelectionOnly must be a Boolean')
+      return Promise.reject(error)
+    }
+    printSettings.shouldPrintSelectionOnly = options.printSelectionOnly
   }
-  if (options.printSelectionOnly) {
-    printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly
+
+  if (options.printBackground !== undefined) {
+    if (typeof options.printBackground !== 'boolean') {
+      const error = new Error('printBackground must be a Boolean')
+      return Promise.reject(error)
+    }
+    printSettings.shouldPrintBackgrounds = options.printBackground
   }
-  if (options.printBackground) {
-    printingSetting.shouldPrintBackgrounds = options.printBackground
+
+  if (options.pageRanges !== undefined) {
+    const pageRanges = options.pageRanges
+    if (!pageRanges.hasOwnProperty('from') || !pageRanges.hasOwnProperty('to')) {
+      const error = new Error(`pageRanges must be an Object with 'from' and 'to' properties`)
+      return Promise.reject(error)
+    }
+
+    if (typeof pageRanges.from !== 'number') {
+      const error = new Error('pageRanges.from must be a Number')
+      return Promise.reject(error)
+    }
+
+    if (typeof pageRanges.to !== 'number') {
+      const error = new Error('pageRanges.to must be a Number')
+      return Promise.reject(error)
+    }
+
+    // Chromium uses 1-based page ranges, so increment each by 1.
+    printSettings.pageRange = [{
+      from: pageRanges.from + 1,
+      to: pageRanges.to + 1
+    }]
+  }
+
+  if (options.headerFooter !== undefined) {
+    const headerFooter = options.headerFooter
+    printSettings.headerFooterEnabled = true
+    if (typeof headerFooter === 'object') {
+      if (!headerFooter.url || !headerFooter.title) {
+        const error = new Error('url and title properties are required for headerFooter')
+        return Promise.reject(error)
+      }
+      if (typeof headerFooter.title !== 'string') {
+        const error = new Error('headerFooter.title must be a String')
+        return Promise.reject(error)
+      }
+      printSettings.title = headerFooter.title
+
+      if (typeof headerFooter.url !== 'string') {
+        const error = new Error('headerFooter.url must be a String')
+        return Promise.reject(error)
+      }
+      printSettings.url = headerFooter.url
+    } else {
+      const error = new Error('headerFooter must be an Object')
+      return Promise.reject(error)
+    }
   }
 
-  if (options.pageSize) {
+  // Optionally set size for PDF.
+  if (options.pageSize !== undefined) {
     const pageSize = options.pageSize
     if (typeof pageSize === 'object') {
       if (!pageSize.height || !pageSize.width) {
-        return Promise.reject(new Error('Must define height and width for pageSize'))
+        const error = new Error('height and width properties are required for pageSize')
+        return Promise.reject(error)
       }
       // Dimensions in Microns
       // 1 meter = 10^6 microns
-      printingSetting.mediaSize = {
+      printSettings.mediaSize = {
         name: 'CUSTOM',
         custom_display_name: 'Custom',
         height_microns: Math.ceil(pageSize.height),
         width_microns: Math.ceil(pageSize.width)
       }
     } else if (PDFPageSizes[pageSize]) {
-      printingSetting.mediaSize = PDFPageSizes[pageSize]
+      printSettings.mediaSize = PDFPageSizes[pageSize]
     } else {
-      return Promise.reject(new Error(`Does not support pageSize with ${pageSize}`))
+      const error = new Error(`Unsupported pageSize: ${pageSize}`)
+      return Promise.reject(error)
     }
   } else {
-    printingSetting.mediaSize = PDFPageSizes['A4']
+    printSettings.mediaSize = PDFPageSizes['A4']
   }
 
   // Chromium expects this in a 0-100 range number, not as float
-  printingSetting.scaleFactor = Math.ceil(printingSetting.scaleFactor) % 100
+  printSettings.scaleFactor = Math.ceil(printSettings.scaleFactor) % 100
   // PrinterType enum from //printing/print_job_constants.h
-  printingSetting.printerType = 2
+  printSettings.printerType = 2
   if (features.isPrintingEnabled()) {
-    return this._printToPDF(printingSetting)
+    return this._printToPDF(printSettings)
   } else {
-    return Promise.reject(new Error('Printing feature is disabled'))
+    const error = new Error('Printing feature is disabled')
+    return Promise.reject(error)
   }
 }
 

+ 27 - 4
spec-main/api-web-contents-spec.ts

@@ -1414,17 +1414,40 @@ describe('webContents module', () => {
   })
 
   ifdescribe(features.isPrintingEnabled())('printToPDF()', () => {
+    let w: BrowserWindow
+
+    beforeEach(async () => {
+      w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } })
+      await w.loadURL('data:text/html,<h1>Hello, World!</h1>')
+    })
+
     afterEach(closeAllWindows)
+
+    it('rejects on incorrectly typed parameters', async () => {
+      const badTypes = {
+        marginsType: 'terrible',
+        scaleFactor: 'not-a-number',
+        landscape: [],
+        pageRanges: { 'oops': 'im-not-the-right-key' },
+        headerFooter: '123',
+        printSelectionOnly: 1,
+        printBackground: 2,
+        pageSize: 'IAmAPageSize'
+      }
+
+      // These will hard crash in Chromium unless we type-check
+      for (const [key, value] of Object.entries(badTypes)) {
+        const param = { [key]: value }
+        await expect(w.webContents.printToPDF(param)).to.eventually.be.rejected()
+      }
+    })
+
     it('can print to PDF', async () => {
-      const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } })
-      await w.loadURL('data:text/html,<h1>Hello, World!</h1>')
       const data = await w.webContents.printToPDF({})
       expect(data).to.be.an.instanceof(Buffer).that.is.not.empty()
     })
 
     it('does not crash when called multiple times', async () => {
-      const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } })
-      await w.loadURL('data:text/html,<h1>Hello, World!</h1>')
       const promises = []
       for (let i = 0; i < 2; i++) {
         promises.push(w.webContents.printToPDF({}))