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 18 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 the thumbnail returned can be. The Windows implementation will ignore this parameter and scale the height according to `width`.
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

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
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
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
63 changes: 63 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,64 @@
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.width() <= 0) {
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
promise.RejectWithErrorMessage(
"invalid width, please enter a positive number");
return handle;
}

if (size.height() <= 0) {
promise.RejectWithErrorMessage(
"invalid height, please enter a positive number");
return handle;
}

if (!path.IsAbsolute()) {
promise.RejectWithErrorMessage("path must be absolute");
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
return handle;
}
ckerr marked this conversation as resolved.
Show resolved Hide resolved

// convert path to CFURLREF
NSString* ns_path = base::mac::FilePathToNSString(path);
CFURLRef cfurl = (__bridge CFURLRef)[NSURL fileURLWithPath:ns_path];
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

CGSize cg_size = CGSizeMake(size.width(), size.height());
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
QLThumbnailRef ql_thumbnail =
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
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), ^{
CGImageRef cg_thumbnail = QLThumbnailCopyImage(ql_thumbnail);
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
if (cg_thumbnail) {
NSImage* result = [[NSImage alloc] initWithCGImage:cg_thumbnail
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
size:cg_size];
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");
});
}
CFRelease(ql_thumbnail);
CGImageRelease(cg_thumbnail);
});
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
115 changes: 115 additions & 0 deletions shell/common/api/electron_api_native_image_win.cc
@@ -0,0 +1,115 @@
// 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> // NOLINT(build/include_order)

#include <thumbcache.h> // NOLINT(build/include_order)
#include <string> // NOLINT(build/include_order)
#include <vector> // NOLINT(build/include_order)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lint warning should be gone if you place headers in this way:

#include <windows.h>

#include <thumbcache.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.width() <= 0) {
promise.RejectWithErrorMessage(
"invalid width, please enter a positive number");
return handle;
}

if (!path.IsAbsolute()) {
promise.RejectWithErrorMessage("path must be absolute");
return handle;
}

// create an IShellItem
IShellItem* pItem = nullptr;
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
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
IThumbnailCache* pThumbnailCache = nullptr;
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
hr = CoCreateInstance(CLSID_LocalThumbnailCache, nullptr, CLSCTX_INPROC,
IID_PPV_ARGS(&pThumbnailCache));
if (FAILED(hr)) {
promise.RejectWithErrorMessage(
"failed to acquire local thumbnail cache reference");
pItem->Release();
return handle;
}

// Populate the IShellBitmap
ISharedBitmap* pThumbnail = nullptr;
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
WTS_CACHEFLAGS flags;
WTS_THUMBNAILID thumbId;
hr = pThumbnailCache->GetThumbnail(pItem, size.width(), WTS_FLAGS::WTS_NONE,
&pThumbnail, &flags, &thumbId);
pItem->Release();

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

// Init HBITMAP
HBITMAP hBitmap = NULL;
hr = pThumbnail->GetSharedBitmap(&hBitmap);
if (FAILED(hr)) {
promise.RejectWithErrorMessage("failed to extract bitmap from thumbnail");
pThumbnailCache->Release();
pThumbnail->Release();
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;

HICON icon(CreateIconIndirect(&icon_info));
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
SkBitmap skbitmap = IconUtil::CreateSkBitmapFromHICON(icon);
DestroyIcon(icon);
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
51 changes: 51 additions & 0 deletions spec/api-native-image-spec.js
Expand Up @@ -515,6 +515,57 @@ describe('nativeImage module', () => {
});
});

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

await expect(
nativeImage.createThumbnailFromPath('path', badSize)
).to.eventually.be.rejectedWith('invalid width, please enter a positive number');
});

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

await expect(
nativeImage.createThumbnailFromPath(badPath, goodSize)
).to.eventually.be.rejectedWith('path must be absolute');
});

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

await expect(
nativeImage.createThumbnailFromPath(badPath, goodSize)
).to.eventually.be.rejectedWith('unable to retrieve thumbnail preview image for the given path');
});

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

await expect(
nativeImage.createThumbnailFromPath(badPath, goodSize)
).to.eventually.be.rejected();
});
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

ifit(process.platform === 'darwin')('returns native image with valid params (MacOS)', async () => {
const goodPath = path.join(__dirname, 'fixtures/apps/xwindow-icon/icon.png');
const goodSize = { width: 100, height: 100 };
const result = await nativeImage.createThumbnailFromPath(goodPath, goodSize);
expect(result.isEmpty()).to.equal(false);
});

ifit(process.platform === 'win32')('returns native image with valid params (Windows)', async () => {
const goodPath = path.join(__dirname, 'fixtures\\apps\\xwindow-icon\\icon.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