Browse Source

fix: NativeImage serialization of <webview>.capturePage() result (#21103)

* refactor: add Error to isSerializableObject() (#20886)

* fix: NativeImage serialization of <webview>.capturePage() result (#20825)
Milan Burda 5 years ago
parent
commit
a62a367b9f

+ 4 - 8
filenames.auto.gni

@@ -135,11 +135,10 @@ auto_filenames = {
     "lib/common/api/module-list.ts",
     "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
-    "lib/common/clipboard-utils.ts",
     "lib/common/crash-reporter.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
-    "lib/common/remote/type-utils.ts",
+    "lib/common/type-utils.ts",
     "lib/common/web-view-methods.ts",
     "lib/common/webpack-globals-provider.ts",
     "lib/renderer/api/context-bridge.ts",
@@ -268,14 +267,13 @@ auto_filenames = {
     "lib/common/api/module-list.ts",
     "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
-    "lib/common/clipboard-utils.ts",
     "lib/common/crash-reporter.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
     "lib/common/init.ts",
     "lib/common/parse-features-string.js",
-    "lib/common/remote/type-utils.ts",
     "lib/common/reset-search-paths.ts",
+    "lib/common/type-utils.ts",
     "lib/common/web-view-methods.ts",
     "lib/common/webpack-globals-provider.ts",
     "lib/renderer/ipc-renderer-internal-utils.ts",
@@ -292,13 +290,12 @@ auto_filenames = {
     "lib/common/api/module-list.ts",
     "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
-    "lib/common/clipboard-utils.ts",
     "lib/common/crash-reporter.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
     "lib/common/init.ts",
-    "lib/common/remote/type-utils.ts",
     "lib/common/reset-search-paths.ts",
+    "lib/common/type-utils.ts",
     "lib/common/web-view-methods.ts",
     "lib/common/webpack-globals-provider.ts",
     "lib/renderer/api/context-bridge.ts",
@@ -342,13 +339,12 @@ auto_filenames = {
     "lib/common/api/module-list.ts",
     "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
-    "lib/common/clipboard-utils.ts",
     "lib/common/crash-reporter.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
     "lib/common/init.ts",
-    "lib/common/remote/type-utils.ts",
     "lib/common/reset-search-paths.ts",
+    "lib/common/type-utils.ts",
     "lib/common/webpack-globals-provider.ts",
     "lib/renderer/api/context-bridge.ts",
     "lib/renderer/api/crash-reporter.js",

+ 7 - 0
lib/browser/guest-view-manager.js

@@ -5,6 +5,7 @@ const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-interna
 const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils')
 const parseFeaturesString = require('@electron/internal/common/parse-features-string')
 const { syncMethods, asyncMethods } = require('@electron/internal/common/web-view-methods')
+const { serialize } = require('@electron/internal/common/type-utils')
 
 // Doesn't exist in early initialization.
 let webViewManager = null
@@ -387,6 +388,12 @@ handleMessageSync('ELECTRON_GUEST_VIEW_MANAGER_CALL', function (event, guestInst
   return guest[method](...args)
 })
 
+handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CAPTURE_PAGE', async function (event, guestInstanceId, args) {
+  const guest = getGuestForWebContents(guestInstanceId, event.sender)
+
+  return serialize(await guest.capturePage(...args))
+})
+
 // Returns WebContents from its guest id hosted in given webContents.
 const getGuestForWebContents = function (guestInstanceId, contents) {
   const guest = getGuest(guestInstanceId)

+ 1 - 1
lib/browser/remote/server.ts

@@ -5,7 +5,7 @@ import { EventEmitter } from 'events'
 import objectsRegistry from './objects-registry'
 import { ipcMainInternal } from '../ipc-main-internal'
 import * as guestViewManager from '@electron/internal/browser/guest-view-manager'
-import { isPromise, isSerializableObject } from '@electron/internal/common/remote/type-utils'
+import { isPromise, isSerializableObject } from '@electron/internal/common/type-utils'
 
 const v8Util = process.electronBinding('v8_util')
 const eventBinding = process.electronBinding('event')

+ 2 - 2
lib/browser/rpc-server.js

@@ -11,7 +11,7 @@ const { crashReporterInit } = require('@electron/internal/browser/crash-reporter
 const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal')
 const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils')
 const guestViewManager = require('@electron/internal/browser/guest-view-manager')
-const clipboardUtils = require('@electron/internal/common/clipboard-utils')
+const typeUtils = require('@electron/internal/common/type-utils')
 
 const emitCustomEvent = function (contents, eventName, ...args) {
   const event = eventBinding.createWithSender(contents)
@@ -62,7 +62,7 @@ ipcMainUtils.handleSync('ELECTRON_BROWSER_CLIPBOARD', function (event, method, .
     throw new Error(`Invalid method: ${method}`)
   }
 
-  return clipboardUtils.serialize(electron.clipboard[method](...clipboardUtils.deserialize(args)))
+  return typeUtils.serialize(electron.clipboard[method](...typeUtils.deserialize(args)))
 })
 
 if (features.isDesktopCapturerEnabled()) {

+ 3 - 3
lib/common/api/clipboard.js

@@ -4,13 +4,13 @@ const clipboard = process.electronBinding('clipboard')
 
 if (process.type === 'renderer') {
   const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils')
-  const clipboardUtils = require('@electron/internal/common/clipboard-utils')
+  const typeUtils = require('@electron/internal/common/type-utils')
 
   const makeRemoteMethod = function (method) {
     return (...args) => {
-      args = clipboardUtils.serialize(args)
+      args = typeUtils.serialize(args)
       const result = ipcRendererUtils.invokeSync('ELECTRON_BROWSER_CLIPBOARD', method, ...args)
-      return clipboardUtils.deserialize(result)
+      return typeUtils.deserialize(result)
     }
   }
 

+ 0 - 25
lib/common/remote/type-utils.ts

@@ -1,25 +0,0 @@
-export function isPromise (val: any) {
-  return (
-    val &&
-    val.then &&
-    val.then instanceof Function &&
-    val.constructor &&
-    val.constructor.reject &&
-    val.constructor.reject instanceof Function &&
-    val.constructor.resolve &&
-    val.constructor.resolve instanceof Function
-  )
-}
-
-const serializableTypes = [
-  Boolean,
-  Number,
-  String,
-  Date,
-  RegExp,
-  ArrayBuffer
-]
-
-export function isSerializableObject (value: any) {
-  return value === null || ArrayBuffer.isView(value) || serializableTypes.some(type => value instanceof type)
-}

+ 29 - 2
lib/common/clipboard-utils.ts → lib/common/type-utils.ts

@@ -1,5 +1,32 @@
 const { nativeImage, NativeImage } = process.electronBinding('native_image')
 
+export function isPromise (val: any) {
+  return (
+    val &&
+    val.then &&
+    val.then instanceof Function &&
+    val.constructor &&
+    val.constructor.reject &&
+    val.constructor.reject instanceof Function &&
+    val.constructor.resolve &&
+    val.constructor.resolve instanceof Function
+  )
+}
+
+const serializableTypes = [
+  Boolean,
+  Number,
+  String,
+  Date,
+  Error,
+  RegExp,
+  ArrayBuffer
+]
+
+export function isSerializableObject (value: any) {
+  return value === null || ArrayBuffer.isView(value) || serializableTypes.some(type => value instanceof type)
+}
+
 const objectMap = function (source: Object, mapper: (value: any) => any) {
   const sourceEntries = Object.entries(source)
   const targetEntries = sourceEntries.map(([key, val]) => [key, mapper(val)])
@@ -15,7 +42,7 @@ export function serialize (value: any): any {
     }
   } else if (Array.isArray(value)) {
     return value.map(serialize)
-  } else if (value instanceof Buffer) {
+  } else if (isSerializableObject(value)) {
     return value
   } else if (value instanceof Object) {
     return objectMap(value, serialize)
@@ -29,7 +56,7 @@ export function deserialize (value: any): any {
     return nativeImage.createFromBitmap(value.buffer, value.size)
   } else if (Array.isArray(value)) {
     return value.map(deserialize)
-  } else if (value instanceof Buffer) {
+  } else if (isSerializableObject(value)) {
     return value
   } else if (value instanceof Object) {
     return objectMap(value, deserialize)

+ 0 - 1
lib/common/web-view-methods.ts

@@ -52,7 +52,6 @@ export const syncMethods = new Set([
 
 export const asyncMethods = new Set([
   'loadURL',
-  'capturePage',
   'executeJavaScript',
   'insertCSS',
   'insertText',

+ 1 - 6
lib/renderer/api/remote.js

@@ -4,7 +4,7 @@ const v8Util = process.electronBinding('v8_util')
 const { hasSwitch } = process.electronBinding('command_line')
 
 const { CallbacksRegistry } = require('@electron/internal/renderer/remote/callbacks-registry')
-const { isPromise, isSerializableObject } = require('@electron/internal/common/remote/type-utils')
+const { isPromise, isSerializableObject } = require('@electron/internal/common/type-utils')
 const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal')
 
 const callbacksRegistry = new CallbacksRegistry()
@@ -64,11 +64,6 @@ function wrapArgs (args, visited = new Set()) {
           type: 'remote-object',
           id: v8Util.getHiddenValue(value, 'atomId')
         }
-      } else if (value instanceof Error) {
-        return {
-          type: 'value',
-          value
-        }
       }
 
       const meta = {

+ 5 - 0
lib/renderer/web-view/web-view-impl.ts

@@ -5,6 +5,7 @@ import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-inte
 import * as guestViewInternal from '@electron/internal/renderer/web-view/guest-view-internal'
 import { WEB_VIEW_CONSTANTS } from '@electron/internal/renderer/web-view/web-view-constants'
 import { syncMethods, asyncMethods } from '@electron/internal/common/web-view-methods'
+import { deserialize } from '@electron/internal/common/type-utils'
 const { webFrame } = electron
 
 const v8Util = process.electronBinding('v8_util')
@@ -269,6 +270,10 @@ export const setupMethods = (WebViewElement: typeof ElectronInternal.WebViewElem
   for (const method of asyncMethods) {
     (WebViewElement.prototype as Record<string, any>)[method] = createNonBlockHandler(method)
   }
+
+  WebViewElement.prototype.capturePage = async function (...args) {
+    return deserialize(await ipcRendererInternal.invoke('ELECTRON_GUEST_VIEW_MANAGER_CAPTURE_PAGE', this.getWebContentsId(), args))
+  }
 }
 
 export const webViewImplModule = {

+ 14 - 0
spec/webview-spec.js

@@ -1055,6 +1055,20 @@ describe('<webview> tag', function () {
     })
   })
 
+  describe('<webview>.capturePage()', () => {
+    it('returns a Promise with a NativeImage', async () => {
+      const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'
+      await loadWebView(webview, { src })
+
+      const image = await webview.capturePage()
+      const imgBuffer = image.toPNG()
+
+      // Check the 25th byte in the PNG.
+      // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha
+      expect(imgBuffer[25]).to.equal(6)
+    })
+  })
+
   describe('<webview>.printToPDF()', () => {
     before(function () {
       if (!features.isPrintingEnabled()) {

+ 1 - 0
typings/internal-electron.d.ts

@@ -167,6 +167,7 @@ declare namespace ElectronInternal {
     // Created in web-view-impl
     public getWebContents(): Electron.WebContents;
     public getWebContentsId(): number;
+    public capturePage(rect?: Electron.Rectangle): Promise<Electron.NativeImage>;
   }
 }