[canvaskit] Add drawImage support

Adds drawImageRect as well.

Bug: skia:
Change-Id: Ib74f92a3ee22664297d8ce7ff1d2cd2644b806b7
Reviewed-on: https://skia-review.googlesource.com/c/173990
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/experimental/canvaskit/canvaskit/example.html b/experimental/canvaskit/canvaskit/example.html
index db19e1b..876e6d5 100644
--- a/experimental/canvaskit/canvaskit/example.html
+++ b/experimental/canvaskit/canvaskit/example.html
@@ -136,35 +136,23 @@
   });
 
   fetch('https://storage.googleapis.com/skia-cdn/misc/robot.nima').then((resp) => {
-    resp.blob().then((blob) => {
-      let reader = new FileReader();
-      reader.addEventListener("loadend", function() {
-          nimaFile = reader.result;
-          NimaExample(CanvasKit, nimaFile, nimaTexture);
-      });
-      reader.readAsArrayBuffer(blob);
+    resp.arrayBuffer().then((buffer) => {
+      nimaFile = buffer;
+      NimaExample(CanvasKit, nimaFile, nimaTexture);
     });
   });
 
   fetch('https://storage.googleapis.com/skia-cdn/misc/robot.nima.png').then((resp) => {
-    resp.blob().then((blob) => {
-      let reader = new FileReader();
-      reader.addEventListener("loadend", function() {
-          nimaTexture = reader.result;
-          NimaExample(CanvasKit, nimaFile, nimaTexture);
-      });
-      reader.readAsArrayBuffer(blob);
+    resp.arrayBuffer().then((arrayBuffer) => {
+      nimaTexture = arrayBuffer;
+      NimaExample(CanvasKit, nimaFile, nimaTexture);
     });
   });
 
   fetch('https://storage.googleapis.com/skia-cdn/misc/bones.jpg').then((resp) => {
-    resp.blob().then((blob) => {
-      let reader = new FileReader();
-      reader.addEventListener("loadend", function() {
-          bonesImage = reader.result;
-          VertexAPI2(CanvasKit, bonesImage);
-      });
-      reader.readAsArrayBuffer(blob);
+    resp.arrayBuffer().then((buffer) => {
+      bonesImage = buffer;
+      VertexAPI2(CanvasKit, bonesImage);
     });
   });
 
@@ -448,25 +436,55 @@
     let skcanvas = CanvasKit.MakeCanvas(300, 300);
     let realCanvas = document.getElementById('api1_c');
 
-    for (let canvas of [skcanvas, realCanvas]) {
-      let ctx = canvas.getContext('2d');
-      ctx.font = '30px Impact'
-      ctx.rotate(.1);
-      let text = ctx.measureText('Awesome');
-      ctx.fillText('Awesome ', 50, 100);
-      ctx.strokeText('Groovy!', 60+text.width, 100);
+    let skPromise   = fetch('./test.png')
+                        // if clients want to use a Blob, they are responsible
+                        // for reading it themselves.
+                        .then((response) => response.arrayBuffer())
+                        .then((buffer) => {
+                          skcanvas._img = skcanvas.decodeImage(buffer);
+                        });
+    let realPromise = fetch('./test.png')
+                        .then((response) => response.blob())
+                        .then((blob) => createImageBitmap(blob))
+                        .then((bitmap) => {
+                          realCanvas._img = bitmap;
+                        });
 
-      // Draw line under Awesome
-      ctx.strokeStyle = 'rgba(125,0,0,0.5)';
-      ctx.beginPath();
-      ctx.lineWidth=6;
-      ctx.lineTo(50, 105);
-      ctx.lineTo(50 + text.width, 105);
-      ctx.stroke();
-    }
 
-    // TODO load image
-    document.getElementById('api1').src = skcanvas.toDataURL();
+    Promise.all([realPromise, skPromise]).then(() => {
+      for (let canvas of [skcanvas, realCanvas]) {
+        let ctx = canvas.getContext('2d');
+        ctx.fillStyle = '#EEE';
+        ctx.fillRect(0, 0, 300, 300);
+        ctx.fillStyle = 'black';
+        ctx.font = '30px Impact'
+        ctx.rotate(.1);
+        let text = ctx.measureText('Awesome');
+        ctx.fillText('Awesome ', 50, 100);
+        ctx.strokeText('Groovy!', 60+text.width, 100);
+
+        // Draw line under Awesome
+        ctx.strokeStyle = 'rgba(125,0,0,0.5)';
+        ctx.beginPath();
+        ctx.lineWidth=6;
+        ctx.lineTo(50, 105);
+        ctx.lineTo(50 + text.width, 105);
+        ctx.stroke();
+
+        // squished vertically
+        ctx.globalAlpha = 0.7
+        ctx.imageSmoothingQuality = 'medium';
+        ctx.drawImage(canvas._img, 150, 150, 150, 100);
+        ctx.rotate(-.2);
+        ctx.imageSmoothingEnabled = false;
+        ctx.drawImage(canvas._img, 100, 150, 400, 350, 10, 200, 150, 100);
+      }
+
+
+      document.getElementById('api1').src = skcanvas.toDataURL();
+      skcanvas.dispose();
+    });
+
   }
 
   function CanvasAPI2(CanvasKit) {
diff --git a/experimental/canvaskit/canvaskit/node.example.js b/experimental/canvaskit/canvaskit/node.example.js
index 955b281..b9afb7f 100644
--- a/experimental/canvaskit/canvaskit/node.example.js
+++ b/experimental/canvaskit/canvaskit/node.example.js
@@ -1,6 +1,9 @@
 console.log('hello world');
 
 const CanvasKitInit = require('./bin/canvaskit.js');
+const fs = require('fs');
+const path = require('path');
+
 
 CanvasKitInit({
   locateFile: (file) => __dirname + '/bin/'+file,
@@ -8,6 +11,9 @@
   CanvasKit = CK;
   let canvas = CanvasKit.MakeCanvas(300, 300);
 
+  let img = fs.readFileSync(path.join(__dirname, 'test.png'));
+  img = canvas.decodeImage(img);
+
   let ctx = canvas.getContext('2d');
   ctx.font = '30px Impact'
   ctx.rotate(.1);
@@ -23,6 +29,14 @@
   ctx.lineTo(50 + text.width, 102);
   ctx.stroke();
 
+  // squished vertically
+  ctx.globalAlpha = 0.7
+  ctx.imageSmoothingQuality = 'medium';
+  ctx.drawImage(img, 150, 150, 150, 100);
+  ctx.rotate(-.2);
+  ctx.imageSmoothingEnabled = false;
+  ctx.drawImage(img, 100, 150, 400, 350, 10, 200, 150, 100);
+
   // TODO load an image from file
   console.log('<img src="' + canvas.toDataURL() + '" />');
 });
diff --git a/experimental/canvaskit/canvaskit/test.png b/experimental/canvaskit/canvaskit/test.png
new file mode 100644
index 0000000..c2efb81
--- /dev/null
+++ b/experimental/canvaskit/canvaskit/test.png
Binary files differ
diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp
index 5d11347..24e37ac 100644
--- a/experimental/canvaskit/canvaskit_bindings.cpp
+++ b/experimental/canvaskit/canvaskit_bindings.cpp
@@ -21,9 +21,11 @@
 #include "SkData.h"
 #include "SkDiscretePathEffect.h"
 #include "SkEncodedImageFormat.h"
+#include "SkFilterQuality.h"
 #include "SkFontMgr.h"
 #include "SkFontMgrPriv.h"
 #include "SkGradientShader.h"
+#include "SkImage.h"
 #include "SkImageInfo.h"
 #include "SkImageShader.h"
 #include "SkMakeUnique.h"
@@ -435,6 +437,12 @@
     function("setCurrentContext", &emscripten_webgl_make_context_current);
     constant("gpu", true);
 #endif
+    function("_decodeImage", optional_override([](uintptr_t /* uint8_t*  */ iptr,
+                                                  size_t length)->sk_sp<SkImage> {
+        uint8_t* imgData = reinterpret_cast<uint8_t*>(iptr);
+        sk_sp<SkData> bytes = SkData::MakeWithoutCopy(imgData, length);
+        return SkImage::MakeFromEncoded(bytes);
+    }), allow_raw_pointers());
     function("_getRasterDirectSurface", optional_override([](const SimpleImageInfo ii,
                                                              uintptr_t /* uint8_t*  */ pptr,
                                                              size_t rowBytes)->sk_sp<SkSurface> {
@@ -577,6 +585,14 @@
             self.clear(SkColor(color));
         }))
         .function("clipPath", select_overload<void (const SkPath&, SkClipOp, bool)>(&SkCanvas::clipPath))
+        .function("drawImage", select_overload<void (const sk_sp<SkImage>&, SkScalar, SkScalar, const SkPaint*)>(&SkCanvas::drawImage), allow_raw_pointers())
+        .function("drawImageRect", optional_override([](SkCanvas& self, const sk_sp<SkImage>& image,
+                                                        SkRect src, SkRect dst,
+                                                        const SkPaint* paint, bool fastSample)->void {
+            self.drawImageRect(image, src, dst, paint,
+                               fastSample ? SkCanvas::kFast_SrcRectConstraint :
+                                            SkCanvas::kStrict_SrcRectConstraint);
+        }), allow_raw_pointers())
         .function("drawPaint", &SkCanvas::drawPaint)
         .function("drawPath", &SkCanvas::drawPath)
         .function("drawRect", &SkCanvas::drawRect)
@@ -613,6 +629,8 @@
 
     class_<SkImage>("SkImage")
         .smart_ptr<sk_sp<SkImage>>("sk_sp<SkImage>")
+        .function("height", &SkImage::height)
+        .function("width", &SkImage::width)
         .function("_encodeToData", select_overload<sk_sp<SkData>()const>(&SkImage::encodeToData))
         .function("_encodeToDataWithFormat", select_overload<sk_sp<SkData>(SkEncodedImageFormat encodedImageFormat, int quality)const>(&SkImage::encodeToData));
 
@@ -631,10 +649,11 @@
             // Add a optional_override to change it out.
             return JSColor(self.getColor());
         }))
-        .function("getStrokeWidth", &SkPaint::getStrokeWidth)
-        .function("getStrokeMiter", &SkPaint::getStrokeMiter)
+        .function("getFilterQuality", &SkPaint::getFilterQuality)
         .function("getStrokeCap", &SkPaint::getStrokeCap)
         .function("getStrokeJoin", &SkPaint::getStrokeJoin)
+        .function("getStrokeMiter", &SkPaint::getStrokeMiter)
+        .function("getStrokeWidth", &SkPaint::getStrokeWidth)
         .function("getTextSize", &SkPaint::getTextSize)
         .function("measureText", optional_override([](SkPaint& self, std::string text) {
             // TODO(kjlubick): This does not work well for non-ascii
@@ -649,13 +668,14 @@
             // Add a optional_override to change it out.
             self.setColor(SkColor(color));
         }))
+        .function("setFilterQuality", &SkPaint::setFilterQuality)
         .function("setMaskFilter", &SkPaint::setMaskFilter)
         .function("setPathEffect", &SkPaint::setPathEffect)
         .function("setShader", &SkPaint::setShader)
-        .function("setStrokeWidth", &SkPaint::setStrokeWidth)
-        .function("setStrokeMiter", &SkPaint::setStrokeMiter)
         .function("setStrokeCap", &SkPaint::setStrokeCap)
         .function("setStrokeJoin", &SkPaint::setStrokeJoin)
+        .function("setStrokeMiter", &SkPaint::setStrokeMiter)
+        .function("setStrokeWidth", &SkPaint::setStrokeWidth)
         .function("setStyle", &SkPaint::setStyle)
         .function("setTextSize", &SkPaint::setTextSize);
 
@@ -807,6 +827,12 @@
         .value("InverseWinding",    SkPath::FillType::kInverseWinding_FillType)
         .value("InverseEvenOdd",    SkPath::FillType::kInverseEvenOdd_FillType);
 
+    enum_<SkFilterQuality>("FilterQuality")
+        .value("None",   SkFilterQuality::kNone_SkFilterQuality)
+        .value("Low",    SkFilterQuality::kLow_SkFilterQuality)
+        .value("Medium", SkFilterQuality::kMedium_SkFilterQuality)
+        .value("High",   SkFilterQuality::kHigh_SkFilterQuality);
+
     enum_<SkEncodedImageFormat>("ImageFormat")
         .value("PNG",  SkEncodedImageFormat::kPNG)
         .value("JPEG", SkEncodedImageFormat::kJPEG);
diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js
index 5e2d724..b4e30a9 100644
--- a/experimental/canvaskit/externs.js
+++ b/experimental/canvaskit/externs.js
@@ -27,10 +27,14 @@
 	Color: function() {},
 	/** @return {CanvasKit.SkRect} */
 	LTRBRect: function() {},
+	/** @return {CanvasKit.SkRect} */
+	XYWHRect: function() {},
 	MakeBlurMaskFilter: function() {},
 	MakeCanvas: function() {},
 	MakeCanvasSurface: function() {},
 	MakeImageShader: function() {},
+	/** @return {CanvasKit.SkImage} */
+	MakeImageFromEncoded: function() {},
 	/** @return {LinearCanvasGradient} */
 	MakeLinearGradientShader: function() {},
 	MakeNimaActor: function() {},
@@ -58,6 +62,7 @@
 	_MakeSkDashPathEffect: function() {},
 	_MakeSkVertices: function() {},
 	_MakeTwoPointConicalGradientShader: function() {},
+	_decodeImage: function() {},
 	_getRasterDirectSurface: function() {},
 	_getRasterN32PremulSurface: function() {},
 	_getWebGLSurface: function() {},
@@ -84,6 +89,8 @@
 		// public API (from C++ bindings)
 		clear: function() {},
 		clipPath: function() {},
+		drawImage: function() {},
+		drawImageRect: function() {},
 		drawPaint: function() {},
 		drawPath: function() {},
 		drawRect: function() {},
@@ -104,6 +111,9 @@
 	},
 
 	SkImage: {
+		// public API (from C++ bindings)
+		height: function() {},
+		width: function() {},
 		// private API
 		_encodeToData: function() {},
 		_encodeToDataWithFormat: function() {},
@@ -125,6 +135,7 @@
 		copy: function() {},
 		getBlendMode: function() {},
 		getColor: function() {},
+		getFilterQuality: function() {},
 		getStrokeCap: function() {},
 		getStrokeJoin: function() {},
 		getStrokeMiter: function() {},
@@ -134,6 +145,7 @@
 		setAntiAlias: function() {},
 		setBlendMode: function() {},
 		setColor: function() {},
+		setFilterQuality: function() {},
 		setMaskFilter: function() {},
 		setPathEffect: function() {},
 		setShader: function() {},
@@ -301,6 +313,13 @@
 		InverseEvenOdd: {},
 	},
 
+	FilterQuality: {
+		None: {},
+		Low: {},
+		Medium: {},
+		High: {},
+	},
+
 	ImageFormat: {
 		PNG: {},
 		JPEG: {},
@@ -411,9 +430,10 @@
 
 // Define everything created in the canvas2d spec here
 var HTMLCanvas = {};
+HTMLCanvas.prototype.decodeImage = function() {};
+HTMLCanvas.prototype.dispose = function() {};
 HTMLCanvas.prototype.getContext = function() {};
 HTMLCanvas.prototype.toDataURL = function() {};
-HTMLCanvas.prototype.dispose = function() {};
 
 var CanvasRenderingContext2D = {};
 CanvasRenderingContext2D.prototype.addHitRegion = function() {};
@@ -428,6 +448,7 @@
 CanvasRenderingContext2D.prototype.createLinearGradient = function() {};
 CanvasRenderingContext2D.prototype.createRadialGradient = function() {};
 CanvasRenderingContext2D.prototype.drawFocusIfNeeded = function() {};
+CanvasRenderingContext2D.prototype.drawImage = function() {};
 CanvasRenderingContext2D.prototype.ellipse = function() {};
 CanvasRenderingContext2D.prototype.fill = function() {};
 CanvasRenderingContext2D.prototype.fillRect = function() {};
diff --git a/experimental/canvaskit/htmlcanvas/canvas2d.js b/experimental/canvaskit/htmlcanvas/canvas2d.js
index 1353b81..e025fb0 100644
--- a/experimental/canvaskit/htmlcanvas/canvas2d.js
+++ b/experimental/canvaskit/htmlcanvas/canvas2d.js
@@ -40,6 +40,17 @@
   function HTMLCanvas(skSurface) {
     this._surface = skSurface;
     this._context = new CanvasRenderingContext2D(skSurface.getCanvas());
+    this._imgs = [];
+
+    // Data is either an ArrayBuffer, a TypedArray, or a Node Buffer
+    this.decodeImage = function(data) {
+      var img = CanvasKit.MakeImageFromEncoded(data);
+      if (!img) {
+        throw 'Invalid input';
+      }
+      this._imgs.push(img);
+      return img;
+    }
 
     // A normal <canvas> requires that clients call getContext
     this.getContext = function(type) {
@@ -76,6 +87,9 @@
 
     this.dispose = function() {
       this._context._dispose();
+      this._imgs.forEach(function(i) {
+        i.delete();
+      });
       this._surface.dispose();
     }
   }
@@ -251,6 +265,8 @@
     this._lineDashList   = [];
     // aka SkBlendMode
     this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
+    this._imageFilterQuality = CanvasKit.FilterQuality.Low;
+    this._imageSmoothingEnabled = true;
 
     this._paint.setStrokeWidth(this._strokeWidth);
     this._paint.setBlendMode(this._globalCompositeOperation);
@@ -524,6 +540,43 @@
       }
     });
 
+    Object.defineProperty(this, 'imageSmoothingEnabled', {
+      enumerable: true,
+      get: function() {
+        return this._imageSmoothingEnabled;
+      },
+      set: function(newVal) {
+        this._imageSmoothingEnabled = !!newVal;
+      }
+    });
+
+    Object.defineProperty(this, 'imageSmoothingQuality', {
+      enumerable: true,
+      get: function() {
+        switch (this._imageFilterQuality) {
+          case CanvasKit.FilterQuality.Low:
+            return 'low';
+          case CanvasKit.FilterQuality.Medium:
+            return 'medium';
+          case CanvasKit.FilterQuality.High:
+            return 'high';
+        }
+      },
+      set: function(newQuality) {
+        switch (newQuality) {
+          case 'low':
+            this._imageFilterQuality = CanvasKit.FilterQuality.Low;
+            return;
+          case 'medium':
+            this._imageFilterQuality = CanvasKit.FilterQuality.Medium;
+            return;
+          case 'high':
+            this._imageFilterQuality = CanvasKit.FilterQuality.High;
+            return;
+        }
+      }
+    });
+
     Object.defineProperty(this, 'lineCap', {
       enumerable: true,
       get: function() {
@@ -742,7 +795,7 @@
       this._canvas.setMatrix(this._currentTransform);
       this._paint.setStyle(CanvasKit.PaintStyle.Fill);
       this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
-      this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), this._paint);
+      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint);
       this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
       this._paint.setBlendMode(this._globalCompositeOperation);
     }
