Browse Source

feat: add safer nativeImage.createFromBitmap(), which does not decode PNG/JPEG (#17337)

Milan Burda 6 years ago
parent
commit
878538f2e8

+ 49 - 0
atom/common/api/atom_api_native_image.cc

@@ -504,6 +504,54 @@ mate::Handle<NativeImage> NativeImage::CreateFromPath(
   return handle;
 }
 
+// static
+mate::Handle<NativeImage> NativeImage::CreateFromBitmap(
+    mate::Arguments* args,
+    v8::Local<v8::Value> buffer,
+    const mate::Dictionary& options) {
+  if (!node::Buffer::HasInstance(buffer)) {
+    args->ThrowError("buffer must be a node Buffer");
+    return mate::Handle<NativeImage>();
+  }
+
+  unsigned int width = 0;
+  unsigned int height = 0;
+  double scale_factor = 1.;
+
+  if (!options.Get("width", &width)) {
+    args->ThrowError("width is required");
+    return mate::Handle<NativeImage>();
+  }
+
+  if (!options.Get("height", &height)) {
+    args->ThrowError("height is required");
+    return mate::Handle<NativeImage>();
+  }
+
+  auto info = SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType);
+  auto size_bytes = info.computeMinByteSize();
+
+  if (size_bytes != node::Buffer::Length(buffer)) {
+    args->ThrowError("invalid buffer size");
+    return mate::Handle<NativeImage>();
+  }
+
+  options.Get("scaleFactor", &scale_factor);
+
+  if (width == 0 || height == 0) {
+    return CreateEmpty(args->isolate());
+  }
+
+  SkBitmap bitmap;
+  bitmap.allocN32Pixels(width, height, false);
+  bitmap.setPixels(node::Buffer::Data(buffer));
+
+  gfx::ImageSkia image_skia;
+  image_skia.AddRepresentation(gfx::ImageSkiaRep(bitmap, scale_factor));
+
+  return Create(args->isolate(), gfx::Image(image_skia));
+}
+
 // static
 mate::Handle<NativeImage> NativeImage::CreateFromBuffer(
     mate::Arguments* args,
@@ -618,6 +666,7 @@ void Initialize(v8::Local<v8::Object> exports,
   mate::Dictionary dict(context->GetIsolate(), exports);
   dict.SetMethod("createEmpty", &atom::api::NativeImage::CreateEmpty);
   dict.SetMethod("createFromPath", &atom::api::NativeImage::CreateFromPath);
+  dict.SetMethod("createFromBitmap", &atom::api::NativeImage::CreateFromBitmap);
   dict.SetMethod("createFromBuffer", &atom::api::NativeImage::CreateFromBuffer);
   dict.SetMethod("createFromDataURL",
                  &atom::api::NativeImage::CreateFromDataURL);

+ 4 - 0
atom/common/api/atom_api_native_image.h

@@ -51,6 +51,10 @@ class NativeImage : public mate::Wrappable<NativeImage> {
                                                   size_t length);
   static mate::Handle<NativeImage> CreateFromPath(v8::Isolate* isolate,
                                                   const base::FilePath& path);
+  static mate::Handle<NativeImage> CreateFromBitmap(
+      mate::Arguments* args,
+      v8::Local<v8::Value> buffer,
+      const mate::Dictionary& options);
   static mate::Handle<NativeImage> CreateFromBuffer(
       mate::Arguments* args,
       v8::Local<v8::Value> buffer);

+ 14 - 1
docs/api/native-image.md

@@ -137,6 +137,19 @@ let image = nativeImage.createFromPath('/Users/somebody/images/icon.png')
 console.log(image)
 ```
 
+### `nativeImage.createFromBitmap(buffer, options)`
+
+* `buffer` [Buffer][buffer]
+* `options` Object
+  * `width` Integer
+  * `height` Integer
+  * `scaleFactor` Double (optional) - Defaults to 1.0.
+
+Returns `NativeImage`
+
+Creates a new `NativeImage` instance from `buffer` that contains the raw bitmap
+pixel data returned by `toBitmap()`. The specific format is platform-dependent.
+
 ### `nativeImage.createFromBuffer(buffer[, options])`
 
 * `buffer` [Buffer][buffer]
@@ -147,7 +160,7 @@ console.log(image)
 
 Returns `NativeImage`
 
-Creates a new `NativeImage` instance from `buffer`.
+Creates a new `NativeImage` instance from `buffer`. Tries to decode as PNG or JPEG first.
 
 ### `nativeImage.createFromDataURL(dataURL)`
 

+ 24 - 0
spec/api-native-image-spec.js

@@ -127,6 +127,30 @@ describe('nativeImage module', () => {
     })
   })
 
+  describe('createFromBitmap(buffer, options)', () => {
+    it('returns an empty image when the buffer is empty', () => {
+      expect(nativeImage.createFromBitmap(Buffer.from([]), { width: 0, height: 0 }).isEmpty())
+    })
+
+    it('returns an image created from the given buffer', () => {
+      const imageA = nativeImage.createFromPath(path.join(__dirname, 'fixtures', 'assets', 'logo.png'))
+
+      const imageB = nativeImage.createFromBitmap(imageA.toBitmap(), imageA.getSize())
+      expect(imageB.getSize()).to.deep.equal({ width: 538, height: 190 })
+
+      const imageC = nativeImage.createFromBuffer(imageA.toBitmap(), { ...imageA.getSize(), scaleFactor: 2.0 })
+      expect(imageC.getSize()).to.deep.equal({ width: 269, height: 95 })
+    })
+
+    it('throws on invalid arguments', () => {
+      expect(() => nativeImage.createFromBitmap(null, {})).to.throw('buffer must be a node Buffer')
+      expect(() => nativeImage.createFromBitmap([12, 14, 124, 12], {})).to.throw('buffer must be a node Buffer')
+      expect(() => nativeImage.createFromBitmap(Buffer.from([]), {})).to.throw('width is required')
+      expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1 })).to.throw('height is required')
+      expect(() => nativeImage.createFromBitmap(Buffer.from([]), { width: 1, height: 1 })).to.throw('invalid buffer size')
+    })
+  })
+
   describe('createFromBuffer(buffer, options)', () => {
     it('returns an empty image when the buffer is empty', () => {
       expect(nativeImage.createFromBuffer(Buffer.from([])).isEmpty())