Bug 1239752 - Create ImageBitmap from ImageData should preserve alpha. r=roc
authorKaku Kuo <tkuo@mozilla.com>
Tue, 19 Jan 2016 15:06:20 +0800
changeset 323089 cc9415599767db66effe14268b0b7690916f8602
parent 323088 101e093a539c544f178ef7b01b34ca70b08f8816
child 323090 e4a9a3c7181a086cde9b96fd479bb446bf982db0
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc
bugs1239752
milestone47.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1239752 - Create ImageBitmap from ImageData should preserve alpha. r=roc MozReview-Commit-ID: RTf8Bva5yb
dom/canvas/ImageBitmap.cpp
dom/canvas/ImageBitmap.h
dom/canvas/test/imagebitmap_bug1239752.js
dom/canvas/test/mochitest.ini
dom/canvas/test/test_imagebitmap.html
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -384,21 +384,23 @@ HasRasterImage(HTMLImageElement& aImageE
         imgContainer->GetType() == imgIContainer::TYPE_RASTER) {
       return true;
     }
   }
 
   return false;
 }
 
-ImageBitmap::ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData)
+ImageBitmap::ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData,
+                         bool aIsPremultipliedAlpha /* = true */)
   : mParent(aGlobal)
   , mData(aData)
   , mSurface(nullptr)
   , mPictureRect(0, 0, aData->GetSize().width, aData->GetSize().height)
+  , mIsPremultipliedAlpha(aIsPremultipliedAlpha)
 {
   MOZ_ASSERT(aData, "aData is null in ImageBitmap constructor.");
 }
 
 ImageBitmap::~ImageBitmap()
 {
 }
 
@@ -428,20 +430,20 @@ ImageBitmap::PrepareForDrawTarget(gfx::D
   MOZ_ASSERT(aTarget);
 
   if (!mData) {
     return nullptr;
   }
 
   if (!mSurface) {
     mSurface = mData->GetAsSourceSurface();
-  }
 
-  if (!mSurface) {
-    return nullptr;
+    if (!mSurface) {
+      return nullptr;
+    }
   }
 
   RefPtr<DrawTarget> target = aTarget;
   IntRect surfRect(0, 0, mSurface->GetSize().width, mSurface->GetSize().height);
 
   // Check if we still need to crop our surface
   if (!mPictureRect.IsEqualEdges(surfRect)) {
 
@@ -491,16 +493,73 @@ ImageBitmap::PrepareForDrawTarget(gfx::D
       target->CopySurface(mSurface, surfPortion, dest);
       mSurface = target->Snapshot();
     }
 
     // Make mCropRect match new surface we've cropped to
     mPictureRect.MoveTo(0, 0);
   }
 
+  // Pre-multiply alpha here.
+  // Apply pre-multiply alpha only if mIsPremultipliedAlpha is false.
+  if (!mIsPremultipliedAlpha) {
+    MOZ_ASSERT(mSurface->GetFormat() == SurfaceFormat::R8G8B8A8 ||
+               mSurface->GetFormat() == SurfaceFormat::B8G8R8A8 ||
+               mSurface->GetFormat() == SurfaceFormat::A8R8G8B8);
+
+    RefPtr<DataSourceSurface> dataSourceSurface = mSurface->GetDataSurface();
+    MOZ_ASSERT(dataSourceSurface);
+
+    DataSourceSurface::ScopedMap map(dataSourceSurface, DataSourceSurface::READ_WRITE);
+    if (NS_WARN_IF(!map.IsMapped())) {
+      return nullptr;
+    }
+
+    uint8_t rIndex = 0;
+    uint8_t gIndex = 0;
+    uint8_t bIndex = 0;
+    uint8_t aIndex = 0;
+
+    if (mSurface->GetFormat() == SurfaceFormat::R8G8B8A8) {
+      rIndex = 0;
+      gIndex = 1;
+      bIndex = 2;
+      aIndex = 3;
+    } else if (mSurface->GetFormat() == SurfaceFormat::B8G8R8A8) {
+      rIndex = 2;
+      gIndex = 1;
+      bIndex = 0;
+      aIndex = 3;
+    } else if (mSurface->GetFormat() == SurfaceFormat::A8R8G8B8) {
+      rIndex = 1;
+      gIndex = 2;
+      bIndex = 3;
+      aIndex = 0;
+    }
+
+    for (int i = 0; i < dataSourceSurface->GetSize().height; ++i) {
+      uint8_t* bufferPtr = map.GetData() + map.GetStride() * i;
+      for (int i = 0; i < dataSourceSurface->GetSize().width; ++i) {
+        uint8_t r = *(bufferPtr+rIndex);
+        uint8_t g = *(bufferPtr+gIndex);
+        uint8_t b = *(bufferPtr+bIndex);
+        uint8_t a = *(bufferPtr+aIndex);
+
+        *(bufferPtr+rIndex) = gfxUtils::sPremultiplyTable[a * 256 + r];
+        *(bufferPtr+gIndex) = gfxUtils::sPremultiplyTable[a * 256 + g];
+        *(bufferPtr+bIndex) = gfxUtils::sPremultiplyTable[a * 256 + b];
+        *(bufferPtr+aIndex) = a;
+
+        bufferPtr += 4;
+      }
+    }
+
+    mSurface = dataSourceSurface;
+  }
+
   // Replace our surface with one optimized for the target we're about to draw
   // to, under the assumption it'll likely be drawn again to that target.
   // This call should be a no-op for already-optimized surfaces
   mSurface = target->OptimizeSourceSurface(mSurface);
 
   RefPtr<gfx::SourceSurface> surface(mSurface);
   return surface.forget();
 }