@@ -784,7 +837,7 @@
       return rcg;
     }
 
-    this._commitSubpath = function () {
+    this._commitSubpath = function() {
       if (this._currentSubpath) {
         this._currentPath.addPath(this._currentSubpath, false);
         this._currentSubpath.delete();
@@ -792,6 +845,43 @@
       }
     }
 
+    this._imagePaint = function() {
+      var iPaint = this._fillPaint();
+      if (!this._imageSmoothingEnabled) {
+        iPaint.setFilterQuality(CanvasKit.FilterQuality.None);
+      } else {
+        iPaint.setFilterQuality(this._imageFilterQuality);
+      }
+      return iPaint;
+    }
+
+    this.drawImage = function(img) {
+      // 3 potential sets of arguments
+      // - image, dx, dy
+      // - image, dx, dy, dWidth, dHeight
+      // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
+      this._canvas.setMatrix(this._currentTransform);
+      // use the fillPaint, which has the globalAlpha in it
+      // which drawImageRect will use.
+      var iPaint = this._imagePaint();
+      if (arguments.length === 3 || arguments.length === 5) {
+        var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
+                          arguments[3] || img.width(), arguments[4] || img.height());
+        var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
+      } else if (arguments.length === 9){
+        var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
+                                          arguments[7], arguments[8]);
+        var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
+                                         arguments[3], arguments[4]);
+      } else {
+        throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
+      }
+      this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);
+
+      this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
+      iPaint.dispose();
+    }
+
     this.ellipse = function(x, y, radiusX, radiusY, rotation,
                             startAngle, endAngle, ccw) {
       if (!allAreFinite(arguments)) {
@@ -866,7 +956,7 @@
     this.fillRect = function(x, y, width, height) {
       var fillPaint = this._fillPaint();
       this._canvas.setMatrix(this._currentTransform);
-      this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), fillPaint);
+      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
       this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
       fillPaint.dispose();
     }
