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 336823 cc9415599767db66effe14268b0b7690916f8602
parent 336822 101e093a539c544f178ef7b01b34ca70b08f8816
child 336824 e4a9a3c7181a086cde9b96fd479bb446bf982db0
push id12189
push usercku@mozilla.com
push dateFri, 04 Mar 2016 07:52:22 +0000
reviewersroc
bugs1239752
milestone47.0a1
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>