Browse Source

feat: add nativeImage.createThumbnailFromPath API (#25072)

* cherry-picking

* add documentation

* convert createThumbnailFromPath to async function

* windows impl protoype

* add tests

* clean up

* fix

* fix test

* update docs

* cleaning up code

* fix test

* 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

* cherry-pick

* lint

* remove getApplicationInfoForProtocol code

* chore: fix filenames conflict

Co-authored-by: Samuel Attard <[email protected]>
George Xu 4 years ago
parent
commit
d73b67f386

+ 7 - 0
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<NativeImage>` - fulfilled with the file's thumbnail preview image, which is a [NativeImage](native-image.md).
+
 ### `nativeImage.createFromPath(path)`
 
 * `path` String

+ 4 - 4
filenames.auto.gni

@@ -138,7 +138,6 @@ auto_filenames = {
     "lib/common/api/clipboard.js",
     "lib/common/api/deprecate.ts",
     "lib/common/api/module-list.ts",
-    "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
@@ -149,6 +148,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.js",
     "lib/renderer/api/web-frame.ts",
     "lib/renderer/inspector.ts",
@@ -204,6 +204,7 @@ auto_filenames = {
     "lib/browser/api/menu.js",
     "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.js",
     "lib/browser/api/net.ts",
@@ -240,7 +241,6 @@ auto_filenames = {
     "lib/common/api/clipboard.js",
     "lib/common/api/deprecate.ts",
     "lib/common/api/module-list.ts",
-    "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
@@ -262,7 +262,6 @@ auto_filenames = {
     "lib/common/api/clipboard.js",
     "lib/common/api/deprecate.ts",
     "lib/common/api/module-list.ts",
-    "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
@@ -277,6 +276,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.js",
     "lib/renderer/api/web-frame.ts",
     "lib/renderer/init.ts",
@@ -304,7 +304,6 @@ auto_filenames = {
     "lib/common/api/clipboard.js",
     "lib/common/api/deprecate.ts",
     "lib/common/api/module-list.ts",
-    "lib/common/api/native-image.js",
     "lib/common/api/shell.js",
     "lib/common/define-properties.ts",
     "lib/common/electron-binding-setup.ts",
@@ -318,6 +317,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.js",
     "lib/renderer/api/web-frame.ts",
     "lib/renderer/ipc-renderer-internal-utils.ts",

+ 1 - 0
filenames.gni

@@ -451,6 +451,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",

+ 1 - 0
lib/browser/api/module-list.ts

@@ -15,6 +15,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') },

+ 1 - 0
lib/browser/api/module-names.ts

@@ -17,6 +17,7 @@ export const browserModuleNames = [
   'inAppPurchase',
   'Menu',
   'MenuItem',
+  'nativeImage',
   'nativeTheme',
   'net',
   'netLog',

+ 3 - 0
lib/browser/api/native-image.ts

@@ -0,0 +1,3 @@
+const { nativeImage } = process._linkedBinding('electron_common_native_image');
+
+export default nativeImage;

+ 0 - 1
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,

+ 4 - 0
lib/browser/rpc-server.js

@@ -134,3 +134,7 @@ ipcMainUtils.handleSync('ELECTRON_CRASH_REPORTER_SET_UPLOAD_TO_SERVER', (event,
 ipcMainUtils.handleSync('ELECTRON_CRASH_REPORTER_GET_CRASHES_DIRECTORY', () => {
   return electron.crashReporter.getCrashesDirectory();
 });
+
+ipcMainInternal.handle('ELECTRON_NATIVE_IMAGE_CREATE_THUMBNAIL_FROM_PATH', async (_, path, size) => {
+  return typeUtils.serialize(await electron.nativeImage.createThumbnailFromPath(path, size));
+});

+ 0 - 1
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 }

+ 1 - 0
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') }
 ];
 

+ 10 - 0
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;

+ 1 - 1
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',

+ 4 - 0
shell/common/api/electron_api_native_image.cc

@@ -600,6 +600,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

+ 6 - 0
shell/common/api/electron_api_native_image.h

@@ -64,6 +64,12 @@ class NativeImage : public gin_helper::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 void BuildPrototype(v8::Isolate* isolate,
                              v8::Local<v8::FunctionTemplate> prototype);

+ 49 - 0
shell/common/api/electron_api_native_image_mac.mm

@@ -5,11 +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"
@@ -33,6 +39,49 @@ double safeShift(double in, double def) {
   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));
+  __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;
+}
+
 gin::Handle<NativeImage> NativeImage::CreateFromNamedImage(gin::Arguments* args,
                                                            std::string name) {
   @autoreleasepool {

+ 106 - 0
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()) {
+    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 - 0
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();