@@ -513,31 +572,33 @@ ImageBitmap::TransferAsImage()
   return image.forget();
 }
 
 ImageBitmapCloneData*
 ImageBitmap::ToCloneData()
 {
   ImageBitmapCloneData* result = new ImageBitmapCloneData();
   result->mPictureRect = mPictureRect;
+  result->mIsPremultipliedAlpha = mIsPremultipliedAlpha;
   RefPtr<SourceSurface> surface = mData->GetAsSourceSurface();
   result->mSurface = surface->GetDataSurface();
   MOZ_ASSERT(result->mSurface);
 
   return result;
 }
 
 /* static */ already_AddRefed<ImageBitmap>
 ImageBitmap::CreateFromCloneData(nsIGlobalObject* aGlobal,
                                  ImageBitmapCloneData* aData)
 {
   RefPtr<layers::Image> data =
     CreateImageFromSurface(aData->mSurface);
 
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data,
+                                            aData->mIsPremultipliedAlpha);
   ErrorResult rv;
   ret->SetPictureRect(aData->mPictureRect, rv);
   return ret.forget();
 }
 
 /* static */ already_AddRefed<ImageBitmap>
 ImageBitmap::CreateFromOffscreenCanvas(nsIGlobalObject* aGlobal,
                                        OffscreenCanvas& aOffscreenCanvas,
@@ -758,17 +819,18 @@ ImageBitmap::CreateInternal(nsIGlobalObj
   }
 
   if (NS_WARN_IF(!data)) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return nullptr;
   }
 
   // Create an ImageBimtap.
-  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data);
+  // ImageData's underlying data is not alpha-premultiplied.
+  RefPtr<ImageBitmap> ret = new ImageBitmap(aGlobal, data, false);
 
   // The cropping information has been handled in the CreateImageFromRawData()
   // function.
 
   return ret.forget();
 }
 
 /* static */ already_AddRefed<ImageBitmap>
