tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ClippedImage.cpp (19545B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "ClippedImage.h"
      7 
      8 #include <algorithm>
      9 #include <cmath>
     10 #include <new>  // Workaround for bug in VS10; see bug 981264.
     11 #include <utility>
     12 
     13 #include "ImageRegion.h"
     14 #include "Orientation.h"
     15 #include "gfxContext.h"
     16 #include "gfxDrawable.h"
     17 #include "gfxPlatform.h"
     18 #include "gfxUtils.h"
     19 #include "mozilla/RefPtr.h"
     20 #include "mozilla/SVGImageContext.h"
     21 #include "nsPresContext.h"
     22 
     23 #include "mozilla/gfx/2D.h"
     24 
     25 namespace mozilla {
     26 
     27 using namespace gfx;
     28 using std::max;
     29 
     30 namespace image {
     31 
     32 class ClippedImageCachedSurface {
     33 public:
     34  ClippedImageCachedSurface(already_AddRefed<SourceSurface> aSurface,
     35                            const nsIntSize& aSize,
     36                            const SVGImageContext& aSVGContext, float aFrame,
     37                            uint32_t aFlags, ImgDrawResult aDrawResult)
     38      : mSurface(aSurface),
     39        mSize(aSize),
     40        mSVGContext(aSVGContext),
     41        mFrame(aFrame),
     42        mFlags(aFlags),
     43        mDrawResult(aDrawResult) {
     44    MOZ_ASSERT(mSurface, "Must have a valid surface");
     45  }
     46 
     47  bool Matches(const nsIntSize& aSize, const SVGImageContext& aSVGContext,
     48               float aFrame, uint32_t aFlags) const {
     49    return mSize == aSize && mSVGContext == aSVGContext && mFrame == aFrame &&
     50           mFlags == aFlags;
     51  }
     52 
     53  already_AddRefed<SourceSurface> Surface() const {
     54    RefPtr<SourceSurface> surf(mSurface);
     55    return surf.forget();
     56  }
     57 
     58  ImgDrawResult GetDrawResult() const { return mDrawResult; }
     59 
     60  bool NeedsRedraw() const {
     61    return mDrawResult != ImgDrawResult::SUCCESS &&
     62           mDrawResult != ImgDrawResult::BAD_IMAGE;
     63  }
     64 
     65 private:
     66  RefPtr<SourceSurface> mSurface;
     67  const nsIntSize mSize;
     68  SVGImageContext mSVGContext;
     69  const float mFrame;
     70  const uint32_t mFlags;
     71  const ImgDrawResult mDrawResult;
     72 };
     73 
     74 class DrawSingleTileCallback : public gfxDrawingCallback {
     75 public:
     76  DrawSingleTileCallback(ClippedImage* aImage, const nsIntSize& aSize,
     77                         const SVGImageContext& aSVGContext,
     78                         uint32_t aWhichFrame, uint32_t aFlags, float aOpacity)
     79      : mImage(aImage),
     80        mSize(aSize),
     81        mSVGContext(aSVGContext),
     82        mWhichFrame(aWhichFrame),
     83        mFlags(aFlags),
     84        mDrawResult(ImgDrawResult::NOT_READY),
     85        mOpacity(aOpacity) {
     86    MOZ_ASSERT(mImage, "Must have an image to clip");
     87  }
     88 
     89  virtual bool operator()(gfxContext* aContext, const gfxRect& aFillRect,
     90                          const SamplingFilter aSamplingFilter,
     91                          const gfxMatrix& aTransform) override {
     92    MOZ_ASSERT(aTransform.IsIdentity(),
     93               "Caller is probably CreateSamplingRestrictedDrawable, "
     94               "which should not happen");
     95 
     96    // Draw the image. |gfxCallbackDrawable| always calls this function with
     97    // arguments that guarantee we never tile.
     98    mDrawResult = mImage->DrawSingleTile(
     99        aContext, mSize, ImageRegion::Create(aFillRect), mWhichFrame,
    100        aSamplingFilter, mSVGContext, mFlags, mOpacity);
    101 
    102    return true;
    103  }
    104 
    105  ImgDrawResult GetDrawResult() { return mDrawResult; }
    106 
    107 private:
    108  RefPtr<ClippedImage> mImage;
    109  const nsIntSize mSize;
    110  const SVGImageContext& mSVGContext;
    111  const uint32_t mWhichFrame;
    112  const uint32_t mFlags;
    113  ImgDrawResult mDrawResult;
    114  float mOpacity;
    115 };
    116 
    117 ClippedImage::ClippedImage(Image* aImage, nsIntRect aClip,
    118                           const Maybe<nsSize>& aSVGViewportSize)
    119    : ImageWrapper(aImage), mClip(aClip) {
    120  MOZ_ASSERT(aImage != nullptr, "ClippedImage requires an existing Image");
    121  MOZ_ASSERT_IF(aSVGViewportSize,
    122                aImage->GetType() == imgIContainer::TYPE_VECTOR);
    123  if (aSVGViewportSize) {
    124    mSVGViewportSize =
    125        Some(aSVGViewportSize->ToNearestPixels(AppUnitsPerCSSPixel()));
    126  }
    127 }
    128 
    129 ClippedImage::~ClippedImage() {}
    130 
    131 bool ClippedImage::ShouldClip() {
    132  // We need to evaluate the clipping region against the image's width and
    133  // height once they're available to determine if it's valid and whether we
    134  // actually need to do any work. We may fail if the image's width and height
    135  // aren't available yet, in which case we'll try again later.
    136  if (mShouldClip.isNothing()) {
    137    int32_t width, height;
    138    RefPtr<ProgressTracker> progressTracker =
    139        InnerImage()->GetProgressTracker();
    140    if (InnerImage()->HasError()) {
    141      // If there's a problem with the inner image we'll let it handle
    142      // everything.
    143      mShouldClip.emplace(false);
    144    } else if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) {
    145      // Clamp the clipping region to the size of the SVG viewport.
    146      nsIntRect svgViewportRect(nsIntPoint(0, 0), *mSVGViewportSize);
    147 
    148      mClip = mClip.Intersect(svgViewportRect);
    149 
    150      // If the clipping region is the same size as the SVG viewport size
    151      // we don't have to do anything.
    152      mShouldClip.emplace(!mClip.IsEqualInterior(svgViewportRect));
    153    } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&width)) && width > 0 &&
    154               NS_SUCCEEDED(InnerImage()->GetHeight(&height)) && height > 0) {
    155      // Clamp the clipping region to the size of the underlying image.
    156      mClip = mClip.Intersect(nsIntRect(0, 0, width, height));
    157 
    158      // If the clipping region is the same size as the underlying image we
    159      // don't have to do anything.
    160      mShouldClip.emplace(
    161          !mClip.IsEqualInterior(nsIntRect(0, 0, width, height)));
    162    } else if (progressTracker &&
    163               !(progressTracker->GetProgress() & FLAG_LOAD_COMPLETE)) {
    164      // The image just hasn't finished loading yet. We don't yet know whether
    165      // clipping with be needed or not for now. Just return without memorizing
    166      // anything.
    167      return false;
    168    } else {
    169      // We have a fully loaded image without a clearly defined width and
    170      // height. This can happen with SVG images.
    171      mShouldClip.emplace(false);
    172    }
    173  }
    174 
    175  MOZ_ASSERT(mShouldClip.isSome(), "Should have computed a result");
    176  return *mShouldClip;
    177 }
    178 
    179 NS_IMETHODIMP
    180 ClippedImage::GetWidth(int32_t* aWidth) {
    181  if (!ShouldClip()) {
    182    return InnerImage()->GetWidth(aWidth);
    183  }
    184 
    185  *aWidth = mClip.Width();
    186  return NS_OK;
    187 }
    188 
    189 NS_IMETHODIMP
    190 ClippedImage::GetHeight(int32_t* aHeight) {
    191  if (!ShouldClip()) {
    192    return InnerImage()->GetHeight(aHeight);
    193  }
    194 
    195  *aHeight = mClip.Height();
    196  return NS_OK;
    197 }
    198 
    199 NS_IMETHODIMP
    200 ClippedImage::GetIntrinsicSize(ImageIntrinsicSize* aIntrinsicSize) {
    201  if (!ShouldClip()) {
    202    return InnerImage()->GetIntrinsicSize(aIntrinsicSize);
    203  }
    204 
    205  aIntrinsicSize->mWidth = Some(mClip.Width());
    206  aIntrinsicSize->mHeight = Some(mClip.Height());
    207  return NS_OK;
    208 }
    209 
    210 NS_IMETHODIMP
    211 ClippedImage::GetIntrinsicSizeInAppUnits(nsSize* aSize) {
    212  if (!ShouldClip()) {
    213    return InnerImage()->GetIntrinsicSizeInAppUnits(aSize);
    214  }
    215 
    216  *aSize = nsSize(nsPresContext::CSSPixelsToAppUnits(mClip.Width()),
    217                  nsPresContext::CSSPixelsToAppUnits(mClip.Height()));
    218  return NS_OK;
    219 }
    220 
    221 AspectRatio ClippedImage::GetIntrinsicRatio() {
    222  if (!ShouldClip()) {
    223    return InnerImage()->GetIntrinsicRatio();
    224  }
    225  return AspectRatio::FromSize(mClip.Width(), mClip.Height());
    226 }
    227 
    228 NS_IMETHODIMP_(already_AddRefed<SourceSurface>)
    229 ClippedImage::GetFrame(uint32_t aWhichFrame, uint32_t aFlags) {
    230  RefPtr<SourceSurface> surface;
    231  std::tie(std::ignore, surface) = GetFrameInternal(
    232      mClip.Size(), SVGImageContext(), Nothing(), aWhichFrame, aFlags, 1.0);
    233  return surface.forget();
    234 }
    235 
    236 NS_IMETHODIMP_(already_AddRefed<SourceSurface>)
    237 ClippedImage::GetFrameAtSize(const IntSize& aSize, uint32_t aWhichFrame,
    238                             uint32_t aFlags) {
    239  // XXX(seth): It'd be nice to support downscale-during-decode for this case,
    240  // but right now we just fall back to the intrinsic size.
    241  return GetFrame(aWhichFrame, aFlags);
    242 }
    243 
    244 std::pair<ImgDrawResult, RefPtr<SourceSurface>> ClippedImage::GetFrameInternal(
    245    const nsIntSize& aSize, const SVGImageContext& aSVGContext,
    246    const Maybe<ImageIntRegion>& aRegion, uint32_t aWhichFrame, uint32_t aFlags,
    247    float aOpacity) {
    248  if (!ShouldClip()) {
    249    RefPtr<SourceSurface> surface = InnerImage()->GetFrame(aWhichFrame, aFlags);
    250    return std::make_pair(
    251        surface ? ImgDrawResult::SUCCESS : ImgDrawResult::NOT_READY,
    252        std::move(surface));
    253  }
    254 
    255  float frameToDraw = InnerImage()->GetFrameIndex(aWhichFrame);
    256  if (!mCachedSurface ||
    257      !mCachedSurface->Matches(aSize, aSVGContext, frameToDraw, aFlags) ||
    258      mCachedSurface->NeedsRedraw()) {
    259    // Create a surface to draw into.
    260    RefPtr<DrawTarget> target =
    261        gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
    262            IntSize(aSize.width, aSize.height), SurfaceFormat::OS_RGBA);
    263    if (!target || !target->IsValid()) {
    264      NS_ERROR("Could not create a DrawTarget");
    265      return std::make_pair(ImgDrawResult::TEMPORARY_ERROR,
    266                            RefPtr<SourceSurface>());
    267    }
    268 
    269    gfxContext ctx(target);
    270 
    271    // Create our callback.
    272    RefPtr<DrawSingleTileCallback> drawTileCallback =
    273        new DrawSingleTileCallback(this, aSize, aSVGContext, aWhichFrame,
    274                                   aFlags, aOpacity);
    275    RefPtr<gfxDrawable> drawable =
    276        new gfxCallbackDrawable(drawTileCallback, aSize);
    277 
    278    // Actually draw. The callback will end up invoking DrawSingleTile.
    279    gfxUtils::DrawPixelSnapped(&ctx, drawable, SizeDouble(aSize),
    280                               ImageRegion::Create(aSize),
    281                               SurfaceFormat::OS_RGBA, SamplingFilter::LINEAR,
    282                               imgIContainer::FLAG_CLAMP);
    283 
    284    // Cache the resulting surface.
    285    mCachedSurface = MakeUnique<ClippedImageCachedSurface>(
    286        target->Snapshot(), aSize, aSVGContext, frameToDraw, aFlags,
    287        drawTileCallback->GetDrawResult());
    288  }
    289 
    290  MOZ_ASSERT(mCachedSurface, "Should have a cached surface now");
    291  RefPtr<SourceSurface> surface = mCachedSurface->Surface();
    292  return std::make_pair(mCachedSurface->GetDrawResult(), std::move(surface));
    293 }
    294 
    295 NS_IMETHODIMP_(bool)
    296 ClippedImage::IsImageContainerAvailable(WindowRenderer* aRenderer,
    297                                        uint32_t aFlags) {
    298  if (!ShouldClip()) {
    299    return InnerImage()->IsImageContainerAvailable(aRenderer, aFlags);
    300  }
    301  return false;
    302 }
    303 
    304 NS_IMETHODIMP_(ImgDrawResult)
    305 ClippedImage::GetImageProvider(WindowRenderer* aRenderer,
    306                               const gfx::IntSize& aSize,
    307                               const SVGImageContext& aSVGContext,
    308                               const Maybe<ImageIntRegion>& aRegion,
    309                               uint32_t aFlags,
    310                               WebRenderImageProvider** aProvider) {
    311  // XXX(seth): We currently don't have a way of clipping the result of
    312  // GetImageContainer. We work around this by always returning null, but if it
    313  // ever turns out that ClippedImage is widely used on codepaths that can
    314  // actually benefit from GetImageContainer, it would be a good idea to fix
    315  // that method for performance reasons.
    316 
    317  if (!ShouldClip()) {
    318    return InnerImage()->GetImageProvider(aRenderer, aSize, aSVGContext,
    319                                          aRegion, aFlags, aProvider);
    320  }
    321 
    322  return ImgDrawResult::NOT_SUPPORTED;
    323 }
    324 
    325 static bool MustCreateSurface(gfxContext* aContext, const nsIntSize& aSize,
    326                              const ImageRegion& aRegion,
    327                              const uint32_t aFlags) {
    328  gfxRect imageRect(0, 0, aSize.width, aSize.height);
    329  bool willTile = !imageRect.Contains(aRegion.Rect()) &&
    330                  !(aFlags & imgIContainer::FLAG_CLAMP);
    331  bool willResample = aContext->CurrentMatrix().HasNonIntegerTranslation() &&
    332                      (willTile || !aRegion.RestrictionContains(imageRect));
    333  return willTile || willResample;
    334 }
    335 
    336 NS_IMETHODIMP_(ImgDrawResult)
    337 ClippedImage::Draw(gfxContext* aContext, const nsIntSize& aSize,
    338                   const ImageRegion& aRegion, uint32_t aWhichFrame,
    339                   SamplingFilter aSamplingFilter,
    340                   const SVGImageContext& aSVGContext, uint32_t aFlags,
    341                   float aOpacity) {
    342  if (!ShouldClip()) {
    343    return InnerImage()->Draw(aContext, aSize, aRegion, aWhichFrame,
    344                              aSamplingFilter, aSVGContext, aFlags, aOpacity);
    345  }
    346 
    347  // Check for tiling. If we need to tile then we need to create a
    348  // gfxCallbackDrawable to handle drawing for us.
    349  if (MustCreateSurface(aContext, aSize, aRegion, aFlags)) {
    350    // Create a temporary surface containing a single tile of this image.
    351    // GetFrame will call DrawSingleTile internally.
    352    auto [result, surface] = GetFrameInternal(aSize, aSVGContext, Nothing(),
    353                                              aWhichFrame, aFlags, aOpacity);
    354    if (!surface) {
    355      MOZ_ASSERT(result != ImgDrawResult::SUCCESS);
    356      return result;
    357    }
    358 
    359    // Create a drawable from that surface.
    360    RefPtr<gfxSurfaceDrawable> drawable =
    361        new gfxSurfaceDrawable(surface, aSize);
    362 
    363    // Draw.
    364    gfxUtils::DrawPixelSnapped(aContext, drawable, SizeDouble(aSize), aRegion,
    365                               SurfaceFormat::OS_RGBA, aSamplingFilter,
    366                               aOpacity);
    367 
    368    return result;
    369  }
    370 
    371  return DrawSingleTile(aContext, aSize, aRegion, aWhichFrame, aSamplingFilter,
    372                        aSVGContext, aFlags, aOpacity);
    373 }
    374 
    375 ImgDrawResult ClippedImage::DrawSingleTile(
    376    gfxContext* aContext, const nsIntSize& aSize, const ImageRegion& aRegion,
    377    uint32_t aWhichFrame, SamplingFilter aSamplingFilter,
    378    const SVGImageContext& aSVGContext, uint32_t aFlags, float aOpacity) {
    379  MOZ_ASSERT(!MustCreateSurface(aContext, aSize, aRegion, aFlags),
    380             "Shouldn't need to create a surface");
    381 
    382  gfxRect clip(mClip.X(), mClip.Y(), mClip.Width(), mClip.Height());
    383  nsIntSize size(aSize), innerSize(aSize);
    384  bool needScale = false;
    385  if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) {
    386    innerSize = *mSVGViewportSize;
    387    needScale = true;
    388  } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&innerSize.width)) &&
    389             NS_SUCCEEDED(InnerImage()->GetHeight(&innerSize.height))) {
    390    needScale = true;
    391  } else {
    392    MOZ_ASSERT_UNREACHABLE(
    393        "If ShouldClip() led us to draw then we should never get here");
    394  }
    395 
    396  if (needScale) {
    397    double scaleX = aSize.width / clip.Width();
    398    double scaleY = aSize.height / clip.Height();
    399 
    400    // Map the clip and size to the scale requested by the caller.
    401    clip.Scale(scaleX, scaleY);
    402    size = innerSize;
    403    size.Scale(scaleX, scaleY);
    404  }
    405 
    406  // We restrict our drawing to only the clipping region, and translate so that
    407  // the clipping region is placed at the position the caller expects.
    408  ImageRegion region(aRegion);
    409  region.MoveBy(clip.X(), clip.Y());
    410  region = region.Intersect(clip);
    411 
    412  gfxContextMatrixAutoSaveRestore saveMatrix(aContext);
    413  aContext->Multiply(gfxMatrix::Translation(-clip.X(), -clip.Y()));
    414 
    415  auto unclipViewport = [&](const SVGImageContext& aOldContext) {
    416    // Map the viewport to the inner image. Note that we don't take the aSize
    417    // parameter of imgIContainer::Draw into account, just the clipping region.
    418    // The size in pixels at which the output will ultimately be drawn is
    419    // irrelevant here since the purpose of the SVG viewport size is to
    420    // determine what *region* of the SVG document will be drawn.
    421    SVGImageContext context(aOldContext);
    422    auto oldViewport = aOldContext.GetViewportSize();
    423    if (oldViewport) {
    424      CSSIntSize newViewport;
    425      newViewport.width =
    426          ceil(oldViewport->width * double(innerSize.width) / mClip.Width());
    427      newViewport.height =
    428          ceil(oldViewport->height * double(innerSize.height) / mClip.Height());
    429      context.SetViewportSize(Some(newViewport));
    430    }
    431    return context;
    432  };
    433 
    434  return InnerImage()->Draw(aContext, size, region, aWhichFrame,
    435                            aSamplingFilter, unclipViewport(aSVGContext),
    436                            aFlags, aOpacity);
    437 }
    438 
    439 NS_IMETHODIMP
    440 ClippedImage::RequestDiscard() {
    441  // We're very aggressive about discarding.
    442  mCachedSurface = nullptr;
    443 
    444  return InnerImage()->RequestDiscard();
    445 }
    446 
    447 NS_IMETHODIMP_(Orientation)
    448 ClippedImage::GetOrientation() {
    449  // XXX(seth): This should not actually be here; this is just to work around a
    450  // what appears to be a bug in MSVC's linker.
    451  return InnerImage()->GetOrientation();
    452 }
    453 
    454 nsIntSize ClippedImage::OptimalImageSizeForDest(const gfxSize& aDest,
    455                                                uint32_t aWhichFrame,
    456                                                SamplingFilter aSamplingFilter,
    457                                                uint32_t aFlags) {
    458  if (!ShouldClip()) {
    459    return InnerImage()->OptimalImageSizeForDest(aDest, aWhichFrame,
    460                                                 aSamplingFilter, aFlags);
    461  }
    462 
    463  int32_t imgWidth, imgHeight;
    464  bool needScale = false;
    465  bool forceUniformScaling = false;
    466  if (mSVGViewportSize && !mSVGViewportSize->IsEmpty()) {
    467    imgWidth = mSVGViewportSize->width;
    468    imgHeight = mSVGViewportSize->height;
    469    needScale = true;
    470    forceUniformScaling = (aFlags & imgIContainer::FLAG_FORCE_UNIFORM_SCALING);
    471  } else if (NS_SUCCEEDED(InnerImage()->GetWidth(&imgWidth)) &&
    472             NS_SUCCEEDED(InnerImage()->GetHeight(&imgHeight))) {
    473    needScale = true;
    474  }
    475 
    476  if (needScale) {
    477    // To avoid ugly sampling artifacts, ClippedImage needs the image size to
    478    // be chosen such that the clipping region lies on pixel boundaries.
    479 
    480    // First, we select a scale that's good for ClippedImage. An integer
    481    // multiple of the size of the clipping region is always fine.
    482    IntSize scale = IntSize::Ceil(aDest.width / mClip.Width(),
    483                                  aDest.height / mClip.Height());
    484 
    485    if (forceUniformScaling) {
    486      scale.width = scale.height = max(scale.height, scale.width);
    487    }
    488 
    489    // Determine the size we'd prefer to render the inner image at, and ask the
    490    // inner image what size we should actually use.
    491    gfxSize desiredSize(double(imgWidth) * scale.width,
    492                        double(imgHeight) * scale.height);
    493    nsIntSize innerDesiredSize = InnerImage()->OptimalImageSizeForDest(
    494        desiredSize, aWhichFrame, aSamplingFilter, aFlags);
    495 
    496    // To get our final result, we take the inner image's desired size and
    497    // determine how large the clipped region would be at that scale. (Again, we
    498    // ensure an integer multiple of the size of the clipping region.)
    499    IntSize finalScale =
    500        IntSize::Ceil(double(innerDesiredSize.width) / imgWidth,
    501                      double(innerDesiredSize.height) / imgHeight);
    502    return mClip.Size() * finalScale;
    503  }
    504 
    505  MOZ_ASSERT(false,
    506             "If ShouldClip() led us to draw then we should never get here");
    507  return InnerImage()->OptimalImageSizeForDest(aDest, aWhichFrame,
    508                                               aSamplingFilter, aFlags);
    509 }
    510 
    511 NS_IMETHODIMP_(nsIntRect)
    512 ClippedImage::GetImageSpaceInvalidationRect(const nsIntRect& aRect) {
    513  if (!ShouldClip()) {
    514    return InnerImage()->GetImageSpaceInvalidationRect(aRect);
    515  }
    516 
    517  nsIntRect rect(InnerImage()->GetImageSpaceInvalidationRect(aRect));
    518  rect = rect.Intersect(mClip);
    519  rect.MoveBy(-mClip.X(), -mClip.Y());
    520  return rect;
    521 }
    522 
    523 }  // namespace image
    524 }  // namespace mozilla