@@ -991,7 +1081,9 @@
       this._globalCompositeOperation = newState.gco;
       this._paint.setBlendMode(this._globalCompositeOperation);
       this._lineDashOffset = newState.ldo;
-      //TODO: font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
+      this._imageSmoothingEnabled = newState.ise;
+      this._imageFilterQuality = newState.isq;
+      //TODO: font, textAlign, textBaseline, direction
 
       // restores the clip
       this._canvas.restore();
@@ -1034,7 +1126,9 @@
         ga:  this._globalAlpha,
         ldo: this._lineDashOffset,
         gco: this._globalCompositeOperation,
-        //TODO: font, textAlign, textBaseline, direction, imageSmoothingEnabled, imageSmoothingQuality.
+        ise: this._imageSmoothingEnabled,
+        isq: this._imageFilterQuality,
+        //TODO: font, textAlign, textBaseline, direction
       });
       // Saves the clip
       this._canvas.save();
@@ -1161,7 +1255,7 @@
     this.strokeRect = function(x, y, width, height) {
       var strokePaint = this._strokePaint();
       this._canvas.setMatrix(this._currentTransform);
-      this._canvas.drawRect(CanvasKit.LTRBRect(x, y, x+width, y+height), strokePaint);
+      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
       this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
       strokePaint.dispose();
     }