@@ -1234,19 +1296,22 @@ ImageBitmap::ReadStructuredClone(JSConte
   MOZ_ASSERT(aCx);
   MOZ_ASSERT(aReader);
   // aParent might be null.
 
   uint32_t picRectX_;
   uint32_t picRectY_;
   uint32_t picRectWidth_;
   uint32_t picRectHeight_;
+  uint32_t isPremultipliedAlpha_;
+  uint32_t dummy_;
 
   if (!JS_ReadUint32Pair(aReader, &picRectX_, &picRectY_) ||
-      !JS_ReadUint32Pair(aReader, &picRectWidth_, &picRectHeight_)) {
+      !JS_ReadUint32Pair(aReader, &picRectWidth_, &picRectHeight_) ||
+      !JS_ReadUint32Pair(aReader, &isPremultipliedAlpha_, &dummy_)) {
     return nullptr;
   }
 
   int32_t picRectX = BitwiseCast<int32_t>(picRectX_);
   int32_t picRectY = BitwiseCast<int32_t>(picRectY_);
   int32_t picRectWidth = BitwiseCast<int32_t>(picRectWidth_);
   int32_t picRectHeight = BitwiseCast<int32_t>(picRectHeight_);
 
@@ -1258,17 +1323,17 @@ ImageBitmap::ReadStructuredClone(JSConte
   // called because the static analysis thinks dereferencing XPCOM objects
   // can GC (because in some cases it can!), and a return statement with a
   // JSObject* type means that JSObject* is on the stack as a raw pointer
   // while destructors are running.
   JS::Rooted<JS::Value> value(aCx);
   {
     RefPtr<layers::Image> img = CreateImageFromSurface(aClonedSurfaces[aIndex]);
     RefPtr<ImageBitmap> imageBitmap =
-      new ImageBitmap(aParent, img);
+      new ImageBitmap(aParent, img, isPremultipliedAlpha_);
 
     ErrorResult error;
     imageBitmap->SetPictureRect(IntRect(picRectX, picRectY,
                                         picRectWidth, picRectHeight), error);
     if (NS_WARN_IF(error.Failed())) {
       error.SuppressException();
       return nullptr;
     }
@@ -1288,23 +1353,25 @@ ImageBitmap::WriteStructuredClone(JSStru
 {
   MOZ_ASSERT(aWriter);
   MOZ_ASSERT(aImageBitmap);
 
   const uint32_t picRectX = BitwiseCast<uint32_t>(aImageBitmap->mPictureRect.x);
   const uint32_t picRectY = BitwiseCast<uint32_t>(aImageBitmap->mPictureRect.y);
   const uint32_t picRectWidth = BitwiseCast<uint32_t>(aImageBitmap->mPictureRect.width);
   const uint32_t picRectHeight = BitwiseCast<uint32_t>(aImageBitmap->mPictureRect.height);
+  const uint32_t isPremultipliedAlpha = aImageBitmap->mIsPremultipliedAlpha ? 1 : 0;
 
   // Indexing the cloned surfaces and send the index to the receiver.
   uint32_t index = aClonedSurfaces.Length();
 
   if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, SCTAG_DOM_IMAGEBITMAP, index)) ||
       NS_WARN_IF(!JS_WriteUint32Pair(aWriter, picRectX, picRectY)) ||
-      NS_WARN_IF(!JS_WriteUint32Pair(aWriter, picRectWidth, picRectHeight))) {
+      NS_WARN_IF(!JS_WriteUint32Pair(aWriter, picRectWidth, picRectHeight)) ||
+      NS_WARN_IF(!JS_WriteUint32Pair(aWriter, isPremultipliedAlpha, 0))) {
     return false;
   }
 
   RefPtr<SourceSurface> surface =
     aImageBitmap->mData->GetAsSourceSurface();
   RefPtr<DataSourceSurface> snapshot = surface->GetDataSurface();
   RefPtr<DataSourceSurface> dstDataSurface;
   {
--- a/dom/canvas/ImageBitmap.h
+++ b/dom/canvas/ImageBitmap.h
@@ -52,16 +52,17 @@ class PostMessageEvent; // For Structure
 class CreateImageBitmapFromBlob;
 class CreateImageBitmapFromBlobTask;
 class CreateImageBitmapFromBlobWorkerTask;
 
 struct ImageBitmapCloneData final
 {
   RefPtr<gfx::DataSourceSurface> mSurface;
   gfx::IntRect mPictureRect;
+  bool mIsPremultipliedAlpha;
 };
 
 /*
  * ImageBitmap is an opaque handler to several kinds of image-like objects from
  * HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, ImageData to
  * CanvasRenderingContext2D and Image Blob.
  *
  * An ImageBitmap could be painted to a canvas element.
@@ -136,17 +137,37 @@ public:
                        ImageBitmap* aImageBitmap);
 
   friend CreateImageBitmapFromBlob;
   friend CreateImageBitmapFromBlobTask;
   friend CreateImageBitmapFromBlobWorkerTask;
 
 protected:
 
-  ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData);
+  /*
+   * The default value of aIsPremultipliedAlpha is TRUE because that the
+   * data stored in HTMLImageElement, HTMLVideoElement, HTMLCanvasElement,
+   * CanvasRenderingContext2D are alpha-premultiplied in default.
+   *
+   * Actually, if one HTMLCanvasElement's rendering context is WebGLContext, it
+   * is possible to get un-premultipliedAlpha data out. But, we do not do it in
+   * the CreateInternal(from HTMLCanvasElement) method.
+   *
+   * It is also possible to decode an image which is encoded with alpha channel
+   * to be non-premultipliedAlpha. This could be applied in
+   * 1) the CreateInternal(from HTMLImageElement) method (which might trigger
+   *    re-decoding if the original decoded data is alpha-premultiplied) and
+   * 2) while decoding a blob. But we do not do it in both code path too.
+   *
+   * ImageData's underlying data is triggered as non-premultipliedAlpha, so set
+   * the aIsPremultipliedAlpha to be false in the
+   * CreateInternal(from ImageData) method.
+   */
+  ImageBitmap(nsIGlobalObject* aGlobal, layers::Image* aData,
+              bool aIsPremultipliedAlpha = true);
 
   virtual ~ImageBitmap();
 
   void SetPictureRect(const gfx::IntRect& aRect, ErrorResult& aRv);
 
   static already_AddRefed<ImageBitmap>
   CreateInternal(nsIGlobalObject* aGlobal, HTMLImageElement& aImageEl,
                  const Maybe<gfx::IntRect>& aCropRect, ErrorResult& aRv);
@@ -197,16 +218,18 @@ protected:
    *
    * Note that if the CreateInternal() copies and crops data from the source
    * image, then this mPictureRect is just the size of the final mData.
    *
    * The mPictureRect will be used at PrepareForDrawTarget() while user is going
    * to draw this ImageBitmap into a HTMLCanvasElement.
    */
   gfx::IntRect mPictureRect;
+
+  const bool mIsPremultipliedAlpha;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_ImageBitmap_h
 
 
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/imagebitmap_bug1239752.js
@@ -0,0 +1,85 @@
+var RGBAValues = [[42,142,23,148],
+                  [234,165,177,91],
+                  [74,228,75,195],
+                  [140,108,73,65],
+                  [25,177,3,201],
+                  [127,104,12,199],
+                  [196,93,240,131],
+                  [250,121,231,189],
+                  [175,131,215,190],
+                  [145,122,166,70],
+                  [18,196,210,162],
+                  [225,1,90,188],
+                  [223,216,182,233],
+                  [115,48,168,56],
+                  [50,206,198,199],
+                  [152,28,70,130],
+                  [176,134,133,51],
+                  [148,46,43,144],
+                  [78,171,141,95],
+                  [24,177,102,110],
+                  [0,27,127,91],
+                  [31,221,41,170],
+                  [85,7,218,146],
+                  [65,30,198,238],
+                  [121,56,123,88],
+                  [246,39,140,146],
+                  [174,195,254,149],
+                  [29,153,92,116],
+                  [17,240,5,111],
+                  [38,162,84,143],
+                  [237,159,201,244],
+                  [93,68,14,246],
+                  [143,142,82,221],
+                  [187,215,243,154],
+                  [24,121,220,53],
+                  [80,153,151,219],
+                  [202,241,250,191]];
+
+function createOneTest(rgbaValue) {
+  return new Promise(function(resolve, reject) {
+    var tolerance = 5;
+    var r = rgbaValue[0];
+    var g = rgbaValue[1];
+    var b = rgbaValue[2];
+    var a = rgbaValue[3];
+    var imageData = new ImageData(new Uint8ClampedArray([r, g, b, a]), 1, 1);
+
+    var newImageData;
+    createImageBitmap(imageData).then(
+      function(imageBitmap) {
+        var context = document.createElement("canvas").getContext("2d");
+        context.drawImage(imageBitmap, 0, 0);
+        newImageData = context.getImageData(0, 0, 1, 1);
+        var newR = newImageData.data[0];
+        var newG = newImageData.data[1];
+        var newB = newImageData.data[2];
+        var newA = newImageData.data[3];
+        var isTheSame = Math.abs(r - newR) <= tolerance &&
+                        Math.abs(g - newG) <= tolerance &&
+                        Math.abs(b - newB) <= tolerance &&
+                        Math.abs(a - newA) <= tolerance;
+        ok(isTheSame, "newImageData(" + newR + "," + newG + "," + newB + "," + newA +
+                      ") should equal to imageData(" + r + "," + g + "," + b + "," + a + ")." +
+                      "Premultiplied Alpha is handled while creating ImageBitmap from ImageData.");
+        if (isTheSame) {
+          resolve();
+        } else {
+          reject();
+        }
+      },
+      function() {
+        reject();
+      }
+    );
+  });
+}
+
+function testBug1239752() {
+  var tests = [];
+  for (var i = 0; i < RGBAValues.length; ++i) {
+    tests.push(createOneTest(RGBAValues[i]));
+  }
+
+  return Promise.all(tests);
+}
\ No newline at end of file
--- a/dom/canvas/test/mochitest.ini
+++ b/dom/canvas/test/mochitest.ini
@@ -21,16 +21,17 @@ support-files =
   image_redtransparent.png
   image_rgrg-256x256.png
   image_rrgg-256x256.png
   image_transparent.png
   image_transparent50.png
   image_yellow.png
   image_yellow75.png
   imagebitmap_bug1239300.js
+  imagebitmap_bug1239752.js
   imagebitmap_on_worker.js
   imagebitmap_structuredclone.js
   imagebitmap_structuredclone_iframe.html
   offscreencanvas.js
   offscreencanvas_mask.svg
   offscreencanvas_neuter.js
   offscreencanvas_serviceworker_inner.html
 
--- a/dom/canvas/test/test_imagebitmap.html
+++ b/dom/canvas/test/test_imagebitmap.html
@@ -7,16 +7,17 @@
 
 <img src="image_anim-gr.gif" id="image" class="resource">
 <video width="320" height="240" src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/media/test/320x240.ogv&type=video/ogg&cors=anonymous" id="video" crossOrigin="anonymous" autoplay></video>
 
 <canvas id="c1" class="output" width="128" height="128"></canvas>
 <canvas id="c2" width="128" height="128"></canvas>
 
 <script src="imagebitmap_bug1239300.js"></script>
+<script src="imagebitmap_bug1239752.js"></script>
 <script type="text/javascript">
 
 SimpleTest.waitForExplicitFinish();
 
 /**
  * [isPixel description]
  * @param  {[type]}  ctx : canvas context
  * @param  {[type]}  x   : pixel x coordinate
@@ -330,15 +331,16 @@ function runTests() {
   ctx2 = document.getElementById('c2').getContext('2d');
 
   testDraw()
     .then(testCreatePattern)
     .then(testSources)
     .then(testExceptions)
     .then(testSecurityErrors)
     .then(testBug1239300)
+    .then(testBug1239752)
     .then(SimpleTest.finish, function(ev) { failed(ev); SimpleTest.finish(); });
 }
 
 addLoadEvent(runTests);
 
 </script>
 </body>