diff options
Diffstat (limited to 'layout/painting/nsImageRenderer.cpp')
-rw-r--r-- | layout/painting/nsImageRenderer.cpp | 1054 |
1 files changed, 1054 insertions, 0 deletions
diff --git a/layout/painting/nsImageRenderer.cpp b/layout/painting/nsImageRenderer.cpp new file mode 100644 index 0000000000..d2fa024d4d --- /dev/null +++ b/layout/painting/nsImageRenderer.cpp @@ -0,0 +1,1054 @@ +/* -*- 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 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/image/WebRenderImageProvider.h" +#include "mozilla/layers/RenderRootStateManager.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 "nsLayoutUtils.h" +#include "nsStyleStructInlines.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/SVGIntegrationUtils.h" +#include "mozilla/SVGPaintServerFrame.h" +#include "mozilla/SVGObserverUtils.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::layers; + +nsSize CSSSizeOrRatio::ComputeConcreteSize() const { + NS_ASSERTION(CanComputeConcreteSize(), "Cannot compute"); + if (mHasWidth && mHasHeight) { + return nsSize(mWidth, mHeight); + } + if (mHasWidth) { + return nsSize(mWidth, mRatio.Inverted().ApplyTo(mWidth)); + } + + MOZ_ASSERT(mHasHeight); + return nsSize(mRatio.ApplyTo(mHeight), mHeight); +} + +nsImageRenderer::nsImageRenderer(nsIFrame* aForFrame, const StyleImage* aImage, + uint32_t aFlags) + : mForFrame(aForFrame), + mImage(&aImage->FinalImage()), + mImageResolution(aImage->GetResolution()), + mType(mImage->tag), + mImageContainer(nullptr), + mGradientData(nullptr), + mPaintServerFrame(nullptr), + mPrepareResult(ImgDrawResult::NOT_READY), + mSize(0, 0), + mFlags(aFlags), + mExtendMode(ExtendMode::CLAMP), + mMaskOp(StyleMaskMode::MatchSource) {} + +bool nsImageRenderer::PrepareImage() { + if (mImage->IsNone()) { + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } + + const bool isImageRequest = mImage->IsImageRequestType(); + MOZ_ASSERT_IF(!isImageRequest, !mImage->GetImageRequest()); + imgRequestProxy* request = nullptr; + if (isImageRequest) { + request = mImage->GetImageRequest(); + if (!request) { + // request could be null here if the StyleImage refused + // to load a same-document URL, or the url was invalid, for example. + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } + } + + if (!mImage->IsComplete()) { + MOZ_DIAGNOSTIC_ASSERT(isImageRequest); + + // Make sure the image is actually decoding. + bool frameComplete = request->StartDecodingWithResult( + imgIContainer::FLAG_ASYNC_NOTIFY | + imgIContainer::FLAG_AVOID_REDECODE_FOR_SIZE); + + // Boost the loading priority since we know we want to draw the image. + if (mFlags & nsImageRenderer::FLAG_PAINTING_TO_WINDOW) { + request->BoostPriority(imgIRequest::CATEGORY_DISPLAY); + } + + // Check again to see if we finished. + // We cannot prepare the image for rendering if it is not fully loaded. + if (!frameComplete && !mImage->IsComplete()) { + uint32_t imageStatus = 0; + request->GetImageStatus(&imageStatus); + if (imageStatus & imgIRequest::STATUS_ERROR) { + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } + + // If not errored, and we requested a sync decode, and the image has + // loaded, push on through because the Draw() will do a sync decode then. + const bool syncDecodeWillComplete = + (mFlags & FLAG_SYNC_DECODE_IMAGES) && + (imageStatus & imgIRequest::STATUS_LOAD_COMPLETE); + + bool canDrawPartial = + (mFlags & nsImageRenderer::FLAG_DRAW_PARTIAL_FRAMES) && + isImageRequest && mImage->IsSizeAvailable() && !mImage->IsRect(); + + // If we are drawing a partial frame then we want to make sure there are + // some pixels to draw, otherwise we waste effort pushing through to draw + // nothing. + if (!syncDecodeWillComplete && canDrawPartial) { + nsCOMPtr<imgIContainer> image; + canDrawPartial = + canDrawPartial && + NS_SUCCEEDED(request->GetImage(getter_AddRefs(image))) && image && + image->GetType() == imgIContainer::TYPE_RASTER && + image->HasDecodedPixels(); + } + + // If we can draw partial then proceed if we at least have the size + // available. + if (!(syncDecodeWillComplete || canDrawPartial)) { + mPrepareResult = ImgDrawResult::NOT_READY; + return false; + } + } + } + + if (isImageRequest) { + nsCOMPtr<imgIContainer> srcImage; + nsresult rv = request->GetImage(getter_AddRefs(srcImage)); + MOZ_ASSERT(NS_SUCCEEDED(rv) && srcImage, + "If GetImage() is failing, mImage->IsComplete() " + "should have returned false"); + if (!NS_SUCCEEDED(rv)) { + srcImage = nullptr; + } + + if (srcImage) { + StyleImageOrientation orientation = + mForFrame->StyleVisibility()->UsedImageOrientation(request); + srcImage = nsLayoutUtils::OrientImage(srcImage, orientation); + } + + if (!mImage->IsRect()) { + mImageContainer.swap(srcImage); + } else { + auto croprect = mImage->ComputeActualCropRect(); + if (!croprect || croprect->mRect.IsEmpty()) { + // The cropped image has zero size + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } + if (croprect->mIsEntireImage) { + // The cropped image is identical to the source image + mImageContainer.swap(srcImage); + } else { + nsCOMPtr<imgIContainer> subImage = + ImageOps::Clip(srcImage, croprect->mRect, Nothing()); + mImageContainer.swap(subImage); + } + } + mPrepareResult = ImgDrawResult::SUCCESS; + } else if (mImage->IsGradient()) { + mGradientData = &*mImage->AsGradient(); + mPrepareResult = ImgDrawResult::SUCCESS; + } else if (mImage->IsElement()) { + dom::Element* paintElement = // may be null + SVGObserverUtils::GetAndObserveBackgroundImage( + mForFrame->FirstContinuation(), mImage->AsElement().AsAtom()); + // If the referenced element is an <img>, <canvas>, or <video> element, + // prefer SurfaceFromElement as it's more reliable. + mImageElementSurface = nsLayoutUtils::SurfaceFromElement(paintElement); + + if (!mImageElementSurface.GetSourceSurface()) { + nsIFrame* paintServerFrame = + paintElement ? paintElement->GetPrimaryFrame() : nullptr; + // If there's no referenced frame, or the referenced frame is + // non-displayable SVG, then we have nothing valid to paint. + if (!paintServerFrame || + (paintServerFrame->IsFrameOfType(nsIFrame::eSVG) && + !static_cast<SVGPaintServerFrame*>( + do_QueryFrame(paintServerFrame)) && + !static_cast<ISVGDisplayableFrame*>( + do_QueryFrame(paintServerFrame)))) { + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } + mPaintServerFrame = paintServerFrame; + } + + mPrepareResult = ImgDrawResult::SUCCESS; + } else if (mImage->IsCrossFade()) { + // See bug 546052 - cross-fade implementation still being worked + // on. + mPrepareResult = ImgDrawResult::BAD_IMAGE; + return false; + } else { + MOZ_ASSERT(mImage->IsNone(), "Unknown image type?"); + } + + return IsReady(); +} + +CSSSizeOrRatio nsImageRenderer::ComputeIntrinsicSize() { + NS_ASSERTION(IsReady(), + "Ensure PrepareImage() has returned true " + "before calling me"); + + CSSSizeOrRatio result; + switch (mType) { + case StyleImage::Tag::Rect: + case StyleImage::Tag::Url: { + bool haveWidth, haveHeight; + CSSIntSize imageIntSize; + nsLayoutUtils::ComputeSizeForDrawing(mImageContainer, mImageResolution, + imageIntSize, result.mRatio, + haveWidth, haveHeight); + if (haveWidth) { + result.SetWidth(CSSPixel::ToAppUnits(imageIntSize.width)); + } + if (haveHeight) { + result.SetHeight(CSSPixel::ToAppUnits(imageIntSize.height)); + } + + // If we know the aspect ratio and one of the dimensions, + // we can compute the other missing width or height. + if (!haveHeight && haveWidth && result.mRatio) { + nscoord intrinsicHeight = + result.mRatio.Inverted().ApplyTo(imageIntSize.width); + result.SetHeight(nsPresContext::CSSPixelsToAppUnits(intrinsicHeight)); + } else if (haveHeight && !haveWidth && result.mRatio) { + nscoord intrinsicWidth = result.mRatio.ApplyTo(imageIntSize.height); + result.SetWidth(nsPresContext::CSSPixelsToAppUnits(intrinsicWidth)); + } + + break; + } + case StyleImage::Tag::Element: { + // XXX element() should have the width/height of the referenced element, + // and that element's ratio, if it matches. If it doesn't match, it + // should have no width/height or ratio. See element() in CSS images: + // <http://dev.w3.org/csswg/css-images-4/#element-notation>. + // Make sure to change nsStyleImageLayers::Size::DependsOnFrameSize + // when fixing this! + if (mPaintServerFrame) { + // SVG images have no intrinsic size + if (!mPaintServerFrame->IsFrameOfType(nsIFrame::eSVG)) { + // The intrinsic image size for a generic nsIFrame paint server is + // the union of the border-box rects of all of its continuations, + // rounded to device pixels. + int32_t appUnitsPerDevPixel = + mForFrame->PresContext()->AppUnitsPerDevPixel(); + result.SetSize(IntSizeToAppUnits( + SVGIntegrationUtils::GetContinuationUnionSize(mPaintServerFrame) + .ToNearestPixels(appUnitsPerDevPixel), + appUnitsPerDevPixel)); + } + } else { + NS_ASSERTION(mImageElementSurface.GetSourceSurface(), + "Surface should be ready."); + IntSize surfaceSize = mImageElementSurface.mSize; + result.SetSize( + nsSize(nsPresContext::CSSPixelsToAppUnits(surfaceSize.width), + nsPresContext::CSSPixelsToAppUnits(surfaceSize.height))); + } + break; + } + case StyleImage::Tag::ImageSet: + MOZ_FALLTHROUGH_ASSERT("image-set should be resolved already"); + // Bug 546052 cross-fade not yet implemented. + case StyleImage::Tag::CrossFade: + // Per <http://dev.w3.org/csswg/css3-images/#gradients>, gradients have no + // intrinsic dimensions. + case StyleImage::Tag::Gradient: + case StyleImage::Tag::None: + break; + } + + return result; +} + +/* static */ +nsSize nsImageRenderer::ComputeConcreteSize( + const CSSSizeOrRatio& aSpecifiedSize, const CSSSizeOrRatio& aIntrinsicSize, + const nsSize& aDefaultSize) { + // The specified size is fully specified, just use that + if (aSpecifiedSize.IsConcrete()) { + return aSpecifiedSize.ComputeConcreteSize(); + } + + MOZ_ASSERT(!aSpecifiedSize.mHasWidth || !aSpecifiedSize.mHasHeight); + + if (!aSpecifiedSize.mHasWidth && !aSpecifiedSize.mHasHeight) { + // no specified size, try using the intrinsic size + if (aIntrinsicSize.CanComputeConcreteSize()) { + return aIntrinsicSize.ComputeConcreteSize(); + } + + if (aIntrinsicSize.mHasWidth) { + return nsSize(aIntrinsicSize.mWidth, aDefaultSize.height); + } + if (aIntrinsicSize.mHasHeight) { + return nsSize(aDefaultSize.width, aIntrinsicSize.mHeight); + } + + // couldn't use the intrinsic size either, revert to using the default size + return ComputeConstrainedSize(aDefaultSize, aIntrinsicSize.mRatio, CONTAIN); + } + + MOZ_ASSERT(aSpecifiedSize.mHasWidth || aSpecifiedSize.mHasHeight); + + // The specified height is partial, try to compute the missing part. + if (aSpecifiedSize.mHasWidth) { + nscoord height; + if (aIntrinsicSize.HasRatio()) { + height = aIntrinsicSize.mRatio.Inverted().ApplyTo(aSpecifiedSize.mWidth); + } else if (aIntrinsicSize.mHasHeight) { + height = aIntrinsicSize.mHeight; + } else { + height = aDefaultSize.height; + } + return nsSize(aSpecifiedSize.mWidth, height); + } + + MOZ_ASSERT(aSpecifiedSize.mHasHeight); + nscoord width; + if (aIntrinsicSize.HasRatio()) { + width = aIntrinsicSize.mRatio.ApplyTo(aSpecifiedSize.mHeight); + } else if (aIntrinsicSize.mHasWidth) { + width = aIntrinsicSize.mWidth; + } else { + width = aDefaultSize.width; + } + return nsSize(width, aSpecifiedSize.mHeight); +} + +/* static */ +nsSize nsImageRenderer::ComputeConstrainedSize( + const nsSize& aConstrainingSize, const AspectRatio& aIntrinsicRatio, + FitType aFitType) { + if (!aIntrinsicRatio) { + return aConstrainingSize; + } + + // Suppose we're doing a "contain" fit. If the image's aspect ratio has a + // "fatter" shape than the constraint area, then we need to use the + // constraint area's full width, and we need to use the aspect ratio to + // produce a height. On the other hand, if the aspect ratio is "skinnier", we + // use the constraint area's full height, and we use the aspect ratio to + // produce a width. (If instead we're doing a "cover" fit, then it can easily + // be seen that we should do precisely the opposite.) + // + // We check if the image's aspect ratio is "fatter" than the constraint area + // by simply applying the aspect ratio to the constraint area's height, to + // produce a "hypothetical width", and we check whether that + // aspect-ratio-provided "hypothetical width" is wider than the constraint + // area's actual width. If it is, then the aspect ratio is fatter than the + // constraint area. + // + // This is equivalent to the more descriptive alternative: + // + // AspectRatio::FromSize(aConstrainingSize) < aIntrinsicRatio + // + // But gracefully handling the case where one of the two dimensions from + // aConstrainingSize is zero. This is easy to prove since: + // + // aConstrainingSize.width / aConstrainingSize.height < aIntrinsicRatio + // + // Is trivially equivalent to: + // + // aIntrinsicRatio.width < aIntrinsicRatio * aConstrainingSize.height + // + // For the cases where height is not zero. + // + // We use float math here to avoid losing precision for very large backgrounds + // since we use saturating nscoord math otherwise. + const float constraintWidth = float(aConstrainingSize.width); + const float hypotheticalWidth = + aIntrinsicRatio.ApplyToFloat(aConstrainingSize.height); + + nsSize size; + if ((aFitType == CONTAIN) == (constraintWidth < hypotheticalWidth)) { + size.width = aConstrainingSize.width; + size.height = aIntrinsicRatio.Inverted().ApplyTo(aConstrainingSize.width); + // If we're reducing the size by less than one css pixel, then just use the + // constraining size. + if (aFitType == CONTAIN && + aConstrainingSize.height - size.height < AppUnitsPerCSSPixel()) { + size.height = aConstrainingSize.height; + } + } else { + size.height = aConstrainingSize.height; + size.width = aIntrinsicRatio.ApplyTo(aConstrainingSize.height); + if (aFitType == CONTAIN && + aConstrainingSize.width - size.width < AppUnitsPerCSSPixel()) { + size.width = aConstrainingSize.width; + } + } + return size; +} + +/** + * mSize is the image's "preferred" size for this particular rendering, while + * the drawn (aka concrete) size is the actual rendered size after accounting + * for background-size etc.. The preferred size is most often the image's + * intrinsic dimensions. But for images with incomplete intrinsic dimensions, + * the preferred size varies, depending on the specified and default sizes, see + * nsImageRenderer::Compute*Size. + * + * This distinction is necessary because the components of a vector image are + * specified with respect to its preferred size for a rendering situation, not + * to its actual rendered size. For example, consider a 4px wide background + * vector image with no height which contains a left-aligned + * 2px wide black rectangle with height 100%. If the background-size width is + * auto (or 4px), the vector image will render 4px wide, and the black rectangle + * will be 2px wide. If the background-size width is 8px, the vector image will + * render 8px wide, and the black rectangle will be 4px wide -- *not* 2px wide. + * In both cases mSize.width will be 4px; but in the first case the returned + * width will be 4px, while in the second case the returned width will be 8px. + */ +void nsImageRenderer::SetPreferredSize(const CSSSizeOrRatio& aIntrinsicSize, + const nsSize& aDefaultSize) { + mSize.width = + aIntrinsicSize.mHasWidth ? aIntrinsicSize.mWidth : aDefaultSize.width; + mSize.height = + aIntrinsicSize.mHasHeight ? aIntrinsicSize.mHeight : aDefaultSize.height; +} + +// Convert from nsImageRenderer flags to the flags we want to use for drawing in +// the imgIContainer namespace. +static uint32_t ConvertImageRendererToDrawFlags(uint32_t aImageRendererFlags) { + uint32_t drawFlags = imgIContainer::FLAG_ASYNC_NOTIFY; + if (aImageRendererFlags & nsImageRenderer::FLAG_SYNC_DECODE_IMAGES) { + drawFlags |= imgIContainer::FLAG_SYNC_DECODE; + } else { + drawFlags |= imgIContainer::FLAG_SYNC_DECODE_IF_FAST; + } + if (aImageRendererFlags & (nsImageRenderer::FLAG_PAINTING_TO_WINDOW | + nsImageRenderer::FLAG_HIGH_QUALITY_SCALING)) { + drawFlags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING; + } + return drawFlags; +} + +ImgDrawResult nsImageRenderer::Draw(nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aDest, const nsRect& aFill, + const nsPoint& aAnchor, + const nsSize& aRepeatSize, + const CSSIntRect& aSrc, float aOpacity) { + if (!IsReady()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + if (aDest.IsEmpty() || aFill.IsEmpty() || mSize.width <= 0 || + mSize.height <= 0) { + return ImgDrawResult::SUCCESS; + } + + SamplingFilter samplingFilter = + nsLayoutUtils::GetSamplingFilterForFrame(mForFrame); + ImgDrawResult result = ImgDrawResult::SUCCESS; + gfxContext* ctx = &aRenderingContext; + Maybe<gfxContext> tempCtx; + IntRect tmpDTRect; + + if (ctx->CurrentOp() != CompositionOp::OP_OVER || + mMaskOp == StyleMaskMode::Luminance) { + gfxRect clipRect = ctx->GetClipExtents(gfxContext::eDeviceSpace); + tmpDTRect = RoundedOut(ToRect(clipRect)); + if (tmpDTRect.IsEmpty()) { + return ImgDrawResult::SUCCESS; + } + RefPtr<DrawTarget> tempDT = ctx->GetDrawTarget()->CreateSimilarDrawTarget( + tmpDTRect.Size(), SurfaceFormat::B8G8R8A8); + if (!tempDT || !tempDT->IsValid()) { + gfxDevCrash(LogReason::InvalidContext) + << "ImageRenderer::Draw problem " << gfx::hexa(tempDT); + return ImgDrawResult::TEMPORARY_ERROR; + } + tempDT->SetTransform(ctx->GetDrawTarget()->GetTransform() * + Matrix::Translation(-tmpDTRect.TopLeft())); + tempCtx.emplace(tempDT, /* aPreserveTransform */ true); + ctx = &tempCtx.ref(); + if (!ctx) { + gfxDevCrash(LogReason::InvalidContext) + << "ImageRenderer::Draw problem " << gfx::hexa(tempDT); + return ImgDrawResult::TEMPORARY_ERROR; + } + } + + switch (mType) { + case StyleImage::Tag::Rect: + case StyleImage::Tag::Url: { + result = nsLayoutUtils::DrawBackgroundImage( + *ctx, mForFrame, aPresContext, mImageContainer, samplingFilter, aDest, + aFill, aRepeatSize, aAnchor, aDirtyRect, + ConvertImageRendererToDrawFlags(mFlags), mExtendMode, aOpacity); + break; + } + case StyleImage::Tag::Gradient: { + nsCSSGradientRenderer renderer = nsCSSGradientRenderer::Create( + aPresContext, mForFrame->Style(), *mGradientData, mSize); + + renderer.Paint(*ctx, aDest, aFill, aRepeatSize, aSrc, aDirtyRect, + aOpacity); + break; + } + case StyleImage::Tag::Element: { + RefPtr<gfxDrawable> drawable = DrawableForElement(aDest, *ctx); + if (!drawable) { + NS_WARNING("Could not create drawable for element"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + nsCOMPtr<imgIContainer> image(ImageOps::CreateFromDrawable(drawable)); + result = nsLayoutUtils::DrawImage( + *ctx, mForFrame->Style(), aPresContext, image, samplingFilter, aDest, + aFill, aAnchor, aDirtyRect, ConvertImageRendererToDrawFlags(mFlags), + aOpacity); + break; + } + case StyleImage::Tag::ImageSet: + MOZ_FALLTHROUGH_ASSERT("image-set should be resolved already"); + // See bug 546052 - cross-fade implementation still being worked + // on. + case StyleImage::Tag::CrossFade: + case StyleImage::Tag::None: + break; + } + + if (!tmpDTRect.IsEmpty()) { + DrawTarget* dt = aRenderingContext.GetDrawTarget(); + Matrix oldTransform = dt->GetTransform(); + dt->SetTransform(Matrix()); + if (mMaskOp == StyleMaskMode::Luminance) { + RefPtr<SourceSurface> surf = ctx->GetDrawTarget()->IntoLuminanceSource( + LuminanceType::LUMINANCE, 1.0f); + dt->MaskSurface(ColorPattern(DeviceColor(0, 0, 0, 1.0f)), surf, + tmpDTRect.TopLeft(), + DrawOptions(1.0f, aRenderingContext.CurrentOp())); + } else { + RefPtr<SourceSurface> surf = ctx->GetDrawTarget()->Snapshot(); + dt->DrawSurface( + surf, + Rect(tmpDTRect.x, tmpDTRect.y, tmpDTRect.width, tmpDTRect.height), + Rect(0, 0, tmpDTRect.width, tmpDTRect.height), + DrawSurfaceOptions(SamplingFilter::POINT), + DrawOptions(1.0f, aRenderingContext.CurrentOp())); + } + + dt->SetTransform(oldTransform); + } + + if (!mImage->IsComplete()) { + result &= ImgDrawResult::SUCCESS_NOT_COMPLETE; + } + + return result; +} + +ImgDrawResult nsImageRenderer::BuildWebRenderDisplayItems( + nsPresContext* aPresContext, mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, + const nsRect& aDirtyRect, const nsRect& aDest, const nsRect& aFill, + const nsPoint& aAnchor, const nsSize& aRepeatSize, const CSSIntRect& aSrc, + float aOpacity) { + if (!IsReady()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return ImgDrawResult::NOT_READY; + } + + if (aDest.IsEmpty() || aFill.IsEmpty() || mSize.width <= 0 || + mSize.height <= 0) { + return ImgDrawResult::SUCCESS; + } + + ImgDrawResult drawResult = ImgDrawResult::SUCCESS; + switch (mType) { + case StyleImage::Tag::Gradient: { + nsCSSGradientRenderer renderer = nsCSSGradientRenderer::Create( + aPresContext, mForFrame->Style(), *mGradientData, mSize); + + renderer.BuildWebRenderDisplayItems(aBuilder, aSc, aDest, aFill, + aRepeatSize, aSrc, + !aItem->BackfaceIsHidden(), aOpacity); + break; + } + case StyleImage::Tag::Rect: + case StyleImage::Tag::Url: { + ExtendMode extendMode = mExtendMode; + if (aDest.Contains(aFill)) { + extendMode = ExtendMode::CLAMP; + } + + uint32_t containerFlags = ConvertImageRendererToDrawFlags(mFlags); + if (extendMode == ExtendMode::CLAMP && + StaticPrefs::image_svg_blob_image() && + mImageContainer->GetType() == imgIContainer::TYPE_VECTOR) { + containerFlags |= imgIContainer::FLAG_RECORD_BLOB; + } + + CSSIntSize destCSSSize{ + nsPresContext::AppUnitsToIntCSSPixels(aDest.width), + nsPresContext::AppUnitsToIntCSSPixels(aDest.height)}; + + SVGImageContext svgContext(Some(destCSSSize)); + Maybe<ImageIntRegion> region; + + const int32_t appUnitsPerDevPixel = + mForFrame->PresContext()->AppUnitsPerDevPixel(); + LayoutDeviceRect destRect = + LayoutDeviceRect::FromAppUnits(aDest, appUnitsPerDevPixel); + LayoutDeviceRect clipRect = + LayoutDeviceRect::FromAppUnits(aFill, appUnitsPerDevPixel); + auto stretchSize = wr::ToLayoutSize(destRect.Size()); + + gfx::IntSize decodeSize = + nsLayoutUtils::ComputeImageContainerDrawingParameters( + mImageContainer, mForFrame, destRect, clipRect, aSc, + containerFlags, svgContext, region); + + RefPtr<image::WebRenderImageProvider> provider; + drawResult = mImageContainer->GetImageProvider( + aManager->LayerManager(), decodeSize, svgContext, region, + containerFlags, getter_AddRefs(provider)); + + Maybe<wr::ImageKey> key = + aManager->CommandBuilder().CreateImageProviderKey( + aItem, provider, drawResult, aResources); + if (key.isNothing()) { + break; + } + + auto rendering = + wr::ToImageRendering(aItem->Frame()->UsedImageRendering()); + wr::LayoutRect clip = wr::ToLayoutRect(clipRect); + + // If we provided a region to the provider, then it already took the + // dest rect into account when it did the recording. + wr::LayoutRect dest = region ? clip : wr::ToLayoutRect(destRect); + + if (extendMode == ExtendMode::CLAMP) { + // The image is not repeating. Just push as a regular image. + aBuilder.PushImage(dest, clip, !aItem->BackfaceIsHidden(), false, + rendering, key.value(), true, + wr::ColorF{1.0f, 1.0f, 1.0f, aOpacity}); + } else { + nsPoint firstTilePos = nsLayoutUtils::GetBackgroundFirstTilePos( + aDest.TopLeft(), aFill.TopLeft(), aRepeatSize); + LayoutDeviceRect fillRect = LayoutDeviceRect::FromAppUnits( + nsRect(firstTilePos.x, firstTilePos.y, + aFill.XMost() - firstTilePos.x, + aFill.YMost() - firstTilePos.y), + appUnitsPerDevPixel); + wr::LayoutRect fill = wr::ToLayoutRect(fillRect); + + switch (extendMode) { + case ExtendMode::REPEAT_Y: + fill.min.x = dest.min.x; + fill.max.x = dest.max.x; + stretchSize.width = dest.width(); + break; + case ExtendMode::REPEAT_X: + fill.min.y = dest.min.y; + fill.max.y = dest.max.y; + stretchSize.height = dest.height(); + break; + default: + break; + } + + LayoutDeviceSize gapSize = LayoutDeviceSize::FromAppUnits( + aRepeatSize - aDest.Size(), appUnitsPerDevPixel); + + aBuilder.PushRepeatingImage(fill, clip, !aItem->BackfaceIsHidden(), + stretchSize, wr::ToLayoutSize(gapSize), + rendering, key.value(), true, + wr::ColorF{1.0f, 1.0f, 1.0f, aOpacity}); + } + break; + } + default: + break; + } + + if (!mImage->IsComplete() && drawResult == ImgDrawResult::SUCCESS) { + return ImgDrawResult::SUCCESS_NOT_COMPLETE; + } + return drawResult; +} + +already_AddRefed<gfxDrawable> nsImageRenderer::DrawableForElement( + const nsRect& aImageRect, gfxContext& aContext) { + NS_ASSERTION(mType == StyleImage::Tag::Element, + "DrawableForElement only makes sense if backed by an element"); + if (mPaintServerFrame) { + // XXX(seth): In order to not pass FLAG_SYNC_DECODE_IMAGES here, + // DrawableFromPaintServer would have to return a ImgDrawResult indicating + // whether any images could not be painted because they weren't fully + // decoded. Even always passing FLAG_SYNC_DECODE_IMAGES won't eliminate all + // problems, as it won't help if there are image which haven't finished + // loading, but it's better than nothing. + int32_t appUnitsPerDevPixel = + mForFrame->PresContext()->AppUnitsPerDevPixel(); + nsRect destRect = aImageRect - aImageRect.TopLeft(); + nsIntSize roundedOut = destRect.ToOutsidePixels(appUnitsPerDevPixel).Size(); + IntSize imageSize(roundedOut.width, roundedOut.height); + + RefPtr<gfxDrawable> drawable; + + SurfaceFormat format = aContext.GetDrawTarget()->GetFormat(); + // Don't allow creating images that are too big + if (aContext.GetDrawTarget()->CanCreateSimilarDrawTarget(imageSize, + format)) { + drawable = SVGIntegrationUtils::DrawableFromPaintServer( + mPaintServerFrame, mForFrame, mSize, imageSize, + aContext.GetDrawTarget(), aContext.CurrentMatrixDouble(), + SVGIntegrationUtils::FLAG_SYNC_DECODE_IMAGES); + } + + return drawable.forget(); + } + NS_ASSERTION(mImageElementSurface.GetSourceSurface(), + "Surface should be ready."); + RefPtr<gfxDrawable> drawable = + new gfxSurfaceDrawable(mImageElementSurface.GetSourceSurface().get(), + mImageElementSurface.mSize); + return drawable.forget(); +} + +ImgDrawResult nsImageRenderer::DrawLayer( + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDest, const nsRect& aFill, const nsPoint& aAnchor, + const nsRect& aDirty, const nsSize& aRepeatSize, float aOpacity) { + if (!IsReady()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + if (aDest.IsEmpty() || aFill.IsEmpty() || mSize.width <= 0 || + mSize.height <= 0) { + return ImgDrawResult::SUCCESS; + } + + return Draw( + aPresContext, aRenderingContext, aDirty, aDest, aFill, aAnchor, + aRepeatSize, + CSSIntRect(0, 0, nsPresContext::AppUnitsToIntCSSPixels(mSize.width), + nsPresContext::AppUnitsToIntCSSPixels(mSize.height)), + aOpacity); +} + +ImgDrawResult nsImageRenderer::BuildWebRenderDisplayItemsForLayer( + nsPresContext* aPresContext, mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, + const nsRect& aDest, const nsRect& aFill, const nsPoint& aAnchor, + const nsRect& aDirty, const nsSize& aRepeatSize, float aOpacity) { + if (!IsReady()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return mPrepareResult; + } + + CSSIntRect srcRect(0, 0, nsPresContext::AppUnitsToIntCSSPixels(mSize.width), + nsPresContext::AppUnitsToIntCSSPixels(mSize.height)); + + if (aDest.IsEmpty() || aFill.IsEmpty() || srcRect.IsEmpty()) { + return ImgDrawResult::SUCCESS; + } + return BuildWebRenderDisplayItems(aPresContext, aBuilder, aResources, aSc, + aManager, aItem, aDirty, aDest, aFill, + aAnchor, aRepeatSize, srcRect, aOpacity); +} + +/** + * Compute the size and position of the master copy of the image. I.e., a single + * tile used to fill the dest rect. + * aFill The destination rect to be filled + * aHFill and aVFill are the repeat patterns for the component - + * StyleBorderImageRepeat - i.e., how a tiling unit is used to fill aFill + * aUnitSize The size of the source rect in dest coords. + */ +static nsRect ComputeTile(nsRect& aFill, StyleBorderImageRepeat aHFill, + StyleBorderImageRepeat aVFill, + const nsSize& aUnitSize, nsSize& aRepeatSize) { + nsRect tile; + switch (aHFill) { + case StyleBorderImageRepeat::Stretch: + tile.x = aFill.x; + tile.width = aFill.width; + aRepeatSize.width = tile.width; + break; + case StyleBorderImageRepeat::Repeat: + tile.x = aFill.x + aFill.width / 2 - aUnitSize.width / 2; + tile.width = aUnitSize.width; + aRepeatSize.width = tile.width; + break; + case StyleBorderImageRepeat::Round: + tile.x = aFill.x; + tile.width = + nsCSSRendering::ComputeRoundedSize(aUnitSize.width, aFill.width); + aRepeatSize.width = tile.width; + break; + case StyleBorderImageRepeat::Space: { + nscoord space; + aRepeatSize.width = nsCSSRendering::ComputeBorderSpacedRepeatSize( + aUnitSize.width, aFill.width, space); + tile.x = aFill.x + space; + tile.width = aUnitSize.width; + aFill.x = tile.x; + aFill.width = aFill.width - space * 2; + } break; + default: + MOZ_ASSERT_UNREACHABLE("unrecognized border-image fill style"); + } + + switch (aVFill) { + case StyleBorderImageRepeat::Stretch: + tile.y = aFill.y; + tile.height = aFill.height; + aRepeatSize.height = tile.height; + break; + case StyleBorderImageRepeat::Repeat: + tile.y = aFill.y + aFill.height / 2 - aUnitSize.height / 2; + tile.height = aUnitSize.height; + aRepeatSize.height = tile.height; + break; + case StyleBorderImageRepeat::Round: + tile.y = aFill.y; + tile.height = + nsCSSRendering::ComputeRoundedSize(aUnitSize.height, aFill.height); + aRepeatSize.height = tile.height; + break; + case StyleBorderImageRepeat::Space: { + nscoord space; + aRepeatSize.height = nsCSSRendering::ComputeBorderSpacedRepeatSize( + aUnitSize.height, aFill.height, space); + tile.y = aFill.y + space; + tile.height = aUnitSize.height; + aFill.y = tile.y; + aFill.height = aFill.height - space * 2; + } break; + default: + MOZ_ASSERT_UNREACHABLE("unrecognized border-image fill style"); + } + + return tile; +} + +/** + * Returns true if the given set of arguments will require the tiles which fill + * the dest rect to be scaled from the source tile. See comment on ComputeTile + * for argument descriptions. + */ +static bool RequiresScaling(const nsRect& aFill, StyleBorderImageRepeat aHFill, + StyleBorderImageRepeat aVFill, + const nsSize& aUnitSize) { + // If we have no tiling in either direction, we can skip the intermediate + // scaling step. + return (aHFill != StyleBorderImageRepeat::Stretch || + aVFill != StyleBorderImageRepeat::Stretch) && + (aUnitSize.width != aFill.width || aUnitSize.height != aFill.height); +} + +ImgDrawResult nsImageRenderer::DrawBorderImageComponent( + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, const nsRect& aFill, const CSSIntRect& aSrc, + StyleBorderImageRepeat aHFill, StyleBorderImageRepeat aVFill, + const nsSize& aUnitSize, uint8_t aIndex, + const Maybe<nsSize>& aSVGViewportSize, const bool aHasIntrinsicRatio) { + if (!IsReady()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return ImgDrawResult::BAD_ARGS; + } + + if (aFill.IsEmpty() || aSrc.IsEmpty()) { + return ImgDrawResult::SUCCESS; + } + + const bool isRequestBacked = + mType == StyleImage::Tag::Url || mType == StyleImage::Tag::Rect; + MOZ_ASSERT(isRequestBacked == mImage->IsImageRequestType()); + + if (isRequestBacked || mType == StyleImage::Tag::Element) { + nsCOMPtr<imgIContainer> subImage; + + // To draw one portion of an image into a border component, we stretch that + // portion to match the size of that border component and then draw onto. + // However, preserveAspectRatio attribute of a SVG image may break this + // rule. To get correct rendering result, we add + // FLAG_FORCE_PRESERVEASPECTRATIO_NONE flag here, to tell mImage to ignore + // preserveAspectRatio attribute, and always do non-uniform stretch. + uint32_t drawFlags = ConvertImageRendererToDrawFlags(mFlags) | + imgIContainer::FLAG_FORCE_PRESERVEASPECTRATIO_NONE; + // For those SVG image sources which don't have fixed aspect ratio (i.e. + // without viewport size and viewBox), we should scale the source uniformly + // after the viewport size is decided by "Default Sizing Algorithm". + if (!aHasIntrinsicRatio) { + drawFlags = drawFlags | imgIContainer::FLAG_FORCE_UNIFORM_SCALING; + } + // Retrieve or create the subimage we'll draw. + nsIntRect srcRect(aSrc.x, aSrc.y, aSrc.width, aSrc.height); + if (isRequestBacked) { + subImage = ImageOps::Clip(mImageContainer, srcRect, aSVGViewportSize); + } else { + // This path, for eStyleImageType_Element, is currently slower than it + // needs to be because we don't cache anything. (In particular, if we have + // to draw to a temporary surface inside ClippedImage, we don't cache that + // temporary surface since we immediately throw the ClippedImage we create + // here away.) However, if we did cache, we'd need to know when to + // invalidate that cache, and it's not clear that it's worth the trouble + // since using border-image with -moz-element is rare. + + RefPtr<gfxDrawable> drawable = + DrawableForElement(nsRect(nsPoint(), mSize), aRenderingContext); + if (!drawable) { + NS_WARNING("Could not create drawable for element"); + return ImgDrawResult::TEMPORARY_ERROR; + } + + nsCOMPtr<imgIContainer> image(ImageOps::CreateFromDrawable(drawable)); + subImage = ImageOps::Clip(image, srcRect, aSVGViewportSize); + } + + MOZ_ASSERT(!aSVGViewportSize || + subImage->GetType() == imgIContainer::TYPE_VECTOR); + + SamplingFilter samplingFilter = + nsLayoutUtils::GetSamplingFilterForFrame(mForFrame); + + if (!RequiresScaling(aFill, aHFill, aVFill, aUnitSize)) { + ImgDrawResult result = nsLayoutUtils::DrawSingleImage( + aRenderingContext, aPresContext, subImage, samplingFilter, aFill, + aDirtyRect, SVGImageContext(), drawFlags); + + if (!mImage->IsComplete()) { + result &= ImgDrawResult::SUCCESS_NOT_COMPLETE; + } + + return result; + } + + nsSize repeatSize; + nsRect fillRect(aFill); + nsRect tile = ComputeTile(fillRect, aHFill, aVFill, aUnitSize, repeatSize); + + ImgDrawResult result = nsLayoutUtils::DrawBackgroundImage( + aRenderingContext, mForFrame, aPresContext, subImage, samplingFilter, + tile, fillRect, repeatSize, tile.TopLeft(), aDirtyRect, drawFlags, + ExtendMode::CLAMP, 1.0); + + if (!mImage->IsComplete()) { + result &= ImgDrawResult::SUCCESS_NOT_COMPLETE; + } + + return result; + } + + nsSize repeatSize(aFill.Size()); + 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()) { + MOZ_ASSERT_UNREACHABLE( + "Ensure PrepareImage() has returned true before " + "calling me"); + return ImgDrawResult::NOT_READY; + } + + if (mSize.width <= 0 || mSize.height <= 0) { + return ImgDrawResult::SUCCESS; + } + + if (mImage->IsImageRequestType()) { + uint32_t drawFlags = + ConvertImageRendererToDrawFlags(mFlags) | imgIContainer::FRAME_FIRST; + nsRect dest(nsPoint(0, 0), mSize); + // We have a tricky situation in our choice of SamplingFilter. Shape + // images define a float area based on the alpha values in the rendered + // pixels. When multiple device pixels are used for one css pixel, the + // sampling can change crisp edges into aliased edges. For visual pixels, + // that's usually the right choice. For defining a float area, it can + // cause problems. If a style is using a shape-image-threshold value that + // is less than the alpha of the edge pixels, any filtering may smear the + // alpha into adjacent pixels and expand the float area in a confusing + // way. Since the alpha threshold can be set precisely in CSS, and since a + // web author may be counting on that threshold to define a precise float + // area from an image, it is least confusing to have the rendered pixels + // have unfiltered alpha. We use SamplingFilter::POINT to ensure that each + // rendered pixel has an alpha that precisely matches the alpha of the + // closest pixel in the image. + return nsLayoutUtils::DrawSingleImage( + aRenderingContext, aPresContext, mImageContainer, SamplingFilter::POINT, + dest, dest, SVGImageContext(), drawFlags); + } + + if (mImage->IsGradient()) { + nsCSSGradientRenderer renderer = nsCSSGradientRenderer::Create( + aPresContext, mForFrame->Style(), *mGradientData, mSize); + nsRect dest(nsPoint(0, 0), mSize); + renderer.Paint(aRenderingContext, dest, dest, mSize, + CSSIntRect::FromAppUnitsRounded(dest), dest, 1.0); + return ImgDrawResult::SUCCESS; + } + + // Unsupported image type. + return ImgDrawResult::BAD_IMAGE; +} + +bool nsImageRenderer::IsRasterImage() { + return mImageContainer && + mImageContainer->GetType() == imgIContainer::TYPE_RASTER; +} + +already_AddRefed<imgIContainer> nsImageRenderer::GetImage() { + return do_AddRef(mImageContainer); +} |