Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nativeImage.createThumbnailFromPath API #24802

Merged
merged 25 commits into from Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/api/native-image.md
Expand Up @@ -119,6 +119,13 @@ Returns `NativeImage`

Creates an empty `NativeImage` instance.

### `nativeImage.createThumbnailFromPath(path, maxSize)` _macOS_ _Windows_

* `path` String - path to a file that we intend to construct a thumbnail out of.
* `maxSize` [Size](structures/size.md) - the maximum width and height (positive numbers) the thumbnail returned can be. The Windows implementation will ignore `maxSize.height` and scale the height according to `maxSize.width`.

Returns `Promise<NativeImage>` - fulfilled with the file's thumbnail preview image, which is a [NativeImage](native-image.md).

### `nativeImage.createFromPath(path)`

* `path` String
Expand Down
8 changes: 4 additions & 4 deletions filenames.auto.gni
Expand Up @@ -138,7 +138,6 @@ auto_filenames = {
"lib/common/api/clipboard.ts",
"lib/common/api/deprecate.ts",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
"lib/common/define-properties.ts",
"lib/common/type-utils.ts",
Expand All @@ -148,6 +147,7 @@ auto_filenames = {
"lib/renderer/api/crash-reporter.ts",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/native-image.ts",
"lib/renderer/api/remote.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/inspector.ts",
Expand Down Expand Up @@ -207,6 +207,7 @@ auto_filenames = {
"lib/browser/api/menu.ts",
"lib/browser/api/message-channel.ts",
"lib/browser/api/module-list.ts",
"lib/browser/api/native-image.ts",
"lib/browser/api/native-theme.ts",
"lib/browser/api/net-log.ts",
"lib/browser/api/net.ts",
Expand Down Expand Up @@ -241,7 +242,6 @@ auto_filenames = {
"lib/common/api/clipboard.ts",
"lib/common/api/deprecate.ts",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
"lib/common/define-properties.ts",
"lib/common/init.ts",
Expand All @@ -264,7 +264,6 @@ auto_filenames = {
"lib/common/api/clipboard.ts",
"lib/common/api/deprecate.ts",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
"lib/common/define-properties.ts",
"lib/common/init.ts",
Expand All @@ -279,6 +278,7 @@ auto_filenames = {
"lib/renderer/api/exports/electron.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/module-list.ts",
"lib/renderer/api/native-image.ts",
"lib/renderer/api/remote.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/init.ts",
Expand Down Expand Up @@ -307,7 +307,6 @@ auto_filenames = {
"lib/common/api/clipboard.ts",
"lib/common/api/deprecate.ts",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
"lib/common/define-properties.ts",
"lib/common/init.ts",
Expand All @@ -321,6 +320,7 @@ auto_filenames = {
"lib/renderer/api/exports/electron.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/module-list.ts",
"lib/renderer/api/native-image.ts",
"lib/renderer/api/remote.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
Expand Down
1 change: 1 addition & 0 deletions filenames.gni
Expand Up @@ -450,6 +450,7 @@ filenames = {
"shell/common/api/electron_api_native_image.cc",
"shell/common/api/electron_api_native_image.h",
"shell/common/api/electron_api_native_image_mac.mm",
"shell/common/api/electron_api_native_image_win.cc",
"shell/common/api/electron_api_shell.cc",
"shell/common/api/electron_api_v8_util.cc",
"shell/common/api/electron_bindings.cc",
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-list.ts
Expand Up @@ -16,6 +16,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'Menu', loader: () => require('./menu') },
{ name: 'MenuItem', loader: () => require('./menu-item') },
{ name: 'MessageChannelMain', loader: () => require('./message-channel') },
{ name: 'nativeImage', loader: () => require('./native-image') },
{ name: 'nativeTheme', loader: () => require('./native-theme') },
{ name: 'net', loader: () => require('./net') },
{ name: 'netLog', loader: () => require('./net-log') },
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-names.ts
Expand Up @@ -18,6 +18,7 @@ export const browserModuleNames = [
'inAppPurchase',
'Menu',
'MenuItem',
'nativeImage',
'nativeTheme',
'net',
'netLog',
Expand Down
File renamed without changes.
7 changes: 6 additions & 1 deletion lib/browser/init.ts
Expand Up @@ -4,7 +4,8 @@ import * as fs from 'fs';
import { Socket } from 'net';
import * as path from 'path';
import * as util from 'util';

import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import { serialize } from '@electron/internal/common/type-utils';
const Module = require('module');

// We modified the original process.argv to let node.js load the init.js,
Expand Down Expand Up @@ -214,3 +215,7 @@ if (packagePath) {
console.error('Failed to locate a valid package to load (app, app.asar or default_app.asar)');
console.error('This normally means you\'ve damaged the Electron package somehow');
}

ipcMainInternal.handle('ELECTRON_NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH', async (_, path: string, size: Electron.Size) => {
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
return serialize(await require('electron').nativeImage.createThumbnailFromPath(path, size));
});
1 change: 0 additions & 1 deletion lib/common/api/module-list.ts
@@ -1,7 +1,6 @@
// Common modules, please sort alphabetically
export const commonModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'clipboard', loader: () => require('./clipboard') },
{ name: 'nativeImage', loader: () => require('./native-image') },
{ name: 'shell', loader: () => require('./shell') },
// The internal modules, invisible unless you know their names.
{ name: 'deprecate', loader: () => require('./deprecate'), private: true }
Expand Down
1 change: 1 addition & 0 deletions lib/renderer/api/module-list.ts
Expand Up @@ -7,6 +7,7 @@ export const rendererModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'contextBridge', loader: () => require('./context-bridge') },
{ name: 'crashReporter', loader: () => require('./crash-reporter') },
{ name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
{ name: 'nativeImage', loader: () => require('./native-image') },
{ name: 'webFrame', loader: () => require('./web-frame') }
];

Expand Down
10 changes: 10 additions & 0 deletions lib/renderer/api/native-image.ts
@@ -0,0 +1,10 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import { deserialize } from '@electron/internal/common/type-utils';

const { nativeImage } = process._linkedBinding('electron_common_native_image');

nativeImage.createThumbnailFromPath = async (path: string, size: Electron.Size) => {
return deserialize(await ipcRendererInternal.invoke('ELECTRON_NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH', path, size));
};

export default nativeImage;
2 changes: 1 addition & 1 deletion lib/sandboxed_renderer/api/module-list.ts
Expand Up @@ -13,7 +13,7 @@ export const moduleList: ElectronInternal.ModuleEntry[] = [
},
{
name: 'nativeImage',
loader: () => require('@electron/internal/common/api/native-image')
loader: () => require('@electron/internal/renderer/api/native-image')
},
{
name: 'webFrame',
Expand Down
4 changes: 4 additions & 0 deletions shell/common/api/electron_api_native_image.cc
Expand Up @@ -619,6 +619,10 @@ void Initialize(v8::Local<v8::Object> exports,
native_image.SetMethod("createFromDataURL", &NativeImage::CreateFromDataURL);
native_image.SetMethod("createFromNamedImage",
&NativeImage::CreateFromNamedImage);
#if !defined(OS_LINUX)
native_image.SetMethod("createThumbnailFromPath",
&NativeImage::CreateThumbnailFromPath);
#endif
}

} // namespace
Expand Down
6 changes: 6 additions & 0 deletions shell/common/api/electron_api_native_image.h
Expand Up @@ -68,6 +68,12 @@ class NativeImage : public gin::Wrappable<NativeImage> {
const GURL& url);
static gin::Handle<NativeImage> CreateFromNamedImage(gin::Arguments* args,
std::string name);
#if !defined(OS_LINUX)
static v8::Local<v8::Promise> CreateThumbnailFromPath(
v8::Isolate* isolate,
const base::FilePath& path,
const gfx::Size& size);
#endif

static v8::Local<v8::FunctionTemplate> GetConstructor(v8::Isolate* isolate);

Expand Down
48 changes: 48 additions & 0 deletions shell/common/api/electron_api_native_image_mac.mm
Expand Up @@ -5,12 +5,17 @@
#include "shell/common/api/electron_api_native_image.h"

#include <string>
#include <utility>
#include <vector>

#import <Cocoa/Cocoa.h>
#import <QuickLook/QuickLook.h>

#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "gin/arguments.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/promise.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
Expand All @@ -34,6 +39,49 @@
return def;
}

// static
v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
v8::Isolate* isolate,
const base::FilePath& path,
const gfx::Size& size) {
gin_helper::Promise<gfx::Image> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();

if (size.IsEmpty()) {
promise.RejectWithErrorMessage("size must not be empty");
return handle;
}

CGSize cg_size = size.ToCGSize();
base::ScopedCFTypeRef<CFURLRef> cfurl = base::mac::FilePathToCFURL(path);
base::ScopedCFTypeRef<QLThumbnailRef> ql_thumbnail(
QLThumbnailCreate(kCFAllocatorDefault, cfurl, cg_size, NULL));
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
__block gin_helper::Promise<gfx::Image> p = std::move(promise);
// we do not want to blocking the main thread while waiting for quicklook to
// generate the thumbnail
QLThumbnailDispatchAsync(
ql_thumbnail,
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, /*flags*/ 0), ^{
base::ScopedCFTypeRef<CGImageRef> cg_thumbnail(
QLThumbnailCopyImage(ql_thumbnail));
if (cg_thumbnail) {
NSImage* result =
[[[NSImage alloc] initWithCGImage:cg_thumbnail
size:cg_size] autorelease];
gfx::Image thumbnail(result);
dispatch_async(dispatch_get_main_queue(), ^{
p.Resolve(thumbnail);
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
p.RejectWithErrorMessage("unable to retrieve thumbnail preview "
"image for the given path");
});
}
});
return handle;
}

ckerr marked this conversation as resolved.
Show resolved Hide resolved
gin::Handle<NativeImage> NativeImage::CreateFromNamedImage(gin::Arguments* args,
std::string name) {
@autoreleasepool {
Expand Down
106 changes: 106 additions & 0 deletions shell/common/api/electron_api_native_image_win.cc
@@ -0,0 +1,106 @@
// Copyright (c) 2020 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/common/api/electron_api_native_image.h"

#include <windows.h>

#include <thumbcache.h>
#include <wrl/client.h>

#include <string>
#include <vector>

#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/skia_util.h"
#include "ui/gfx/icon_util.h"

namespace electron {

namespace api {

// static
v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
v8::Isolate* isolate,
const base::FilePath& path,
const gfx::Size& size) {
gin_helper::Promise<gfx::Image> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
HRESULT hr;

if (size.IsEmpty()) {
ckerr marked this conversation as resolved.
Show resolved Hide resolved
promise.RejectWithErrorMessage("size must not be empty");
return handle;
}

// create an IShellItem
Microsoft::WRL::ComPtr<IShellItem> pItem;
std::wstring image_path = path.AsUTF16Unsafe();
hr = SHCreateItemFromParsingName(image_path.c_str(), nullptr,
IID_PPV_ARGS(&pItem));

if (FAILED(hr)) {
promise.RejectWithErrorMessage(
"failed to create IShellItem from the given path");
return handle;
}

// Init thumbnail cache
Microsoft::WRL::ComPtr<IThumbnailCache> pThumbnailCache;
hr = CoCreateInstance(CLSID_LocalThumbnailCache, nullptr, CLSCTX_INPROC,
IID_PPV_ARGS(&pThumbnailCache));
if (FAILED(hr)) {
promise.RejectWithErrorMessage(
"failed to acquire local thumbnail cache reference");
return handle;
}

// Populate the IShellBitmap
Microsoft::WRL::ComPtr<ISharedBitmap> pThumbnail;
WTS_CACHEFLAGS flags;
WTS_THUMBNAILID thumbId;
hr = pThumbnailCache->GetThumbnail(pItem.Get(), size.width(),
WTS_FLAGS::WTS_NONE, &pThumbnail, &flags,
&thumbId);

if (FAILED(hr)) {
promise.RejectWithErrorMessage(
"failed to get thumbnail from local thumbnail cache reference");
return handle;
}

// Init HBITMAP
HBITMAP hBitmap = NULL;
hr = pThumbnail->GetSharedBitmap(&hBitmap);
if (FAILED(hr)) {
promise.RejectWithErrorMessage("failed to extract bitmap from thumbnail");
return handle;
}

// convert HBITMAP to gfx::Image
BITMAP bitmap;
if (!GetObject(hBitmap, sizeof(bitmap), &bitmap)) {
promise.RejectWithErrorMessage("could not convert HBITMAP to BITMAP");
return handle;
}

ICONINFO icon_info;
icon_info.fIcon = TRUE;
icon_info.hbmMask = hBitmap;
icon_info.hbmColor = hBitmap;

base::win::ScopedHICON icon(CreateIconIndirect(&icon_info));
SkBitmap skbitmap = IconUtil::CreateSkBitmapFromHICON(icon.get());
gfx::ImageSkia image_skia;
image_skia.AddRepresentation(
gfx::ImageSkiaRep(skbitmap, 1.0 /*scale factor*/));
gfx::Image gfx_image = gfx::Image(image_skia);
promise.Resolve(gfx_image);
return handle;
}

} // namespace api

} // namespace electron
26 changes: 26 additions & 0 deletions spec/api-native-image-spec.js
Expand Up @@ -515,6 +515,32 @@ describe('nativeImage module', () => {
});
});

ifdescribe(process.platform !== 'linux')('createThumbnailFromPath(path, size)', () => {
it('throws when invalid size is passed', async () => {
const badSize = { width: -1, height: -1 };

await expect(
nativeImage.createThumbnailFromPath('path', badSize)
).to.eventually.be.rejectedWith('size must not be empty');
});

it('throws when a bad path is passed', async () => {
const badPath = process.platform === 'win32' ? '\\hey\\hi\\hello' : '/hey/hi/hello';
const goodSize = { width: 100, height: 100 };

await expect(
nativeImage.createThumbnailFromPath(badPath, goodSize)
).to.eventually.be.rejected();
});

it('returns native image given valid params', async () => {
const goodPath = path.join(__dirname, 'fixtures', 'assets', 'logo.png');
const goodSize = { width: 100, height: 100 };
const result = await nativeImage.createThumbnailFromPath(goodPath, goodSize);
expect(result.isEmpty()).to.equal(false);
});
});

describe('addRepresentation()', () => {
it('does not add representation when the buffer is too small', () => {
const image = nativeImage.createEmpty();
Expand Down