Bug 1172796 - Part 4: Implement OffscreenCanvas::ToBlob. r=roc draft
authorMorris Tseng <mtseng@mozilla.com>
Wed, 18 Nov 2015 12:02:19 +0800
changeset 309550 88423434bcb4c32bec833b01f2c39df6e2c47e79
parent 309549 646525cfff7182a84a5c782624bf7809039a0143
child 309551 76b05bf5d585d534d51f6d5299acc26b657f24f0
push id7622
push usermtseng@mozilla.com
push dateWed, 18 Nov 2015 04:04:35 +0000
reviewersroc
bugs1172796
milestone45.0a1
Bug 1172796 - Part 4: Implement OffscreenCanvas::ToBlob. r=roc
dom/canvas/CanvasRenderingContextHelper.cpp
dom/canvas/CanvasRenderingContextHelper.h
dom/canvas/OffscreenCanvas.cpp
dom/canvas/OffscreenCanvas.h
dom/canvas/test/mochitest.ini
dom/canvas/test/offscreencanvas.js
dom/canvas/test/test_offscreencanvas_toblob.html
dom/webidl/OffscreenCanvas.webidl
--- a/dom/canvas/CanvasRenderingContextHelper.cpp
+++ b/dom/canvas/CanvasRenderingContextHelper.cpp
@@ -21,46 +21,16 @@ namespace dom {
 void
 CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
                                      nsIGlobalObject* aGlobal,
                                      FileCallback& aCallback,
                                      const nsAString& aType,
                                      JS::Handle<JS::Value> aParams,
                                      ErrorResult& aRv)
 {
-  nsAutoString type;
-  nsContentUtils::ASCIIToLower(aType, type);
-
-  nsAutoString params;
-  bool usingCustomParseOptions;
-  aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions);
-  if (aRv.Failed()) {
-    return;
-  }
-
-  if (mCurrentContext) {
-    // We disallow canvases of width or height zero, and set them to 1, so
-    // we will have a discrepancy with the sizes of the canvas and the context.
-    // That discrepancy is OK, the rest are not.
-    nsIntSize elementSize = GetWidthHeight();
-    if ((elementSize.width != mCurrentContext->GetWidth() &&
-         (elementSize.width != 0 || mCurrentContext->GetWidth() != 1)) ||
-        (elementSize.height != mCurrentContext->GetHeight() &&
-         (elementSize.height != 0 || mCurrentContext->GetHeight() != 1))) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return;
-    }
-  }
-
-  UniquePtr<uint8_t[]> imageBuffer;
-  int32_t format = 0;
-  if (mCurrentContext) {
-    imageBuffer = mCurrentContext->GetImageBuffer(&format);
-  }
-
   // Encoder callback when encoding is complete.
   class EncodeCallback : public EncodeCompleteCallback
   {
   public:
     EncodeCallback(nsIGlobalObject* aGlobal, FileCallback* aCallback)
       : mGlobal(aGlobal)
       , mFileCallback(aCallback) {}
 
@@ -92,16 +62,59 @@ CanvasRenderingContextHelper::ToBlob(JSC
 
     nsCOMPtr<nsIGlobalObject> mGlobal;
     RefPtr<FileCallback> mFileCallback;
   };
 
   RefPtr<EncodeCompleteCallback> callback =
     new EncodeCallback(aGlobal, &aCallback);
 
+  ToBlob(aCx, aGlobal, callback, aType, aParams, aRv);
+}
+
+void
+CanvasRenderingContextHelper::ToBlob(JSContext* aCx,
+                                     nsIGlobalObject* aGlobal,
+                                     EncodeCompleteCallback* aCallback,
+                                     const nsAString& aType,
+                                     JS::Handle<JS::Value> aParams,
+                                     ErrorResult& aRv)
+{
+  nsAutoString type;
+  nsContentUtils::ASCIIToLower(aType, type);
+
+  nsAutoString params;
+  bool usingCustomParseOptions;
+  aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  if (mCurrentContext) {
+    // We disallow canvases of width or height zero, and set them to 1, so
+    // we will have a discrepancy with the sizes of the canvas and the context.
+    // That discrepancy is OK, the rest are not.
+    nsIntSize elementSize = GetWidthHeight();
+    if ((elementSize.width != mCurrentContext->GetWidth() &&
+         (elementSize.width != 0 || mCurrentContext->GetWidth() != 1)) ||
+        (elementSize.height != mCurrentContext->GetHeight() &&
+         (elementSize.height != 0 || mCurrentContext->GetHeight() != 1))) {
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+  }
+
+  UniquePtr<uint8_t[]> imageBuffer;
+  int32_t format = 0;
+  if (mCurrentContext) {
+    imageBuffer = mCurrentContext->GetImageBuffer(&format);
+  }
+
+  RefPtr<EncodeCompleteCallback> callback = aCallback;
+
   aRv = ImageEncoder::ExtractDataAsync(type,
                                        params,
                                        usingCustomParseOptions,
                                        Move(imageBuffer),
                                        format,
                                        GetWidthHeight(),
                                        callback);
 }
--- a/dom/canvas/CanvasRenderingContextHelper.h
+++ b/dom/canvas/CanvasRenderingContextHelper.h
@@ -13,16 +13,17 @@ class nsICanvasRenderingContextInternal;
 class nsIGlobalObject;
 
 namespace mozilla {
 
 class ErrorResult;
 
 namespace dom {
 
+class EncodeCompleteCallback;
 class FileCallback;
 
 enum class CanvasContextType : uint8_t {
   NoContext,
   Canvas2D,
   WebGL1,
   WebGL2
 };
@@ -52,16 +53,20 @@ protected:
                                const JS::Value& aEncoderOptions,
                                nsAString& outParams,
                                bool* const outCustomParseOptions);
 
   void ToBlob(JSContext* aCx, nsIGlobalObject* global, FileCallback& aCallback,
               const nsAString& aType, JS::Handle<JS::Value> aParams,
               ErrorResult& aRv);
 
+  void ToBlob(JSContext* aCx, nsIGlobalObject* aGlobal, EncodeCompleteCallback* aCallback,
+              const nsAString& aType, JS::Handle<JS::Value> aParams,
+              ErrorResult& aRv);
+
   virtual already_AddRefed<nsICanvasRenderingContextInternal>
   CreateContext(CanvasContextType aContextType);
 
   virtual nsIntSize GetWidthHeight() = 0;
 
   CanvasContextType mCurrentContextType;
   nsCOMPtr<nsICanvasRenderingContextInternal> mCurrentContext;
 };
--- a/dom/canvas/OffscreenCanvas.cpp
+++ b/dom/canvas/OffscreenCanvas.cpp
@@ -209,16 +209,85 @@ OffscreenCanvas::CommitFrameToCompositor
 
 OffscreenCanvasCloneData*
 OffscreenCanvas::ToCloneData()
 {
   return new OffscreenCanvasCloneData(mCanvasRenderer, mWidth, mHeight,
                                       mCompositorBackendType, mNeutered, mIsWriteOnly);
 }
 
+already_AddRefed<Promise>
+OffscreenCanvas::ToBlob(JSContext* aCx,
+                        const nsAString& aType,
+                        JS::Handle<JS::Value> aParams,
+                        ErrorResult& aRv)
+{
+  // do a trust check if this is a write-only canvas
+  if (mIsWriteOnly) {
+    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+    return nullptr;
+  }
+
+  dom::workers::WorkerPrivate* workerPrivate =
+    dom::workers::GetCurrentThreadWorkerPrivate();
+  nsCOMPtr<nsIGlobalObject> global = workerPrivate->GlobalScope();
+
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  // Encoder callback when encoding is complete.
+  class EncodeCallback : public EncodeCompleteCallback
+  {
+  public:
+    EncodeCallback(nsIGlobalObject* aGlobal, Promise* aPromise)
+      : mGlobal(aGlobal)
+      , mPromise(aPromise) {}
+
+    // This is called on main thread.
+    nsresult ReceiveBlob(already_AddRefed<Blob> aBlob)
+    {
+      RefPtr<Blob> blob = aBlob;
+
+      ErrorResult rv;
+      uint64_t size = blob->GetSize(rv);
+      if (rv.Failed()) {
+        rv.SuppressException();
+      } else {
+        AutoJSAPI jsapi;
+        if (jsapi.Init(mGlobal)) {
+          JS_updateMallocCounter(jsapi.cx(), size);
+        }
+      }
+
+      if (mPromise) {
+        RefPtr<Blob> newBlob = Blob::Create(mGlobal, blob->Impl());
+        mPromise->MaybeResolve(newBlob);
+      }
+
+      mGlobal = nullptr;
+      mPromise = nullptr;
+
+      return rv.StealNSResult();
+    }
+
+    nsCOMPtr<nsIGlobalObject> mGlobal;
+    RefPtr<Promise> mPromise;
+  };
+
+  RefPtr<EncodeCompleteCallback> callback =
+    new EncodeCallback(global, promise);
+
+  CanvasRenderingContextHelper::ToBlob(aCx, global,
+                                       callback, aType, aParams, aRv);
+
+  return promise.forget();
+}
+
 /* static */ already_AddRefed<OffscreenCanvas>
 OffscreenCanvas::CreateFromCloneData(OffscreenCanvasCloneData* aData)
 {
   MOZ_ASSERT(aData);
   RefPtr<OffscreenCanvas> wc =
     new OffscreenCanvas(aData->mWidth, aData->mHeight,
                         aData->mCompositorBackendType, aData->mRenderer);
   if (aData->mNeutered) {
--- a/dom/canvas/OffscreenCanvas.h
+++ b/dom/canvas/OffscreenCanvas.h
@@ -20,16 +20,17 @@ namespace mozilla {
 class ErrorResult;
 
 namespace layers {
 class AsyncCanvasRenderer;
 class CanvasClient;
 } // namespace layers
 
 namespace dom {
+class Blob;
 
 // This is helper class for transferring OffscreenCanvas to worker thread.
 // Because OffscreenCanvas is not thread-safe. So we cannot pass Offscreen-
 // Canvas to worker thread directly. Thus, we create this helper class and
 // store necessary data in it then pass it to worker thread.
 struct OffscreenCanvasCloneData final
 {
   OffscreenCanvasCloneData(layers::AsyncCanvasRenderer* aRenderer,
@@ -102,16 +103,22 @@ public:
     }
 
     if (mHeight != aHeight) {
       mHeight = aHeight;
       CanvasAttrChanged();
     }
   }
 
+  already_AddRefed<Promise>
+  ToBlob(JSContext* aCx,
+         const nsAString& aType,
+         JS::Handle<JS::Value> aParams,
+         ErrorResult& aRv);
+
   nsICanvasRenderingContextInternal* GetContext() const
   {
     return mCurrentContext;
   }
 
   static already_AddRefed<OffscreenCanvas>
   CreateFromCloneData(OffscreenCanvasCloneData* aData);
 
--- a/dom/canvas/test/mochitest.ini
+++ b/dom/canvas/test/mochitest.ini
@@ -262,16 +262,18 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_toDataURL_parameters.html]
 [test_windingRuleUndefined.html]
 [test_2d.fillText.gradient.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # bug 1040965
 [test_2d_composite_canvaspattern_setTransform.html]
 [test_createPattern_broken.html]
 [test_setlinedash.html]
 [test_filter.html]
+[test_offscreencanvas_toblob.html]
+tags = offscreencanvas
 [test_offscreencanvas_basic_webgl.html]
 tags = offscreencanvas
 [test_offscreencanvas_dynamic_fallback.html]
 tags = offscreencanvas
 [test_offscreencanvas_sharedworker.html]
 tags = offscreencanvas
 [test_offscreencanvas_serviceworker.html]
 tags = offscreencanvas
--- a/dom/canvas/test/offscreencanvas.js
+++ b/dom/canvas/test/offscreencanvas.js
@@ -20,16 +20,24 @@ function finish() {
 function drawCount(count) {
   if (port) {
     port.postMessage({type: "draw", count: count});
   } else {
     postMessage({type: "draw", count: count});
   }
 }
 
+function sendBlob(blob) {
+  if (port) {
+    port.postMessage({type: "blob", blob: blob});
+  } else {
+    postMessage({type: "blob", blob: blob});
+  }
+}
+
 //--------------------------------------------------------------------
 // WebGL Drawing Functions
 //--------------------------------------------------------------------
 function createDrawFunc(canvas) {
   var gl;
 
   try {
     gl = canvas.getContext("experimental-webgl");
@@ -135,37 +143,42 @@ function createDrawFunc(canvas) {
 
   gl.useProgram(program);
   gl.enableVertexAttribArray(program.position);
   gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0);
 
   // Start drawing
   checkGLError('after setup');
 
-  return function(prefix) {
+  return function(prefix, needCommitFrame) {
     if (prefix) {
       prefix = "[" + prefix + "] ";
     } else {
       prefix = "";
     }
 
     gl.viewport(0, 0, canvas.width, canvas.height);
 
     preDraw(prefix);
     gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
     postDraw(prefix);
-    gl.commit();
+    if (needCommitFrame) {
+      gl.commit();
+    }
     checkGLError(prefix);
   };
 }
 
 /* entry point */
 function entryFunction(testStr, subtests, offscreenCanvas) {
   var test = testStr;
   var canvas = offscreenCanvas;
+  if (test == "webgl_imagebitmap") {
+    canvas = new OffscreenCanvas(64, 64);
+  }
 
   if (test != "subworker") {
     ok(canvas, "Canvas successfully transfered to worker");
     ok(canvas.getContext, "Canvas has getContext");
 
     ok(canvas.width == 64, "OffscreenCanvas width should be 64");
     ok(canvas.height == 64, "OffscreenCanvas height should be 64");
   }
@@ -185,61 +198,75 @@ function entryFunction(testStr, subtests
     var count = 0;
     var iid = setInterval(function() {
       if (count++ > 20) {
         clearInterval(iid);
         ok(true, "Worker is done");
         finish();
         return;
       }
-      draw("loop " +count);
+      draw("loop " +count, true);
     }, 0);
   }
   //------------------------------------------------------------------------
   // Test dynamic fallback
   //------------------------------------------------------------------------
   else if (test == "webgl_fallback") {
     draw = createDrawFunc(canvas);
     if (!draw) {
       return;
     }
 
     var count = 0;
     var iid = setInterval(function() {
       ++count;
-      draw("loop " + count);
+      draw("loop " + count, true);
       drawCount(count);
     }, 0);
   }
   //------------------------------------------------------------------------
+  // Test toBlob
+  //------------------------------------------------------------------------
+  else if (test == "webgl_toblob") {
+    draw = createDrawFunc(canvas);
+    if (!draw) {
+      return;
+    }
+
+    draw("", false);
+    canvas.toBlob().then(function(blob) {
+      sendBlob(blob);
+    });
+  }
+  //------------------------------------------------------------------------
   // Canvas Size Change from Worker
   //------------------------------------------------------------------------
   else if (test == "webgl_changesize") {
     draw = createDrawFunc(canvas);
     if (!draw) {
       finish();
       return;
     }
 
-    draw("64x64");
+    draw("64x64", true);
 
     setTimeout(function() {
       canvas.width = 128;
       canvas.height = 128;
-      draw("Increased to 128x128");
+      draw("Increased to 128x128", true);
 
       setTimeout(function() {
         canvas.width = 32;
         canvas.width = 32;
-        draw("Decreased to 32x32");
+        draw("Decreased to 32x32", true);
 
         setTimeout(function() {
           canvas.width = 64;
           canvas.height = 64;
-          draw("Increased to 64x64");
+          draw("Increased to 64x64", true);
 
           ok(true, "Worker is done");
           finish();
         }, 0);
       }, 0);
     }, 0);
   }
   //------------------------------------------------------------------------
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/test_offscreencanvas_toblob.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>WebGL in OffscreenCanvas</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<canvas id="c" width="64" height="64"></canvas>
+<canvas id="c-ref" width="64" height="64"></canvas>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+
+  var htmlCanvas = document.getElementById("c");
+  var worker = new Worker("offscreencanvas.js");
+
+  ok(htmlCanvas, "Should have HTML canvas element");
+  ok(worker, "Web worker successfully created");
+
+  worker.onmessage = function(evt) {
+    var msg = evt.data || {};
+    if (msg.type == "test") {
+      ok(msg.result, msg.name);
+    }
+    if (msg.type == "blob") {
+      // testing toBlob
+      // Fill c-ref with green color.
+      var c = document.getElementById("c-ref");
+      var ctx = c.getContext("2d");
+      ctx.rect(0, 0, 64, 64);
+      ctx.fillStyle = "#00FF00";
+      ctx.fill();
+      var reader = new FileReader();
+      reader.onload = function(e) {
+        ok(c.toDataURL() == e.target.result, "toBlob should return a 64x64 green square");
+        worker.terminate();
+        SimpleTest.finish();
+      };
+      reader.readAsDataURL(msg.blob);
+    }
+  }
+
+  ok(htmlCanvas.transferControlToOffscreen, "HTMLCanvasElement has transferControlToOffscreen function");
+
+  var offscreenCanvas = htmlCanvas.transferControlToOffscreen();
+  ok(offscreenCanvas, "Expected transferControlToOffscreen to succeed");
+
+  worker.postMessage({test: 'webgl_toblob', canvas: offscreenCanvas}, [offscreenCanvas]);
+}
+
+SpecialPowers.pushPrefEnv({'set': [
+  ['gfx.offscreencanvas.enabled', true],
+  ['webgl.force-enabled', true],
+]}, runTest);
+
+</script>
+</body>
+</html>
--- a/dom/webidl/OffscreenCanvas.webidl
+++ b/dom/webidl/OffscreenCanvas.webidl
@@ -19,11 +19,14 @@ interface OffscreenCanvas : EventTarget 
   [Pure, SetterThrows]
   attribute unsigned long width;
   [Pure, SetterThrows]
   attribute unsigned long height;
 
   [Throws]
   nsISupports? getContext(DOMString contextId,
                           optional any contextOptions = null);
+  [Throws]
+  Promise<Blob> toBlob(optional DOMString type = "",
+                       optional any encoderOptions);
 };
 
 // OffscreenCanvas implements Transferable;