diff --git a/experimental/canvaskit/interface.js b/experimental/canvaskit/interface.js
index 6ecb327..e3d6b3a 100644
--- a/experimental/canvaskit/interface.js
+++ b/experimental/canvaskit/interface.js
@@ -328,6 +328,15 @@
     };
   }
 
+  CanvasKit.XYWHRect = function(x, y, w, h) {
+    return {
+      fLeft: x,
+      fTop: y,
+      fRight: x+w,
+      fBottom: y+h,
+    };
+  }
+
   var nullptr = 0; // emscripten doesn't like to take null as uintptr_t
 
   // arr can be a normal JS array or a TypedArray
@@ -403,6 +412,26 @@
     return dpe;
   }
 
+  // data is a TypedArray or ArrayBuffer
+  CanvasKit.MakeImageFromEncoded = function(data) {
+    data = new Uint8Array(data);
+
+    var iptr = CanvasKit._malloc(data.byteLength);
+    CanvasKit.HEAPU8.set(data, iptr);
+    var img = CanvasKit._decodeImage(iptr, data.byteLength);
+    if (!img) {
+      SkDebug('Could not decode image');
+      CanvasKit._free(iptr);
+      return null;
+    }
+    var realDelete = img.delete.bind(img);
+    img.delete = function() {
+      CanvasKit._free(iptr);
+      realDelete();
+    }
+    return img;
+  }
+
   CanvasKit.MakeImageShader = function(imgData, xTileMode, yTileMode) {
     var iptr = CanvasKit._malloc(imgData.byteLength);
     CanvasKit.HEAPU8.set(new Uint8Array(imgData), iptr);
diff --git a/experimental/canvaskit/karma.conf.js b/experimental/canvaskit/karma.conf.js
index 31f5b46..c324460 100644
--- a/experimental/canvaskit/karma.conf.js
+++ b/experimental/canvaskit/karma.conf.js
@@ -11,13 +11,15 @@
     // list of files / patterns to load in the browser
     files: [
       { pattern: 'canvaskit/bin/canvaskit.wasm', included:false, served:true},
+      { pattern: 'tests/assets/*', included:false, served:true},
       '../../modules/pathkit/tests/testReporter.js',
       'canvaskit/bin/canvaskit.js',
       'tests/*.spec.js'
     ],
 
     proxies: {
-      '/canvaskit/': '/base/canvaskit/bin/'
+      '/assets/': '/base/tests/assets/',
+      '/canvaskit/': '/base/canvaskit/bin/',
     },
 
     // test results reporter to use
diff --git a/experimental/canvaskit/tests/assets/brickwork-texture.jpg b/experimental/canvaskit/tests/assets/brickwork-texture.jpg
new file mode 100644
index 0000000..9a7dd11
--- /dev/null
+++ b/experimental/canvaskit/tests/assets/brickwork-texture.jpg
Binary files differ
diff --git a/experimental/canvaskit/tests/assets/color_wheel.gif b/experimental/canvaskit/tests/assets/color_wheel.gif
new file mode 100644
index 0000000..ec90050
--- /dev/null
+++ b/experimental/canvaskit/tests/assets/color_wheel.gif
Binary files differ
diff --git a/experimental/canvaskit/tests/assets/mandrill_512.png b/experimental/canvaskit/tests/assets/mandrill_512.png
new file mode 100644
index 0000000..c2efb81
--- /dev/null
+++ b/experimental/canvaskit/tests/assets/mandrill_512.png
Binary files differ
diff --git a/experimental/canvaskit/tests/canvas2d.spec.js b/experimental/canvaskit/tests/canvas2d.spec.js
index 570ea0f..07a0ad8 100644
--- a/experimental/canvaskit/tests/canvas2d.spec.js
+++ b/experimental/canvaskit/tests/canvas2d.spec.js
@@ -344,40 +344,77 @@
         it('supports gradients, which respect clip/save/restore', function(done) {
             LoadCanvasKit.then(catchException(done, () => {
                 multipleCanvasTest('gradients_clip', done, (canvas) => {
-                      let ctx = canvas.getContext('2d');
+                    let ctx = canvas.getContext('2d');
 
-                      var rgradient = ctx.createRadialGradient(200, 300, 10, 100, 100, 300);
+                    var rgradient = ctx.createRadialGradient(200, 300, 10, 100, 100, 300);
 
-                      rgradient.addColorStop(0, 'red');
-                      rgradient.addColorStop(.7, 'white');
-                      rgradient.addColorStop(1, 'blue');
+                    rgradient.addColorStop(0, 'red');
+                    rgradient.addColorStop(.7, 'white');
+                    rgradient.addColorStop(1, 'blue');
 
-                      ctx.fillStyle = rgradient;
-                      ctx.globalAlpha = 0.7;
-                      ctx.fillRect(0,0,600,600);
-                      ctx.globalAlpha = 0.95;
+                    ctx.fillStyle = rgradient;
+                    ctx.globalAlpha = 0.7;
+                    ctx.fillRect(0,0,600,600);
+                    ctx.globalAlpha = 0.95;
 
-                      ctx.beginPath();
-                      ctx.arc(300, 100, 90, 0, Math.PI*1.66);
-                      ctx.closePath();
-                      ctx.strokeStyle = 'yellow';
-                      ctx.lineWidth = 5;
-                      ctx.stroke();
-                      ctx.save();
-                      ctx.clip();
+                    ctx.beginPath();
+                    ctx.arc(300, 100, 90, 0, Math.PI*1.66);
+                    ctx.closePath();
+                    ctx.strokeStyle = 'yellow';
+                    ctx.lineWidth = 5;
+                    ctx.stroke();
+                    ctx.save();
+                    ctx.clip();
 
-                      var lgradient = ctx.createLinearGradient(200, 20, 420, 40);
+                    var lgradient = ctx.createLinearGradient(200, 20, 420, 40);
 
-                      lgradient.addColorStop(0, 'green');
-                      lgradient.addColorStop(.5, 'cyan');
-                      lgradient.addColorStop(1, 'orange');
+                    lgradient.addColorStop(0, 'green');
+                    lgradient.addColorStop(.5, 'cyan');
+                    lgradient.addColorStop(1, 'orange');
 
-                      ctx.fillStyle = lgradient;
+                    ctx.fillStyle = lgradient;
 
-                      ctx.fillRect(200, 30, 200, 300);
+                    ctx.fillRect(200, 30, 200, 300);
 
-                      ctx.restore();
-                      ctx.fillRect(550, 550, 40, 40);
+                    ctx.restore();
+                    ctx.fillRect(550, 550, 40, 40);
+                });
+            }));
+        });
+
+        it('can draw png images', function(done) {
+            let skImageData = null;
+            let htmlImage = null;
+            let skPromise = fetch('/assets/mandrill_512.png')
+                .then((response) => response.arrayBuffer())
+                .then((buffer) => {
+                    skImageData = buffer;
+
+                });
+            let realPromise = fetch('/assets/mandrill_512.png')
+                .then((response) => response.blob())
+                .then((blob) => createImageBitmap(blob))
+                .then((bitmap) => {
+                    htmlImage = bitmap;
+                });
+            LoadCanvasKit.then(catchException(done, () => {
+                Promise.all([realPromise, skPromise]).then(() => {
+                    multipleCanvasTest('draw_image', done, (canvas) => {
+                        let ctx = canvas.getContext('2d');
+                        let img = htmlImage;
+                        if (canvas._config == 'software_canvas') {
+                            img = canvas.decodeImage(skImageData);
+                        }
+                        ctx.drawImage(img, 30, -200);
+
+                        ctx.globalAlpha = 0.7
+                        ctx.rotate(.1);
+                        ctx.imageSmoothingQuality = 'medium';
+                        ctx.drawImage(img, 200, 350, 150, 100);
+                        ctx.rotate(-.2);
+                        ctx.imageSmoothingEnabled = false;
+                        ctx.drawImage(img, 100, 150, 400, 350, 10, 400, 150, 100);
+                    });
                 });
             }));
         });
@@ -396,7 +433,8 @@
                                 'lineJoin', 'miterLimit', 'shadowOffsetY',
                                 'shadowBlur', 'shadowColor', 'shadowOffsetX',
                                 'globalAlpha', 'globalCompositeOperation',
-                                'lineDashOffset'];
+                                'lineDashOffset', 'imageSmoothingEnabled',
+                                'imageFilterQuality'];
 
                 // Compare all the default values of the properties of skcanvas
                 // to the default values on the properties of a real canvas.