Bug 1404222 Part 1 - Implement shape-outside: <image>. draft
authorTing-Yu Lin <aethanyc@gmail.com>
Thu, 25 Jan 2018 14:55:18 +0800
changeset 737855 5fa7f7fe13d305afd4eb2f32ae8b27daab08dc30
parent 723950 b0baaec09caf3e1b30ec6b238f5c46ef9b3188be
child 737856 ab1a2801ac23bddd2723e29c55c03a8b63139131
push id96786
push userbmo:aethanyc@gmail.com
push dateThu, 25 Jan 2018 08:56:50 +0000
bugs1404222
milestone60.0a1
Bug 1404222 Part 1 - Implement shape-outside: <image>. When creating ImageShapeInfo, it's likely that the image is still decoding. Part 2 will add mechanism to trigger reflow after the image is ready. MozReview-Commit-ID: FpWE7g7NK70
layout/generic/nsFloatManager.cpp
layout/generic/nsFloatManager.h
layout/painting/nsImageRenderer.cpp
layout/painting/nsImageRenderer.h
--- a/layout/generic/nsFloatManager.cpp
+++ b/layout/generic/nsFloatManager.cpp
@@ -6,20 +6,22 @@
 
 /* class that manages rules for positioning floats */
 
 #include "nsFloatManager.h"
 
 #include <algorithm>
 #include <initializer_list>
 
+#include "gfxContext.h"
 #include "mozilla/ReflowInput.h"
 #include "mozilla/ShapeUtils.h"
 #include "nsBlockFrame.h"
 #include "nsError.h"
+#include "nsImageRenderer.h"
 #include "nsIPresShell.h"
 #include "nsMemory.h"
 
 using namespace mozilla;
 
 int32_t nsFloatManager::sCachedFloatManagerCount = 0;
 void* nsFloatManager::sCachedFloatManagers[NS_FLOAT_MANAGER_CACHE_SIZE];
 
@@ -575,16 +577,23 @@ public:
     const nsSize& aContainerSize);
 
   static UniquePtr<ShapeInfo> CreatePolygon(
     const UniquePtr<StyleBasicShape>& aBasicShape,
     const LogicalRect& aShapeBoxRect,
     WritingMode aWM,
     const nsSize& aContainerSize);
 
+  static UniquePtr<ShapeInfo> CreateImageShape(
+    const UniquePtr<nsStyleImage>& aShapeImage,
+    float aShapeImageThreshold,
+    nsIFrame* const aFrame,
+    WritingMode aWM,
+    const nsSize& aContainerSize);
+
 protected:
   // Compute the minimum line-axis difference between the bounding shape
   // box and its rounded corner within the given band (block-axis region).
   // This is used as a helper function to compute the LineRight() and
   // LineLeft(). See the picture in the implementation for an example.
   // RadiusL and RadiusB stand for radius on the line-axis and block-axis.
   //
   // Returns radius-x diff on the line-axis, or 0 if there's no rounded
@@ -610,16 +619,17 @@ protected:
     const nscoord aRadii[8],
     WritingMode aWM);
 };
 
 /////////////////////////////////////////////////////////////////////////////
 // RoundedBoxShapeInfo
 //
 // Implements shape-outside: <shape-box> and shape-outside: inset().
+//
 class nsFloatManager::RoundedBoxShapeInfo final : public nsFloatManager::ShapeInfo
 {
 public:
   RoundedBoxShapeInfo(const nsRect& aRect,
                       UniquePtr<nscoord[]> aRadii)
     : mRect(aRect)
     , mRadii(Move(aRadii))
   {}
@@ -948,44 +958,258 @@ nsFloatManager::PolygonShapeInfo::XInter
 
   MOZ_ASSERT(aP1.y != aP2.y,
              "A horizontal line segment results in dividing by zero error!");
 
   return aP1.x + (aY - aP1.y) * (aP2.x - aP1.x) / (aP2.y - aP1.y);
 }
 
 /////////////////////////////////////////////////////////////////////////////
