From beaf60de0a611c4a699b04e515d72a5a60ca7718 Mon Sep 17 00:00:00 2001 From: George Xu <33054982+georgexu99@users.noreply.github.com> Date: Mon, 24 Aug 2020 09:36:13 -0700 Subject: [PATCH] feat: add nativeImage.createThumbnailFromPath API (#24802) * initial commit, mac implementation * add documentation * convert createThumbnailFromPath to async function * windows impl protoype * add tests * added test * fix * fix test * clean up * update docs * cleaning up code * fix test * retrigger CI * retrigger CI * refactor from app to native_image * windows build * lint * lint * add smart pointers, fix test * change tests and update docs * fix test, remove nolint * add renderer-main process routing to fix tests * lint * thanks sam * thanks sam --- docs/api/native-image.md | 7 ++ filenames.auto.gni | 8 +- filenames.gni | 1 + lib/browser/api/module-list.ts | 1 + lib/browser/api/module-names.ts | 1 + lib/{common => browser}/api/native-image.ts | 0 lib/browser/init.ts | 1 - lib/browser/rpc-server.ts | 6 +- lib/common/api/module-list.ts | 1 - lib/renderer/api/module-list.ts | 1 + lib/renderer/api/native-image.ts | 10 ++ lib/sandboxed_renderer/api/module-list.ts | 2 +- shell/common/api/electron_api_native_image.cc | 4 + shell/common/api/electron_api_native_image.h | 6 + .../api/electron_api_native_image_mac.mm | 48 ++++++++ .../api/electron_api_native_image_win.cc | 106 ++++++++++++++++++ spec/api-native-image-spec.js | 26 +++++ 17 files changed, 221 insertions(+), 8 deletions(-) rename lib/{common => browser}/api/native-image.ts (100%) create mode 100644 lib/renderer/api/native-image.ts create mode 100644 shell/common/api/electron_api_native_image_win.cc diff --git a/docs/api/native-image.md b/docs/api/native-image.md index e8ca45fd262a..7856412aeae1 100644 --- a/docs/api/native-image.md +++ b/docs/api/native-image.md @@ -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` - fulfilled with the file's thumbnail preview image, which is a [NativeImage](native-image.md). + ### `nativeImage.createFromPath(path)` * `path` String diff --git a/filenames.auto.gni b/filenames.auto.gni index cb3a8d442355..96103e92df79 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/filenames.gni b/filenames.gni index 26de2d06009e..ae4c2fb2de27 100644 --- a/filenames.gni +++ b/filenames.gni @@ -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", diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts index 4045974901ba..b027226e0edd 100644 --- a/lib/browser/api/module-list.ts +++ b/lib/browser/api/module-list.ts @@ -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') }, diff --git a/lib/browser/api/module-names.ts b/lib/browser/api/module-names.ts index d46a44231be8..2caeed8783d0 100644 --- a/lib/browser/api/module-names.ts +++ b/lib/browser/api/module-names.ts @@ -18,6 +18,7 @@ export const browserModuleNames = [ 'inAppPurchase', 'Menu', 'MenuItem', + 'nativeImage', 'nativeTheme', 'net', 'netLog', diff --git a/lib/common/api/native-image.ts b/lib/browser/api/native-image.ts similarity index 100% rename from lib/common/api/native-image.ts rename to lib/browser/api/native-image.ts diff --git a/lib/browser/init.ts b/lib/browser/init.ts index e1ba52bf0aed..7ef7f5032a32 100644 --- a/lib/browser/init.ts +++ b/lib/browser/init.ts @@ -4,7 +4,6 @@ import * as fs from 'fs'; import { Socket } from 'net'; import * as path from 'path'; import * as util from 'util'; - const Module = require('module'); // We modified the original process.argv to let node.js load the init.js, diff --git a/lib/browser/rpc-server.ts b/lib/browser/rpc-server.ts index 5bab076d1c15..1139f487e501 100644 --- a/lib/browser/rpc-server.ts +++ b/lib/browser/rpc-server.ts @@ -1,6 +1,6 @@ import { app } from 'electron/main'; import type { IpcMainInvokeEvent, WebContents } from 'electron/main'; -import { clipboard, crashReporter } from 'electron/common'; +import { clipboard, crashReporter, nativeImage } from 'electron/common'; import * as fs from 'fs'; import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal'; import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils'; @@ -136,3 +136,7 @@ ipcMainUtils.handleSync('ELECTRON_CRASH_REPORTER_SET_UPLOAD_TO_SERVER', (event: ipcMainUtils.handleSync('ELECTRON_CRASH_REPORTER_GET_CRASHES_DIRECTORY', () => { return crashReporter.getCrashesDirectory(); }); + +ipcMainInternal.handle('ELECTRON_NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH', async (_, path: string, size: Electron.Size) => { + return typeUtils.serialize(await nativeImage.createThumbnailFromPath(path, size)); +}); diff --git a/lib/common/api/module-list.ts b/lib/common/api/module-list.ts index a5493e552dfa..1c9d459cfa5c 100644 --- a/lib/common/api/module-list.ts +++ b/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 } diff --git a/lib/renderer/api/module-list.ts b/lib/renderer/api/module-list.ts index 1c84b104976b..dcc7394eda24 100644 --- a/lib/renderer/api/module-list.ts +++ b/lib/renderer/api/module-list.ts @@ -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') } ]; diff --git a/lib/renderer/api/native-image.ts b/lib/renderer/api/native-image.ts new file mode 100644 index 000000000000..1264a900edbe --- /dev/null +++ b/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; diff --git a/lib/sandboxed_renderer/api/module-list.ts b/lib/sandboxed_renderer/api/module-list.ts index 254f8a2d992e..ef957d36104e 100644 --- a/lib/sandboxed_renderer/api/module-list.ts +++ b/lib/sandboxed_renderer/api/module-list.ts @@ -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', diff --git a/shell/common/api/electron_api_native_image.cc b/shell/common/api/electron_api_native_image.cc index b68755912333..0bde8885b9d4 100644 --- a/shell/common/api/electron_api_native_image.cc +++ b/shell/common/api/electron_api_native_image.cc @@ -619,6 +619,10 @@ void Initialize(v8::Local 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 diff --git a/shell/common/api/electron_api_native_image.h b/shell/common/api/electron_api_native_image.h index 09da8faf79b6..5e0ec3e0933c 100644 --- a/shell/common/api/electron_api_native_image.h +++ b/shell/common/api/electron_api_native_image.h @@ -68,6 +68,12 @@ class NativeImage : public gin::Wrappable { const GURL& url); static gin::Handle CreateFromNamedImage(gin::Arguments* args, std::string name); +#if !defined(OS_LINUX) + static v8::Local CreateThumbnailFromPath( + v8::Isolate* isolate, + const base::FilePath& path, + const gfx::Size& size); +#endif static v8::Local GetConstructor(v8::Isolate* isolate); diff --git a/shell/common/api/electron_api_native_image_mac.mm b/shell/common/api/electron_api_native_image_mac.mm index 8124f5a9d4cd..68aacfdf28b2 100644 --- a/shell/common/api/electron_api_native_image_mac.mm +++ b/shell/common/api/electron_api_native_image_mac.mm @@ -5,12 +5,17 @@ #include "shell/common/api/electron_api_native_image.h" #include +#include #include #import +#import +#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" @@ -34,6 +39,49 @@ double safeShift(double in, double def) { return def; } +// static +v8::Local NativeImage::CreateThumbnailFromPath( + v8::Isolate* isolate, + const base::FilePath& path, + const gfx::Size& size) { + gin_helper::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + + if (size.IsEmpty()) { + promise.RejectWithErrorMessage("size must not be empty"); + return handle; + } + + CGSize cg_size = size.ToCGSize(); + base::ScopedCFTypeRef cfurl = base::mac::FilePathToCFURL(path); + base::ScopedCFTypeRef ql_thumbnail( + QLThumbnailCreate(kCFAllocatorDefault, cfurl, cg_size, NULL)); + __block gin_helper::Promise 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 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; +} + gin::Handle NativeImage::CreateFromNamedImage(gin::Arguments* args, std::string name) { @autoreleasepool { diff --git a/shell/common/api/electron_api_native_image_win.cc b/shell/common/api/electron_api_native_image_win.cc new file mode 100644 index 000000000000..4ae0c15103f6 --- /dev/null +++ b/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 + +#include +#include + +#include +#include + +#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 NativeImage::CreateThumbnailFromPath( + v8::Isolate* isolate, + const base::FilePath& path, + const gfx::Size& size) { + gin_helper::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + HRESULT hr; + + if (size.IsEmpty()) { + promise.RejectWithErrorMessage("size must not be empty"); + return handle; + } + + // create an IShellItem + Microsoft::WRL::ComPtr 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 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 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 diff --git a/spec/api-native-image-spec.js b/spec/api-native-image-spec.js index 23ba0bcd1b38..ae5ef33de6a9 100644 --- a/spec/api-native-image-spec.js +++ b/spec/api-native-image-spec.js @@ -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();