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