+// ImageShapeInfo
+//
+// Implements shape-outside: <image>
+//
+class nsFloatManager::ImageShapeInfo final : public nsFloatManager::ShapeInfo
+{
+public:
+  ImageShapeInfo(uint8_t* aAlphaBitmaps,
+                 const CSSIntSize& aSize,
+                 float aShapeImageThreshold,
+                 const nsRect& aContentRect,
+                 WritingMode aWM,
+                 const nsSize& aContainerSize);
+
+  nscoord LineLeft(const nscoord aBStart,
+                   const nscoord aBEnd) const override;
+  nscoord LineRight(const nscoord aBStart,
+                    const nscoord aBEnd) const override;
+  nscoord BStart() const override { return mBStart; }
+  nscoord BEnd() const override { return mBEnd; }
+  bool IsEmpty() const override { return mIntervals.IsEmpty(); }
+
+  void Translate(nscoord aLineLeft, nscoord aBlockStart) override;
+
+private:
+  // An Interval stores line-left and line-right at a given block coordinate
+  // in the float manager's coordinate space. The Y() of mLineLeft and
+  // mLineRight should be the same.
+  struct Interval {
+    nsPoint mLineLeft;
+    nsPoint mLineRight;
+  };
+
+  // The intervals should be sorted in ascending order on mLineLeft.Y().
+  nsTArray<Interval> mIntervals;
+
+  nscoord mBStart = nscoord_MAX;
+  nscoord mBEnd = nscoord_MIN;
+};
+
+nsFloatManager::ImageShapeInfo::ImageShapeInfo(
+  uint8_t* aAlphaPixels,
+  const CSSIntSize& aImageSize,
+  float aShapeImageThreshold,
+  const nsRect& aContentRect,
+  WritingMode aWM,
+  const nsSize& aContainerSize)
+{
+  MOZ_ASSERT(aShapeImageThreshold >=0.0 && aShapeImageThreshold <=1.0,
+             "The computed value of shape-image-threshold is wrong!");
+
+  const uint8_t threshold = NSToIntRound(aShapeImageThreshold * 255);
+  const int32_t w = aImageSize.width;
+  const int32_t h = aImageSize.height;
+
+  int32_t min = -1;
+  int32_t max = -1;
+
+  // Scan the pixels row by row, from top to bottom (if vertical, column by
+  // column, from left to right).
+  for (int32_t index = 0; index < w * h; ++index) {
+    const int32_t col = aWM.IsVertical() ? index / h : index % w;
+    const int32_t row = aWM.IsVertical() ? index % h : index / w;
+    const int32_t curr = aWM.IsVertical() ? row : col;
+
+    // At the beginning of a row (or column if vertical).
+    if (curr == 0) {
+      min = -1;
+      max = -1;
+    }
+
+    // Find the min and max columns (rows if vertical) in those pixels whose
+    // alpha channel is greater than the threshold.
+    // https://drafts.csswg.org/css-shapes-1/#valdef-shape-image-threshold-number
+    const uint8_t alpha = aAlphaPixels[col + row * w];
+    if (alpha > threshold) {
+      if (min == -1) {
+        min = curr;
+      }
+      if (max < curr) {
+        max = curr;
+      }
+    }
+
+    // At the end of a row (or column if vertical), and found something.
+    if (curr == (aWM.IsVertical() ? h - 1 : w - 1) && (min != -1)) {
+      // Make the position be relative to the container.
+      nsPoint lineLeft(aContentRect.TopLeft());
+      nsPoint lineRight(aContentRect.TopLeft());
+
+      nscoord colAppUnits = CSSPixel::ToAppUnits(col);
+      nscoord rowAppUnits = CSSPixel::ToAppUnits(row);
+      nscoord minAppUnits = CSSPixel::ToAppUnits(min);
+      // Add one to max because we need the position of the pixels's right
+      // edge (or bottom edge if vertical).
+      nscoord maxAppUnits = CSSPixel::ToAppUnits(max + 1);
+
+      if (aWM.IsVerticalLR() && aWM.IsSideways()) {
+        // sideways-lr: its physical directions of line-left and line-right
+        // are bottom and top, which are the opposite of other vertical
+        // writing modes.
+        lineLeft.MoveBy(colAppUnits, maxAppUnits);
+        lineRight.MoveBy(colAppUnits, minAppUnits);
+      } else if (aWM.IsVertical()) {
+        lineLeft.MoveBy(colAppUnits, minAppUnits);
+        lineRight.MoveBy(colAppUnits, maxAppUnits);
+      } else {
+        // horizontal-tb
+        lineLeft.MoveBy(minAppUnits, rowAppUnits);
+        lineRight.MoveBy(maxAppUnits, rowAppUnits);
+      }
+
+      mIntervals.AppendElement(
+        Interval{ ConvertToFloatLogical(lineLeft, aWM, aContainerSize),
+                  ConvertToFloatLogical(lineRight, aWM, aContainerSize) });
+    }
+  }
+
+  if (aWM.IsVerticalRL()) {
+    // Because we scan the columns from left to right, we need to reverse
+    // the array so that it's sorted (in ascending order) on the block
+    // direction.
+    mIntervals.Reverse();
+  }
+
+  if (!mIntervals.IsEmpty()) {
+    mBStart = mIntervals[0].mLineLeft.Y();
+    mBEnd = mIntervals[mIntervals.Length() - 1].mLineLeft.Y();
+  }
+}
+
+nscoord
+nsFloatManager::ImageShapeInfo::LineLeft(const nscoord aBStart,
+                                         const nscoord aBEnd) const
+{
+  MOZ_ASSERT(aBStart <= aBEnd,
+             "The band's block start is greater than its block end?");
+
+  nscoord lineLeft = nscoord_MAX;
+
+  for (const Interval& interval : mIntervals) {
+    nscoord bCoord = interval.mLineLeft.Y();
+
+    if (bCoord < aBStart) {
+      continue;
+    }
+
+    if (bCoord > aBEnd) {
+      break;
+    }
+
+    lineLeft = std::min(lineLeft, interval.mLineLeft.X());
+  }
+
+  return lineLeft;
+}
+
+nscoord
+nsFloatManager::ImageShapeInfo::LineRight(const nscoord aBStart,
+                                          const nscoord aBEnd) const
+{
+  MOZ_ASSERT(aBStart <= aBEnd,
+             "The band's block start is greater than its block end?");
+
+  nscoord lineRight = nscoord_MIN;
+
+  for (const Interval& interval : mIntervals) {
+    nscoord bCoord = interval.mLineRight.Y();
+
+    if (bCoord < aBStart) {
+      continue;
+    }
+
+    if (bCoord > aBEnd) {
+      break;
+    }
+
+    lineRight = std::max(lineRight, interval.mLineRight.X());
+  }
+
+  return lineRight;
+}
+
+void
+nsFloatManager::ImageShapeInfo::Translate(nscoord aLineLeft,
+                                          nscoord aBlockStart)
+{
+  for (Interval& interval : mIntervals) {
+    interval.mLineLeft.MoveBy(aLineLeft, aBlockStart);
+    interval.mLineRight.MoveBy(aLineLeft, aBlockStart);
+  }
+
+  mBStart += aBlockStart;
+  mBEnd += aBlockStart;
+}
+
+/////////////////////////////////////////////////////////////////////////////
 // FloatInfo
 
 nsFloatManager::FloatInfo::FloatInfo(nsIFrame* aFrame,
                                      nscoord aLineLeft, nscoord aBlockStart,
                                      const LogicalRect& aMarginRect,
                                      WritingMode aWM,
                                      const nsSize& aContainerSize)
   : mFrame(aFrame)
   , mRect(ShapeInfo::ConvertToFloatLogical(aMarginRect, aWM, aContainerSize) +
           nsPoint(aLineLeft, aBlockStart))
 {
   MOZ_COUNT_CTOR(nsFloatManager::FloatInfo);
 
+  if (IsEmpty()) {
+    // Per spec, a float area defined by a shape is clipped to the float’s
+    // margin box. Therefore, no need to create a shape info if the float's
+    // margin rect is empty.
+    return;
+  }
+
   const StyleShapeSource& shapeOutside = mFrame->StyleDisplay()->mShapeOutside;
 
   switch (shapeOutside.GetType()) {
     case StyleShapeSourceType::None:
       // No need to create shape info.
       return;
 
     case StyleShapeSourceType::URL:
       MOZ_ASSERT_UNREACHABLE("shape-outside doesn't have URL source type!");
       return;
 
-    case StyleShapeSourceType::Image:
-      // Bug 1265343: Implement 'shape-image-threshold'
-      // Bug 1404222: Support shape-outside: <image>
-      return;
+    case StyleShapeSourceType::Image: {
+      float shapeImageThreshold = mFrame->StyleDisplay()->mShapeImageThreshold;
+      mShapeInfo = ShapeInfo::CreateImageShape(shapeOutside.GetShapeImage(),
+                                               shapeImageThreshold,
+                                               mFrame,
+                                               aWM,
+                                               aContainerSize);
+      if (!mShapeInfo) {
+        // Image is not ready, or fails to load, etc.
+        return;
+      }
+
+      break;
+    }
 
     case StyleShapeSourceType::Box: {
       // Initialize <shape-box>'s reference rect.
       LogicalRect shapeBoxRect =
         ShapeInfo::ComputeShapeBoxRect(shapeOutside, mFrame, aMarginRect, aWM);
       mShapeInfo = ShapeInfo::CreateShapeBox(mFrame, shapeBoxRect, aWM,
                                              aContainerSize);
       break;
@@ -1272,16 +1496,75 @@ nsFloatManager::ShapeInfo::CreatePolygon
   // Convert all the physical vertices to logical.
   for (nsPoint& vertex : vertices) {
     vertex = ConvertToFloatLogical(vertex, aWM, aContainerSize);
   }
 
   return MakeUnique<PolygonShapeInfo>(Move(vertices));
 }
 
+/* static */ UniquePtr<nsFloatManager::ShapeInfo>
+nsFloatManager::ShapeInfo::CreateImageShape(
+  const UniquePtr<nsStyleImage>& aShapeImage,
+  float aShapeImageThreshold,
+  nsIFrame* const aFrame,
+  WritingMode aWM,
+  const nsSize& aContainerSize)
+{
+  MOZ_ASSERT(aShapeImage ==
+             aFrame->StyleDisplay()->mShapeOutside.GetShapeImage(),
+             "aFrame should be the frame that we got aShapeImage from");
+
+  nsImageRenderer imageRenderer(aFrame, aShapeImage.get(),
+                                nsImageRenderer::FLAG_SYNC_DECODE_IMAGES);
+
+  if (!imageRenderer.PrepareImage()) {
+    // The image is not ready yet.
+    return nullptr;
+  }
+
+  nsRect contentRect = aFrame->GetContentRect();
+
+  // Use empty CSSSizeOrRatio to force set the preferred size as the frame's
+  // content box size.
+  imageRenderer.SetPreferredSize(CSSSizeOrRatio(), contentRect.Size());
+
+  // Create a draw target and draw shape image on it.
+  CSSIntSize imageIntSize = CSSIntSize::FromAppUnitsRounded(contentRect.Size());
+  RefPtr<gfx::DrawTarget> drawTarget =
+    gfx::Factory::CreateDrawTarget(gfx::BackendType::SKIA,
+                                   imageIntSize.ToUnknownSize(),
+                                   gfx::SurfaceFormat::A8);
+  RefPtr<gfxContext> context = gfxContext::CreateOrNull(drawTarget);
+
+  ImgDrawResult result =
+    imageRenderer.DrawShapeImage(aFrame->PresContext(), *context);
+
+  if (result != ImgDrawResult::SUCCESS) {
+    return nullptr;
+  }
+
+  // Retrieve the pixel image buffer to create the image shape info.
+  RefPtr<SourceSurface> sourceSurface = drawTarget->Snapshot();
+  RefPtr<DataSourceSurface> dataSourceSurface = sourceSurface->GetDataSurface();
+  DataSourceSurface::ScopedMap map(dataSourceSurface, DataSourceSurface::READ);
+
+  if (!map.IsMapped()) {
+    return nullptr;
+  }
+
+  MOZ_ASSERT(sourceSurface->GetSize() == imageIntSize.ToUnknownSize(),
+             "Who changes the size?");
+
+  uint8_t* alphaPixels = map.GetData();
+  return MakeUnique<ImageShapeInfo>(alphaPixels, imageIntSize,
+                                    aShapeImageThreshold, contentRect, aWM,
+                                    aContainerSize);
+}
+
 /* static */ nscoord
 nsFloatManager::ShapeInfo::ComputeEllipseLineInterceptDiff(
   const nscoord aShapeBoxBStart, const nscoord aShapeBoxBEnd,
   const nscoord aBStartCornerRadiusL, const nscoord aBStartCornerRadiusB,
   const nscoord aBEndCornerRadiusL, const nscoord aBEndCornerRadiusB,
   const nscoord aBandBStart, const nscoord aBandBEnd)
 {
   // An example for the band intersecting with the top right corner of an
--- a/layout/generic/nsFloatManager.h
+++ b/layout/generic/nsFloatManager.h
@@ -339,16 +339,17 @@ public:
 #endif
 
 private:
 
   class ShapeInfo;
   class RoundedBoxShapeInfo;
   class EllipseShapeInfo;
   class PolygonShapeInfo;
+  class ImageShapeInfo;
 
   struct FloatInfo {
     nsIFrame *const mFrame;
     // The lowest block-ends of left/right floats up to and including
     // this one.
     nscoord mLeftBEnd, mRightBEnd;
 
     FloatInfo(nsIFrame* aFrame, nscoord aLineLeft, nscoord aBlockStart,
--- a/layout/painting/nsImageRenderer.cpp
+++ b/layout/painting/nsImageRenderer.cpp
@@ -1,28 +1,31 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* utility functions for drawing borders and backgrounds */
+/* This file contains utility code for drawing images as CSS borders,
+ * backgrounds, and shapes. */
 
 #include "nsImageRenderer.h"
 
 #include "mozilla/webrender/WebRenderAPI.h"
 
 #include "gfxContext.h"
 #include "gfxDrawable.h"
 #include "ImageOps.h"
+#include "ImageRegion.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 #include "nsContentUtils.h"
 #include "nsCSSRendering.h"
 #include "nsCSSRenderingGradients.h"
+#include "nsDeviceContext.h"
 #include "nsIFrame.h"
 #include "nsStyleStructInlines.h"
 #include "nsSVGDisplayableFrame.h"
 #include "SVGObserverUtils.h"
 #include "nsSVGIntegrationUtils.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 
 using namespace mozilla;
@@ -942,16 +945,74 @@ nsImageRenderer::DrawBorderImageComponen
   nsRect fillRect(aFill);
   nsRect destTile = RequiresScaling(fillRect, aHFill, aVFill, aUnitSize)
                   ? ComputeTile(fillRect, aHFill, aVFill, aUnitSize, repeatSize)
                   : fillRect;
   return Draw(aPresContext, aRenderingContext, aDirtyRect, destTile,
               fillRect, destTile.TopLeft(), repeatSize, aSrc);
 }
 
+ImgDrawResult
+nsImageRenderer::DrawShapeImage(nsPresContext* aPresContext,
+                                gfxContext& aRenderingContext)
+{
+  if (!IsReady()) {
+    NS_NOTREACHED("Ensure PrepareImage() has returned true before calling me");
+    return ImgDrawResult::NOT_READY;
+  }
+
+  if (mSize.width <= 0 || mSize.height <= 0) {
+    return ImgDrawResult::SUCCESS;
+  }
+
+  ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+  switch (GetType()) {
+    case eStyleImageType_Image: {
+      nsIntSize imageIntSize = CSSIntSize::FromAppUnitsRounded(mSize).ToUnknownSize();
+      result = mImageContainer->Draw(&aRenderingContext, imageIntSize,
+                                     ImageRegion::Create(imageIntSize),
+                                     imgIContainer::FRAME_FIRST,
+                                     SamplingFilter::GOOD, Nothing(), 0, 1.0);
+      break;
+    }
+
+    case eStyleImageType_Gradient: {
+      // Set DPI scale and full zoom to 1.0 so that nsCSSGradientRenderer
+      // won't take device pixels into consideration.
+      nsDeviceContext* dc = aPresContext->DeviceContext();
+      double targetDPI = 1.0;
+      float oldFullZoom = dc->GetFullZoom();
+
+      dc->CheckDPIChange(&targetDPI);
+      dc->SetFullZoom(1.0f);
+
+      nsCSSGradientRenderer renderer =
+        nsCSSGradientRenderer::Create(aPresContext, mGradientData, mSize);
+      nsRect dest(nsPoint(0, 0), mSize);
+
+      renderer.Paint(aRenderingContext, dest, dest, mSize,
+                     CSSIntRect::FromAppUnitsRounded(dest),
+                     dest, 1.0);
+
+      // Restore DPI and full zoom.
+      dc->CheckDPIChange();
+      dc->SetFullZoom(oldFullZoom);
+      break;
+    }
+
+    default:
+      // Unsupported image type.
+      result = ImgDrawResult::BAD_IMAGE;
+      break;
+  }
+
+  return result;
+}
+
 bool
 nsImageRenderer::IsRasterImage()
 {
   if (mType != eStyleImageType_Image || !mImageContainer)
     return false;
   return mImageContainer->GetType() == imgIContainer::TYPE_RASTER;
 }
 
--- a/layout/painting/nsImageRenderer.h
+++ b/layout/painting/nsImageRenderer.h
@@ -249,16 +249,24 @@ public:
                            const mozilla::CSSIntRect& aSrc,
                            mozilla::StyleBorderImageRepeat aHFill,
                            mozilla::StyleBorderImageRepeat aVFill,
                            const nsSize&        aUnitSize,
                            uint8_t              aIndex,
                            const mozilla::Maybe<nsSize>& aSVGViewportSize,
                            const bool           aHasIntrinsicRatio);
 
+  /**
+   * Draw the image to aRenderingContext which can be used to define the
+   * float area in the presence of "shape-outside: <image>".
+   */
+  ImgDrawResult
+  DrawShapeImage(nsPresContext* aPresContext,
+                 gfxContext& aRenderingContext);
+
   bool IsRasterImage();
   bool IsAnimatedImage();
 
   /// Retrieves the image associated with this nsImageRenderer, if there is one.
   already_AddRefed<imgIContainer> GetImage();
 
   bool IsImageContainerAvailable(layers::LayerManager* aManager, uint32_t aFlags);
   bool IsReady() const { return mPrepareResult == ImgDrawResult::SUCCESS; }