diff --git a/atom/common/api/atom_api_native_image.cc b/atom/common/api/atom_api_native_image.cc index 0146efa711f7..77ce36606194 100644 --- a/atom/common/api/atom_api_native_image.cc +++ b/atom/common/api/atom_api_native_image.cc @@ -11,6 +11,7 @@ #include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/gfx_converter.h" #include "atom/common/native_mate_converters/gurl_converter.h" +#include "atom/common/native_mate_converters/value_converter.h" #include "base/base64.h" #include "base/files/file_util.h" #include "base/strings/pattern.h" @@ -24,6 +25,7 @@ #include "ui/gfx/codec/png_codec.h" #include "ui/gfx/geometry/size.h" #include "ui/gfx/image/image_skia.h" +#include "ui/gfx/image/image_skia_operations.h" #include "ui/gfx/image/image_util.h" #if defined(OS_WIN) @@ -282,6 +284,57 @@ gfx::Size NativeImage::GetSize() { return image_.Size(); } +float NativeImage::GetAspectRatio() { + gfx::Size size = GetSize(); + if (size.IsEmpty()) + return 1.f; + else + return static_cast(size.width()) / static_cast(size.height()); +} + +mate::Handle NativeImage::Resize( + v8::Isolate* isolate, const base::DictionaryValue& options) { + gfx::Size size = GetSize(); + int width = size.width(); + int height = size.height(); + bool width_set = options.GetInteger("width", &width); + bool height_set = options.GetInteger("height", &height); + size.SetSize(width, height); + + 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()); + } else if (height_set && !width_set) { + // Scale width to preserve original aspect ratio + size.set_width(height); + size = gfx::ScaleToRoundedSize(size, GetAspectRatio(), 1.f); + } + + skia::ImageOperations::ResizeMethod method = + skia::ImageOperations::ResizeMethod::RESIZE_BEST; + std::string quality; + options.GetString("quality", &quality); + if (quality == "good") + method = skia::ImageOperations::ResizeMethod::RESIZE_GOOD; + else if (quality == "better") + method = skia::ImageOperations::ResizeMethod::RESIZE_BETTER; + + gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage( + image_.AsImageSkia(), method, size); + return mate::CreateHandle(isolate, + new NativeImage(isolate, gfx::Image(resized))); +} + +mate::Handle NativeImage::Crop(v8::Isolate* isolate, + const gfx::Rect& rect) { + gfx::ImageSkia cropped = gfx::ImageSkiaOperations::ExtractSubset( + image_.AsImageSkia(), rect); + return mate::CreateHandle(isolate, + new NativeImage(isolate, gfx::Image(cropped))); +} + + #if !defined(OS_MACOSX) void NativeImage::SetTemplateImage(bool setAsTemplate) { } @@ -382,6 +435,9 @@ void NativeImage::BuildPrototype( .SetMethod("getSize", &NativeImage::GetSize) .SetMethod("setTemplateImage", &NativeImage::SetTemplateImage) .SetMethod("isTemplateImage", &NativeImage::IsTemplateImage) + .SetMethod("resize", &NativeImage::Resize) + .SetMethod("crop", &NativeImage::Crop) + .SetMethod("getAspectRatio", &NativeImage::GetAspectRatio) // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings .SetMethod("toPng", &NativeImage::ToPNG) .SetMethod("toJpeg", &NativeImage::ToJPEG); diff --git a/atom/common/api/atom_api_native_image.h b/atom/common/api/atom_api_native_image.h index 743a3c599b7e..ee1b5f5d4b81 100644 --- a/atom/common/api/atom_api_native_image.h +++ b/atom/common/api/atom_api_native_image.h @@ -8,8 +8,10 @@ #include #include +#include "base/values.h" #include "native_mate/handle.h" #include "native_mate/wrappable.h" +#include "ui/gfx/geometry/rect.h" #include "ui/gfx/image/image.h" #if defined(OS_WIN) @@ -75,9 +77,14 @@ class NativeImage : public mate::Wrappable { v8::Local GetNativeHandle( v8::Isolate* isolate, mate::Arguments* args); + mate::Handle Resize(v8::Isolate* isolate, + const base::DictionaryValue& options); + mate::Handle Crop(v8::Isolate* isolate, + const gfx::Rect& rect); std::string ToDataURL(); bool IsEmpty(); gfx::Size GetSize(); + float GetAspectRatio(); // Mark the image as template image. void SetTemplateImage(bool setAsTemplate); diff --git a/docs/api/native-image.md b/docs/api/native-image.md index e2785437c82a..69df4a73897e 100644 --- a/docs/api/native-image.md +++ b/docs/api/native-image.md @@ -215,4 +215,35 @@ Marks the image as a template image. Returns `Boolean` - Whether the image is a template image. +#### `image.crop(rect)` + +* `rect` Object - The area of the image to crop + * `x` Integer + * `y` Integer + * `width` Integer + * `height` Integer + +Returns `NativeImage` - The cropped image. + +#### `image.resize(options)` + +* `options` Object + * `width` Integer (optional) + * `height` Integer (optional) + * `quality` String (optional) - The desired quality of the resize image. + Possible values are `good`, `better` or `best`. The default is `best`. + These values express a desired quality/speed tradeoff. They are translated + into an algorithm-specific method that depends on the capabilities + (CPU, GPU) of the underlying platform. It is possible for all three methods + to be mapped to the same algorithm on a given platform. + +Returns `NativeImage` - The resized image. + +If only the `height` or the `width` are specified then the current aspect ratio +will be preserved in the resized image. + +#### `image.getAspectRatio()` + +Returns `Float` - The image's aspect ratio. + [buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer diff --git a/spec/api-native-image-spec.js b/spec/api-native-image-spec.js index 4fcd710766c8..3ee5e55c25cf 100644 --- a/spec/api-native-image-spec.js +++ b/spec/api-native-image-spec.js @@ -1,10 +1,46 @@ 'use strict' const assert = require('assert') -const nativeImage = require('electron').nativeImage +const {nativeImage} = require('electron') const path = require('path') describe('nativeImage module', () => { + describe('createEmpty()', () => { + it('returns an empty image', () => { + assert(nativeImage.createEmpty().isEmpty()) + }) + }) + + describe('createFromBuffer(buffer, scaleFactor)', () => { + it('returns an empty image when the buffer is empty', () => { + assert(nativeImage.createFromBuffer(Buffer.from([])).isEmpty()) + }) + + it('returns an image created from the given buffer', () => { + const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + + const imageB = nativeImage.createFromBuffer(imageA.toPNG()) + assert.deepEqual(imageB.getSize(), {width: 538, height: 190}) + assert(imageA.toBitmap().equals(imageB.toBitmap())) + + const imageC = nativeImage.createFromBuffer(imageA.toJPEG(100)) + assert.deepEqual(imageC.getSize(), {width: 538, height: 190}) + }) + }) + + describe('createFromDataURL(dataURL)', () => { + it('returns an empty image when the dataURL is empty', () => { + assert(nativeImage.createFromDataURL('').isEmpty()) + }) + + it('returns an image created from the given buffer', () => { + const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const imageB = nativeImage.createFromDataURL(imageA.toDataURL()) + assert.deepEqual(imageB.getSize(), {width: 538, height: 190}) + assert(imageA.toBitmap().equals(imageB.toBitmap())) + }) + }) + describe('createFromPath(path)', () => { it('returns an empty image for invalid paths', () => { assert(nativeImage.createFromPath('').isEmpty()) @@ -18,24 +54,21 @@ describe('nativeImage module', () => { const imagePath = `.${path.sep}${path.join('spec', 'fixtures', 'assets', 'logo.png')}` const image = nativeImage.createFromPath(imagePath) assert(!image.isEmpty()) - assert.equal(image.getSize().height, 190) - assert.equal(image.getSize().width, 538) + assert.deepEqual(image.getSize(), {width: 538, height: 190}) }) it('loads images from paths with `.` segments', () => { const imagePath = `${path.join(__dirname, 'fixtures')}${path.sep}.${path.sep}${path.join('assets', 'logo.png')}` const image = nativeImage.createFromPath(imagePath) assert(!image.isEmpty()) - assert.equal(image.getSize().height, 190) - assert.equal(image.getSize().width, 538) + assert.deepEqual(image.getSize(), {width: 538, height: 190}) }) it('loads images from paths with `..` segments', () => { const imagePath = `${path.join(__dirname, 'fixtures', 'api')}${path.sep}..${path.sep}${path.join('assets', 'logo.png')}` const image = nativeImage.createFromPath(imagePath) assert(!image.isEmpty()) - assert.equal(image.getSize().height, 190) - assert.equal(image.getSize().width, 538) + assert.deepEqual(image.getSize(), {width: 538, height: 190}) }) it('Gets an NSImage pointer on macOS', () => { @@ -57,8 +90,67 @@ describe('nativeImage module', () => { const imagePath = path.join(__dirname, 'fixtures', 'assets', 'icon.ico') const image = nativeImage.createFromPath(imagePath) assert(!image.isEmpty()) - assert.equal(image.getSize().height, 256) - assert.equal(image.getSize().width, 256) + assert.deepEqual(image.getSize(), {width: 256, height: 256}) + }) + }) + + describe('resize(options)', () => { + it('returns a resized image', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + assert.deepEqual(image.resize({}).getSize(), {width: 538, height: 190}) + assert.deepEqual(image.resize({width: 269}).getSize(), {width: 269, height: 95}) + assert.deepEqual(image.resize({width: 600}).getSize(), {width: 600, height: 212}) + assert.deepEqual(image.resize({height: 95}).getSize(), {width: 269, height: 95}) + assert.deepEqual(image.resize({height: 200}).getSize(), {width: 566, height: 200}) + assert.deepEqual(image.resize({width: 80, height: 65}).getSize(), {width: 80, height: 65}) + assert.deepEqual(image.resize({width: 600, height: 200}).getSize(), {width: 600, height: 200}) + assert.deepEqual(image.resize({width: 0, height: 0}).getSize(), {width: 0, height: 0}) + assert.deepEqual(image.resize({width: -1, height: -1}).getSize(), {width: 0, height: 0}) + }) + + it('returns an empty image when called on an empty image', () => { + assert(nativeImage.createEmpty().resize({width: 1, height: 1}).isEmpty()) + assert(nativeImage.createEmpty().resize({width: 0, height: 0}).isEmpty()) + }) + + it('supports a quality option', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const good = image.resize({width: 100, height: 100, quality: 'good'}) + const better = image.resize({width: 100, height: 100, quality: 'better'}) + const best = image.resize({width: 100, height: 100, quality: 'best'}) + assert(good.toPNG().length <= better.toPNG().length) + assert(better.toPNG().length < best.toPNG().length) + }) + }) + + describe('crop(bounds)', () => { + it('returns an empty image when called on an empty image', () => { + assert(nativeImage.createEmpty().crop({width: 1, height: 2, x: 0, y: 0}).isEmpty()) + assert(nativeImage.createEmpty().crop({width: 0, height: 0, x: 0, y: 0}).isEmpty()) + }) + + it('returns an empty image when the bounds are invalid', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + assert(image.crop({width: 0, height: 0, x: 0, y: 0}).isEmpty()) + assert(image.crop({width: -1, height: 10, x: 0, y: 0}).isEmpty()) + assert(image.crop({width: 10, height: -35, x: 0, y: 0}).isEmpty()) + assert(image.crop({width: 100, height: 100, x: 1000, y: 1000}).isEmpty()) + }) + + it('returns a cropped image', () => { + const image = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')) + const cropA = image.crop({width: 25, height: 64, x: 0, y: 0}) + const cropB = image.crop({width: 25, height: 64, x: 30, y: 40}) + assert.deepEqual(cropA.getSize(), {width: 25, height: 64}) + assert.deepEqual(cropB.getSize(), {width: 25, height: 64}) + assert(!cropA.toPNG().equals(cropB.toPNG())) + }) + }) + + describe('getAspectRatio()', () => { + it('returns the aspect ratio of the image', () => { + assert.equal(nativeImage.createEmpty().getAspectRatio(), 1.0) + assert.equal(nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png')).getAspectRatio(), 2.8315789699554443) }) }) })