Skip to content

Commit

Permalink
fix: nativeImage remote serialization (#23796)
Browse files Browse the repository at this point in the history
  • Loading branch information
codebytere committed May 28, 2020
1 parent 568d38c commit 718bbf1
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 25 deletions.
25 changes: 23 additions & 2 deletions lib/browser/remote/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import * as electron from 'electron';
import { EventEmitter } from 'events';
import objectsRegistry from './objects-registry';
import { ipcMainInternal } from '../ipc-main-internal';
import { isPromise, isSerializableObject } from '@electron/internal/common/type-utils';
import { isPromise, isSerializableObject, deserialize, serialize } from '@electron/internal/common/type-utils';

const v8Util = process.electronBinding('v8_util');
const eventBinding = process.electronBinding('event');
const features = process.electronBinding('features');
const { NativeImage } = process.electronBinding('native_image');

if (!features.isRemoteModuleEnabled()) {
throw new Error('remote module is disabled');
Expand Down Expand Up @@ -113,6 +114,9 @@ type MetaType = {
} | {
type: 'promise',
then: MetaType
} | {
type: 'nativeimage'
value: electron.NativeImage
}

// Convert a real value into meta data.
Expand All @@ -123,6 +127,8 @@ const valueToMeta = function (sender: electron.WebContents, contextId: string, v
// Recognize certain types of objects.
if (value instanceof Buffer) {
type = 'buffer';
} else if (value instanceof NativeImage) {
type = 'nativeimage';
} else if (Array.isArray(value)) {
type = 'array';
} else if (value instanceof Error) {
Expand All @@ -146,6 +152,8 @@ const valueToMeta = function (sender: electron.WebContents, contextId: string, v
type,
members: value.map((el: any) => valueToMeta(sender, contextId, el, optimizeSimpleObject))
};
} else if (type === 'nativeimage') {
return { type, value: serialize(value) };
} else if (type === 'object' || type === 'function') {
return {
type,
Expand Down Expand Up @@ -233,7 +241,10 @@ type MetaTypeFromRenderer = {
} | {
type: 'object',
name: string,
members: { name: string, value: MetaTypeFromRenderer }[]
members: {
name: string,
value: MetaTypeFromRenderer
}[]
} | {
type: 'function-with-return-value',
value: MetaTypeFromRenderer
Expand All @@ -242,6 +253,14 @@ type MetaTypeFromRenderer = {
id: number,
location: string,
length: number
} | {
type: 'nativeimage',
value: {
size: electron.Size,
buffer: Buffer,
scaleFactor: number,
dataURL: string
}[]
}

const fakeConstructor = (constructor: Function, name: string) =>
Expand All @@ -259,6 +278,8 @@ const fakeConstructor = (constructor: Function, name: string) =>
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
const metaToValue = function (meta: MetaTypeFromRenderer): any {
switch (meta.type) {
case 'nativeimage':
return deserialize(meta.value);
case 'value':
return meta.value;
case 'remote-object':
Expand Down
56 changes: 49 additions & 7 deletions lib/common/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const serializableTypes = [
ArrayBuffer
];

// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#Supported_types
export function isSerializableObject (value: any) {
return value === null || ArrayBuffer.isView(value) || serializableTypes.some(type => value instanceof type);
}
Expand All @@ -33,14 +34,55 @@ const objectMap = function (source: Object, mapper: (value: any) => any) {
return Object.fromEntries(targetEntries);
};

function serializeNativeImage (image: any) {
const representations = [];
const scaleFactors = image.getScaleFactors();

// Use Buffer when there's only one representation for better perf.
// This avoids compressing to/from PNG where it's not necessary to
// ensure uniqueness of dataURLs (since there's only one).
if (scaleFactors.length === 1) {
const scaleFactor = scaleFactors[0];
const size = image.getSize(scaleFactor);
const buffer = image.toBitmap({ scaleFactor });
representations.push({ scaleFactor, size, buffer });
} else {
// Construct from dataURLs to ensure that they are not lost in creation.
for (const scaleFactor of scaleFactors) {
const size = image.getSize(scaleFactor);
const dataURL = image.toDataURL({ scaleFactor });
representations.push({ scaleFactor, size, dataURL });
}
}
return { __ELECTRON_SERIALIZED_NativeImage__: true, representations };
}

function deserializeNativeImage (value: any) {
const image = nativeImage.createEmpty();

// Use Buffer when there's only one representation for better perf.
// This avoids compressing to/from PNG where it's not necessary to
// ensure uniqueness of dataURLs (since there's only one).
if (value.representations.length === 1) {
const { buffer, size, scaleFactor } = value.representations[0];
const { width, height } = size;
image.addRepresentation({ buffer, scaleFactor, width, height });
} else {
// Construct from dataURLs to ensure that they are not lost in creation.
for (const rep of value.representations) {
const { dataURL, size, scaleFactor } = rep;
const { width, height } = size;
image.addRepresentation({ dataURL, scaleFactor, width, height });
}
}

return image;
}

export function serialize (value: any): any {
if (value instanceof NativeImage) {
return {
buffer: value.toBitmap(),
size: value.getSize(),
__ELECTRON_SERIALIZED_NativeImage__: true
};
} else if (Array.isArray(value)) {
return serializeNativeImage(value);
} if (Array.isArray(value)) {
return value.map(serialize);
} else if (isSerializableObject(value)) {
return value;
Expand All @@ -53,7 +95,7 @@ export function serialize (value: any): any {

export function deserialize (value: any): any {
if (value && value.__ELECTRON_SERIALIZED_NativeImage__) {
return nativeImage.createFromBitmap(value.buffer, value.size);
return deserializeNativeImage(value);
} else if (Array.isArray(value)) {
return value.map(deserialize);
} else if (isSerializableObject(value)) {
Expand Down
8 changes: 6 additions & 2 deletions lib/renderer/api/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

const v8Util = process.electronBinding('v8_util');
const { hasSwitch } = process.electronBinding('command_line');
const { NativeImage } = process.electronBinding('native_image');

const { CallbacksRegistry } = require('@electron/internal/renderer/remote/callbacks-registry');
const { isPromise, isSerializableObject } = require('@electron/internal/common/type-utils');
const { isPromise, isSerializableObject, serialize, deserialize } = require('@electron/internal/common/type-utils');
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal');

const callbacksRegistry = new CallbacksRegistry();
Expand Down Expand Up @@ -41,7 +42,9 @@ function wrapArgs (args, visited = new Set()) {
};
}

if (Array.isArray(value)) {
if (value instanceof NativeImage) {
return { type: 'nativeimage', value: serialize(value) };
} else if (Array.isArray(value)) {
visited.add(value);
const meta = {
type: 'array',
Expand Down Expand Up @@ -221,6 +224,7 @@ function metaToValue (meta) {
const types = {
value: () => meta.value,
array: () => meta.members.map((member) => metaToValue(member)),
nativeimage: () => deserialize(meta.value),
buffer: () => Buffer.from(meta.value.buffer, meta.value.byteOffset, meta.value.byteLength),
promise: () => Promise.resolve({ then: metaToValue(meta.then) }),
error: () => metaToError(meta),
Expand Down
33 changes: 23 additions & 10 deletions shell/common/api/electron_api_native_image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -248,21 +248,32 @@ bool NativeImage::IsEmpty() {
return image_.IsEmpty();
}

gfx::Size NativeImage::GetSize() {
return image_.Size();
gfx::Size NativeImage::GetSize(const base::Optional<float> scale_factor) {
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());
}

std::vector<float> NativeImage::GetScaleFactors() {
gfx::ImageSkia image_skia = image_.AsImageSkia();
return image_skia.GetSupportedScales();
}

float NativeImage::GetAspectRatio() {
gfx::Size size = GetSize();
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())
return 1.f;
else
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) {
gfx::Size size = GetSize();
float scale_factor = GetScaleFactorFromOptions(args);

gfx::Size size = GetSize(scale_factor);
int width = size.width();
int height = size.height();
bool width_set = options.GetInteger("width", &width);
Expand All @@ -272,11 +283,12 @@ gin::Handle<NativeImage> NativeImage::Resize(v8::Isolate* isolate,
if (width_set && !height_set) {
// Scale height to preserve original aspect ratio
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) {
// Scale width to preserve original aspect ratio
size.set_width(height);
size = gfx::ScaleToRoundedSize(size, GetAspectRatio(), 1.f);
size = gfx::ScaleToRoundedSize(size, GetAspectRatio(scale_factor), 1.f);
}

skia::ImageOperations::ResizeMethod method =
Expand All @@ -290,8 +302,8 @@ gin::Handle<NativeImage> NativeImage::Resize(v8::Isolate* isolate,

gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage(
image_.AsImageSkia(), method, size);
return gin::CreateHandle(isolate,
new NativeImage(isolate, gfx::Image(resized)));
return gin::CreateHandle(
args->isolate(), new NativeImage(args->isolate(), gfx::Image(resized)));
}

gin::Handle<NativeImage> NativeImage::Crop(v8::Isolate* isolate,
Expand Down Expand Up @@ -506,6 +518,7 @@ void NativeImage::BuildPrototype(v8::Isolate* isolate,
.SetMethod("toJPEG", &NativeImage::ToJPEG)
.SetMethod("toBitmap", &NativeImage::ToBitmap)
.SetMethod("getBitmap", &NativeImage::GetBitmap)
.SetMethod("getScaleFactors", &NativeImage::GetScaleFactors)
.SetMethod("getNativeHandle", &NativeImage::GetNativeHandle)
.SetMethod("toDataURL", &NativeImage::ToDataURL)
.SetMethod("isEmpty", &NativeImage::IsEmpty)
Expand Down
8 changes: 5 additions & 3 deletions shell/common/api/electron_api_native_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <map>
#include <string>
#include <vector>

#include "base/values.h"
#include "gin/handle.h"
Expand Down Expand Up @@ -84,15 +85,16 @@ class NativeImage : public gin_helper::Wrappable<NativeImage> {
v8::Local<v8::Value> ToPNG(gin::Arguments* args);
v8::Local<v8::Value> ToJPEG(v8::Isolate* isolate, int quality);
v8::Local<v8::Value> ToBitmap(gin::Arguments* args);
std::vector<float> GetScaleFactors();
v8::Local<v8::Value> GetBitmap(gin::Arguments* args);
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);
gin::Handle<NativeImage> Crop(v8::Isolate* isolate, const gfx::Rect& rect);
std::string ToDataURL(gin::Arguments* args);
bool IsEmpty();
gfx::Size GetSize();
float GetAspectRatio();
gfx::Size GetSize(const base::Optional<float> scale_factor);
float GetAspectRatio(const base::Optional<float> scale_factor);
void AddRepresentation(const gin_helper::Dictionary& options);

// Mark the image as template image.
Expand Down

0 comments on commit 718bbf1

Please sign in to comment.