fix: nativeImage remote serialization (#23543)
We weren't serializing nativeImages properly in the remote module, leading to gin conversion errors when trying to, for example, create a new context menu in the renderer with icons using nativeImage. This fixes that by adding a new special case to handle them.
This commit is contained in:
parent
eb341b383d
commit
ee0f67d541
5 changed files with 100 additions and 14 deletions
|
@ -5,6 +5,7 @@ import { EventEmitter } from 'events';
|
||||||
import objectsRegistry from './objects-registry';
|
import objectsRegistry from './objects-registry';
|
||||||
import { ipcMainInternal } from '../ipc-main-internal';
|
import { ipcMainInternal } from '../ipc-main-internal';
|
||||||
import { isPromise, isSerializableObject } from '@electron/internal/common/type-utils';
|
import { isPromise, isSerializableObject } from '@electron/internal/common/type-utils';
|
||||||
|
import { Size } from 'electron/main';
|
||||||
|
|
||||||
const v8Util = process.electronBinding('v8_util');
|
const v8Util = process.electronBinding('v8_util');
|
||||||
const eventBinding = process.electronBinding('event');
|
const eventBinding = process.electronBinding('event');
|
||||||
|
@ -242,6 +243,9 @@ type MetaTypeFromRenderer = {
|
||||||
id: number,
|
id: number,
|
||||||
location: string,
|
location: string,
|
||||||
length: number
|
length: number
|
||||||
|
} | {
|
||||||
|
type: 'nativeimage',
|
||||||
|
value: { size: Size, buffer: Buffer, scaleFactor: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const fakeConstructor = (constructor: Function, name: string) =>
|
const fakeConstructor = (constructor: Function, name: string) =>
|
||||||
|
@ -259,6 +263,19 @@ const fakeConstructor = (constructor: Function, name: string) =>
|
||||||
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
|
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
|
||||||
const metaToValue = function (meta: MetaTypeFromRenderer): any {
|
const metaToValue = function (meta: MetaTypeFromRenderer): any {
|
||||||
switch (meta.type) {
|
switch (meta.type) {
|
||||||
|
case 'nativeimage': {
|
||||||
|
const image = electron.nativeImage.createEmpty();
|
||||||
|
for (const rep of meta.value) {
|
||||||
|
const { buffer, size, scaleFactor } = rep;
|
||||||
|
image.addRepresentation({
|
||||||
|
buffer,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
scaleFactor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
case 'value':
|
case 'value':
|
||||||
return meta.value;
|
return meta.value;
|
||||||
case 'remote-object':
|
case 'remote-object':
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const v8Util = process.electronBinding('v8_util');
|
const v8Util = process.electronBinding('v8_util');
|
||||||
const { hasSwitch } = process.electronBinding('command_line');
|
const { hasSwitch } = process.electronBinding('command_line');
|
||||||
|
const { NativeImage } = process.electronBinding('native_image');
|
||||||
|
|
||||||
const { CallbacksRegistry } = require('@electron/internal/renderer/remote/callbacks-registry');
|
const { CallbacksRegistry } = require('@electron/internal/renderer/remote/callbacks-registry');
|
||||||
const { isPromise, isSerializableObject } = require('@electron/internal/common/type-utils');
|
const { isPromise, isSerializableObject } = require('@electron/internal/common/type-utils');
|
||||||
|
@ -35,7 +36,15 @@ function wrapArgs (args, visited = new Set()) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (value instanceof NativeImage) {
|
||||||
|
const images = [];
|
||||||
|
for (const scaleFactor of value.getScaleFactors()) {
|
||||||
|
const size = value.getSize(scaleFactor);
|
||||||
|
const buffer = value.toBitmap({ scaleFactor });
|
||||||
|
images.push({ buffer, scaleFactor, size });
|
||||||
|
}
|
||||||
|
return { type: 'nativeimage', value: images };
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
visited.add(value);
|
visited.add(value);
|
||||||
const meta = {
|
const meta = {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
|
@ -248,21 +248,32 @@ bool NativeImage::IsEmpty() {
|
||||||
return image_.IsEmpty();
|
return image_.IsEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::Size NativeImage::GetSize() {
|
gfx::Size NativeImage::GetSize(const base::Optional<float> scale_factor) {
|
||||||
return image_.Size();
|
float sf = scale_factor.value_or(1.0f);
|
||||||
|
gfx::ImageSkiaRep image_rep = image_.AsImageSkia().GetRepresentation(sf);
|
||||||
|
|
||||||
|
return gfx::Size(image_rep.GetWidth(), image_rep.GetHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
float NativeImage::GetAspectRatio() {
|
std::vector<float> NativeImage::GetScaleFactors() {
|
||||||
gfx::Size size = GetSize();
|
gfx::ImageSkia image_skia = image_.AsImageSkia();
|
||||||
|
return image_skia.GetSupportedScales();
|
||||||
|
}
|
||||||
|
|
||||||
|
float NativeImage::GetAspectRatio(const base::Optional<float> scale_factor) {
|
||||||
|
float sf = scale_factor.value_or(1.0f);
|
||||||
|
gfx::Size size = GetSize(sf);
|
||||||
if (size.IsEmpty())
|
if (size.IsEmpty())
|
||||||
return 1.f;
|
return 1.f;
|
||||||
else
|
else
|
||||||
return static_cast<float>(size.width()) / static_cast<float>(size.height());
|
return static_cast<float>(size.width()) / static_cast<float>(size.height());
|
||||||
}
|
}
|
||||||
|
|
||||||
gin::Handle<NativeImage> NativeImage::Resize(v8::Isolate* isolate,
|
gin::Handle<NativeImage> NativeImage::Resize(gin::Arguments* args,
|
||||||
base::DictionaryValue options) {
|
base::DictionaryValue options) {
|
||||||
gfx::Size size = GetSize();
|
float scale_factor = GetScaleFactorFromOptions(args);
|
||||||
|
|
||||||
|
gfx::Size size = GetSize(scale_factor);
|
||||||
int width = size.width();
|
int width = size.width();
|
||||||
int height = size.height();
|
int height = size.height();
|
||||||
bool width_set = options.GetInteger("width", &width);
|
bool width_set = options.GetInteger("width", &width);
|
||||||
|
@ -272,11 +283,12 @@ gin::Handle<NativeImage> NativeImage::Resize(v8::Isolate* isolate,
|
||||||
if (width_set && !height_set) {
|
if (width_set && !height_set) {
|
||||||
// Scale height to preserve original aspect ratio
|
// Scale height to preserve original aspect ratio
|
||||||
size.set_height(width);
|
size.set_height(width);
|
||||||
size = gfx::ScaleToRoundedSize(size, 1.f, 1.f / GetAspectRatio());
|
size =
|
||||||
|
gfx::ScaleToRoundedSize(size, 1.f, 1.f / GetAspectRatio(scale_factor));
|
||||||
} else if (height_set && !width_set) {
|
} else if (height_set && !width_set) {
|
||||||
// Scale width to preserve original aspect ratio
|
// Scale width to preserve original aspect ratio
|
||||||
size.set_width(height);
|
size.set_width(height);
|
||||||
size = gfx::ScaleToRoundedSize(size, GetAspectRatio(), 1.f);
|
size = gfx::ScaleToRoundedSize(size, GetAspectRatio(scale_factor), 1.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
skia::ImageOperations::ResizeMethod method =
|
skia::ImageOperations::ResizeMethod method =
|
||||||
|
@ -290,8 +302,8 @@ gin::Handle<NativeImage> NativeImage::Resize(v8::Isolate* isolate,
|
||||||
|
|
||||||
gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage(
|
gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage(
|
||||||
image_.AsImageSkia(), method, size);
|
image_.AsImageSkia(), method, size);
|
||||||
return gin::CreateHandle(isolate,
|
return gin::CreateHandle(
|
||||||
new NativeImage(isolate, gfx::Image(resized)));
|
args->isolate(), new NativeImage(args->isolate(), gfx::Image(resized)));
|
||||||
}
|
}
|
||||||
|
|
||||||
gin::Handle<NativeImage> NativeImage::Crop(v8::Isolate* isolate,
|
gin::Handle<NativeImage> NativeImage::Crop(v8::Isolate* isolate,
|
||||||
|
@ -505,6 +517,7 @@ void NativeImage::BuildPrototype(v8::Isolate* isolate,
|
||||||
.SetMethod("toJPEG", &NativeImage::ToJPEG)
|
.SetMethod("toJPEG", &NativeImage::ToJPEG)
|
||||||
.SetMethod("toBitmap", &NativeImage::ToBitmap)
|
.SetMethod("toBitmap", &NativeImage::ToBitmap)
|
||||||
.SetMethod("getBitmap", &NativeImage::GetBitmap)
|
.SetMethod("getBitmap", &NativeImage::GetBitmap)
|
||||||
|
.SetMethod("getScaleFactors", &NativeImage::GetScaleFactors)
|
||||||
.SetMethod("getNativeHandle", &NativeImage::GetNativeHandle)
|
.SetMethod("getNativeHandle", &NativeImage::GetNativeHandle)
|
||||||
.SetMethod("toDataURL", &NativeImage::ToDataURL)
|
.SetMethod("toDataURL", &NativeImage::ToDataURL)
|
||||||
.SetMethod("isEmpty", &NativeImage::IsEmpty)
|
.SetMethod("isEmpty", &NativeImage::IsEmpty)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "base/values.h"
|
#include "base/values.h"
|
||||||
#include "gin/handle.h"
|
#include "gin/handle.h"
|
||||||
|
@ -84,15 +85,16 @@ class NativeImage : public gin_helper::Wrappable<NativeImage> {
|
||||||
v8::Local<v8::Value> ToPNG(gin::Arguments* args);
|
v8::Local<v8::Value> ToPNG(gin::Arguments* args);
|
||||||
v8::Local<v8::Value> ToJPEG(v8::Isolate* isolate, int quality);
|
v8::Local<v8::Value> ToJPEG(v8::Isolate* isolate, int quality);
|
||||||
v8::Local<v8::Value> ToBitmap(gin::Arguments* args);
|
v8::Local<v8::Value> ToBitmap(gin::Arguments* args);
|
||||||
|
std::vector<float> GetScaleFactors();
|
||||||
v8::Local<v8::Value> GetBitmap(gin::Arguments* args);
|
v8::Local<v8::Value> GetBitmap(gin::Arguments* args);
|
||||||
v8::Local<v8::Value> GetNativeHandle(gin_helper::ErrorThrower thrower);
|
v8::Local<v8::Value> GetNativeHandle(gin_helper::ErrorThrower thrower);
|
||||||
gin::Handle<NativeImage> Resize(v8::Isolate* isolate,
|
gin::Handle<NativeImage> Resize(gin::Arguments* args,
|
||||||
base::DictionaryValue options);
|
base::DictionaryValue options);
|
||||||
gin::Handle<NativeImage> Crop(v8::Isolate* isolate, const gfx::Rect& rect);
|
gin::Handle<NativeImage> Crop(v8::Isolate* isolate, const gfx::Rect& rect);
|
||||||
std::string ToDataURL(gin::Arguments* args);
|
std::string ToDataURL(gin::Arguments* args);
|
||||||
bool IsEmpty();
|
bool IsEmpty();
|
||||||
gfx::Size GetSize();
|
gfx::Size GetSize(const base::Optional<float> scale_factor);
|
||||||
float GetAspectRatio();
|
float GetAspectRatio(const base::Optional<float> scale_factor);
|
||||||
void AddRepresentation(const gin_helper::Dictionary& options);
|
void AddRepresentation(const gin_helper::Dictionary& options);
|
||||||
|
|
||||||
// Mark the image as template image.
|
// Mark the image as template image.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ifdescribe } from './spec-helpers';
|
||||||
|
|
||||||
import { ipcMain, BrowserWindow } from 'electron/main';
|
import { ipcMain, BrowserWindow } from 'electron/main';
|
||||||
import { emittedOnce } from './events-helpers';
|
import { emittedOnce } from './events-helpers';
|
||||||
|
import { NativeImage } from 'electron/common';
|
||||||
|
|
||||||
const features = process.electronBinding('features');
|
const features = process.electronBinding('features');
|
||||||
|
|
||||||
|
@ -222,6 +223,50 @@ ifdescribe(features.isRemoteModuleEnabled())('remote module', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('nativeImage serialization', () => {
|
||||||
|
const w = makeWindow();
|
||||||
|
const remotely = makeRemotely(w);
|
||||||
|
|
||||||
|
it('can serialize an empty nativeImage', async () => {
|
||||||
|
const getEmptyImage = (img: NativeImage) => img.isEmpty();
|
||||||
|
|
||||||
|
w().webContents.once('remote-get-global', (event) => {
|
||||||
|
event.returnValue = getEmptyImage;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(remotely(() => {
|
||||||
|
const emptyImage = require('electron').nativeImage.createEmpty();
|
||||||
|
return require('electron').remote.getGlobal('someFunction')(emptyImage);
|
||||||
|
})).to.eventually.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can serialize a non-empty nativeImage', async () => {
|
||||||
|
const getNonEmptyImage = (img: NativeImage) => img.getSize();
|
||||||
|
|
||||||
|
w().webContents.once('remote-get-global', (event) => {
|
||||||
|
event.returnValue = getNonEmptyImage;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(remotely(() => {
|
||||||
|
const { nativeImage } = require('electron');
|
||||||
|
const nonEmptyImage = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFklEQVQYlWP8//8/AwMDEwMDAwMDAwAkBgMBBMzldwAAAABJRU5ErkJggg==');
|
||||||
|
return require('electron').remote.getGlobal('someFunction')(nonEmptyImage);
|
||||||
|
})).to.eventually.deep.equal({ width: 2, height: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can properly create a menu with an nativeImage icon in the renderer', async () => {
|
||||||
|
await expect(remotely(() => {
|
||||||
|
const { remote, nativeImage } = require('electron');
|
||||||
|
remote.Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'hello',
|
||||||
|
icon: nativeImage.createEmpty()
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
})).to.be.fulfilled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('remote listeners', () => {
|
describe('remote listeners', () => {
|
||||||
afterEach(closeAllWindows);
|
afterEach(closeAllWindows);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue