gecko-dev/layout/svg/nsFilterInstance.cpp

631 строка
22 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
// Main header first:
#include "nsFilterInstance.h"
// MFBT headers next:
#include "mozilla/UniquePtr.h"
// Keep others in (case-insensitive) order:
#include "DrawResult.h"
#include "gfx2DGlue.h"
#include "gfxContext.h"
#include "gfxPlatform.h"
#include "gfxUtils.h"
#include "mozilla/gfx/Helpers.h"
#include "mozilla/gfx/PatternHelpers.h"
#include "nsSVGDisplayableFrame.h"
#include "nsCSSFilterInstance.h"
#include "nsSVGFilterInstance.h"
#include "nsSVGFilterPaintCallback.h"
#include "nsSVGUtils.h"
#include "SVGContentUtils.h"
#include "FilterSupport.h"
#include "gfx2DGlue.h"
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;
using namespace mozilla::image;
FilterDescription
nsFilterInstance::GetFilterDescription(nsIContent* aFilteredElement,
const nsTArray<nsStyleFilter>& aFilterChain,
bool aFilterInputIsTainted,
const UserSpaceMetrics& aMetrics,
const gfxRect& aBBox,
nsTArray<RefPtr<SourceSurface>>& aOutAdditionalImages)
{
gfxMatrix identity;
nsFilterInstance instance(nullptr, aFilteredElement, aMetrics,
aFilterChain, aFilterInputIsTainted, nullptr,
identity, nullptr, nullptr, nullptr, &aBBox);
if (!instance.IsInitialized()) {
return FilterDescription();
}
return instance.ExtractDescriptionAndAdditionalImages(aOutAdditionalImages);
}
static UniquePtr<UserSpaceMetrics>
UserSpaceMetricsForFrame(nsIFrame* aFrame)
{
if (aFrame->GetContent()->IsSVGElement()) {
nsSVGElement* element = static_cast<nsSVGElement*>(aFrame->GetContent());
return MakeUnique<SVGElementMetrics>(element);
}
return MakeUnique<NonSVGFrameUserSpaceMetrics>(aFrame);
}
void
nsFilterInstance::PaintFilteredFrame(nsIFrame *aFilteredFrame,
gfxContext* aCtx,
nsSVGFilterPaintCallback *aPaintCallback,
const nsRegion *aDirtyArea,
imgDrawingParams& aImgParams)
{
auto& filterChain = aFilteredFrame->StyleEffects()->mFilters;
UniquePtr<UserSpaceMetrics> metrics = UserSpaceMetricsForFrame(aFilteredFrame);
gfxContextMatrixAutoSaveRestore autoSR(aCtx);
gfxSize scaleFactors = aCtx->CurrentMatrix().ScaleFactors(true);
gfxMatrix scaleMatrix(scaleFactors.width, 0.0f,
0.0f, scaleFactors.height,
0.0f, 0.0f);
gfxMatrix reverseScaleMatrix = scaleMatrix;
DebugOnly<bool> invertible = reverseScaleMatrix.Invert();
MOZ_ASSERT(invertible);
// Pull scale vector out of aCtx's transform, put all scale factors, which
// includes css and css-to-dev-px scale, into scaleMatrixInDevUnits.
aCtx->SetMatrix(reverseScaleMatrix * aCtx->CurrentMatrix());
gfxMatrix scaleMatrixInDevUnits =
scaleMatrix * nsSVGUtils::GetCSSPxToDevPxMatrix(aFilteredFrame);
// Hardcode InputIsTainted to true because we don't want JS to be able to
// read the rendered contents of aFilteredFrame.
nsFilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(),
*metrics, filterChain, /* InputIsTainted */ true,
aPaintCallback, scaleMatrixInDevUnits,
aDirtyArea, nullptr, nullptr, nullptr);
if (instance.IsInitialized()) {
instance.Render(aCtx, aImgParams);
}
}
nsRegion
nsFilterInstance::GetPostFilterDirtyArea(nsIFrame *aFilteredFrame,
const nsRegion& aPreFilterDirtyRegion)
{
if (aPreFilterDirtyRegion.IsEmpty()) {
return nsRegion();
}
gfxMatrix tm = nsSVGUtils::GetCanvasTM(aFilteredFrame);
auto& filterChain = aFilteredFrame->StyleEffects()->mFilters;
UniquePtr<UserSpaceMetrics> metrics = UserSpaceMetricsForFrame(aFilteredFrame);
// Hardcode InputIsTainted to true because we don't want JS to be able to
// read the rendered contents of aFilteredFrame.
nsFilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(),
*metrics, filterChain, /* InputIsTainted */ true,
nullptr, tm, nullptr, &aPreFilterDirtyRegion);
if (!instance.IsInitialized()) {
return nsRegion();
}
// We've passed in the source's dirty area so the instance knows about it.
// Now we can ask the instance to compute the area of the filter output
// that's dirty.
return instance.ComputePostFilterDirtyRegion();
}
nsRegion
nsFilterInstance::GetPreFilterNeededArea(nsIFrame *aFilteredFrame,
const nsRegion& aPostFilterDirtyRegion)
{
gfxMatrix tm = nsSVGUtils::GetCanvasTM(aFilteredFrame);
auto& filterChain = aFilteredFrame->StyleEffects()->mFilters;
UniquePtr<UserSpaceMetrics> metrics = UserSpaceMetricsForFrame(aFilteredFrame);
// Hardcode InputIsTainted to true because we don't want JS to be able to
// read the rendered contents of aFilteredFrame.
nsFilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(),
*metrics, filterChain, /* InputIsTainted */ true,
nullptr, tm, &aPostFilterDirtyRegion);
if (!instance.IsInitialized()) {
return nsRect();
}
// Now we can ask the instance to compute the area of the source
// that's needed.
return instance.ComputeSourceNeededRect();
}
nsRect
nsFilterInstance::GetPostFilterBounds(nsIFrame *aFilteredFrame,
const gfxRect *aOverrideBBox,
const nsRect *aPreFilterBounds)
{
MOZ_ASSERT(!(aFilteredFrame->GetStateBits() & NS_FRAME_SVG_LAYOUT) ||
!(aFilteredFrame->GetStateBits() & NS_FRAME_IS_NONDISPLAY),
"Non-display SVG do not maintain visual overflow rects");
nsRegion preFilterRegion;
nsRegion* preFilterRegionPtr = nullptr;
if (aPreFilterBounds) {
preFilterRegion = *aPreFilterBounds;
preFilterRegionPtr = &preFilterRegion;
}
gfxMatrix tm = nsSVGUtils::GetCanvasTM(aFilteredFrame);
auto& filterChain = aFilteredFrame->StyleEffects()->mFilters;
UniquePtr<UserSpaceMetrics> metrics = UserSpaceMetricsForFrame(aFilteredFrame);
// Hardcode InputIsTainted to true because we don't want JS to be able to
// read the rendered contents of aFilteredFrame.
nsFilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(),
*metrics, filterChain, /* InputIsTainted */ true,
nullptr, tm, nullptr, preFilterRegionPtr,
aPreFilterBounds, aOverrideBBox);
if (!instance.IsInitialized()) {
return nsRect();
}
return instance.ComputePostFilterExtents();
}
nsFilterInstance::nsFilterInstance(nsIFrame *aTargetFrame,
nsIContent* aTargetContent,
const UserSpaceMetrics& aMetrics,
const nsTArray<nsStyleFilter>& aFilterChain,
bool aFilterInputIsTainted,
nsSVGFilterPaintCallback *aPaintCallback,
const gfxMatrix& aPaintTransform,
const nsRegion *aPostFilterDirtyRegion,
const nsRegion *aPreFilterDirtyRegion,
const nsRect *aPreFilterVisualOverflowRectOverride,
const gfxRect *aOverrideBBox)
: mTargetFrame(aTargetFrame)
, mTargetContent(aTargetContent)
, mMetrics(aMetrics)
, mPaintCallback(aPaintCallback)
, mPaintTransform(aPaintTransform)
, mInitialized(false)
{
if (aOverrideBBox) {
mTargetBBox = *aOverrideBBox;
} else {
MOZ_ASSERT(mTargetFrame, "Need to supply a frame when there's no aOverrideBBox");
mTargetBBox = nsSVGUtils::GetBBox(mTargetFrame,
nsSVGUtils::eUseFrameBoundsForOuterSVG |
nsSVGUtils::eBBoxIncludeFillGeometry);
}
// Compute user space to filter space transforms.
if (!ComputeUserSpaceToFilterSpaceScale()) {
return;
}
if (!ComputeTargetBBoxInFilterSpace()) {
return;
}
// Get various transforms:
gfxMatrix filterToUserSpace(mFilterSpaceToUserSpaceScale.width, 0.0f,
0.0f, mFilterSpaceToUserSpaceScale.height,
0.0f, 0.0f);
mFilterSpaceToFrameSpaceInCSSPxTransform =
filterToUserSpace * GetUserSpaceToFrameSpaceInCSSPxTransform();
// mFilterSpaceToFrameSpaceInCSSPxTransform is always invertible
mFrameSpaceInCSSPxToFilterSpaceTransform =
mFilterSpaceToFrameSpaceInCSSPxTransform;
mFrameSpaceInCSSPxToFilterSpaceTransform.Invert();
nsIntRect targetBounds;
if (aPreFilterVisualOverflowRectOverride) {
targetBounds =
FrameSpaceToFilterSpace(aPreFilterVisualOverflowRectOverride);
} else if (mTargetFrame) {
nsRect preFilterVOR = mTargetFrame->GetPreEffectsVisualOverflowRect();
targetBounds = FrameSpaceToFilterSpace(&preFilterVOR);
}
mTargetBounds.UnionRect(mTargetBBoxInFilterSpace, targetBounds);
// Build the filter graph.
if (NS_FAILED(BuildPrimitives(aFilterChain, aTargetFrame,
aFilterInputIsTainted))) {
return;
}
// Convert the passed in rects from frame space to filter space:
mPostFilterDirtyRegion = FrameSpaceToFilterSpace(aPostFilterDirtyRegion);
mPreFilterDirtyRegion = FrameSpaceToFilterSpace(aPreFilterDirtyRegion);
mInitialized = true;
}
bool
nsFilterInstance::ComputeTargetBBoxInFilterSpace()
{
gfxRect targetBBoxInFilterSpace = UserSpaceToFilterSpace(mTargetBBox);
targetBBoxInFilterSpace.RoundOut();
return gfxUtils::GfxRectToIntRect(targetBBoxInFilterSpace,
&mTargetBBoxInFilterSpace);
}
bool
nsFilterInstance::ComputeUserSpaceToFilterSpaceScale()
{
if (mTargetFrame) {
mUserSpaceToFilterSpaceScale = mPaintTransform.ScaleFactors(true);
if (mUserSpaceToFilterSpaceScale.width <= 0.0f ||
mUserSpaceToFilterSpaceScale.height <= 0.0f) {
// Nothing should be rendered.
return false;
}
} else {
mUserSpaceToFilterSpaceScale = gfxSize(1.0, 1.0);
}
mFilterSpaceToUserSpaceScale =
gfxSize(1.0f / mUserSpaceToFilterSpaceScale.width,
1.0f / mUserSpaceToFilterSpaceScale.height);
return true;
}
gfxRect
nsFilterInstance::UserSpaceToFilterSpace(const gfxRect& aUserSpaceRect) const
{
gfxRect filterSpaceRect = aUserSpaceRect;
filterSpaceRect.Scale(mUserSpaceToFilterSpaceScale.width,
mUserSpaceToFilterSpaceScale.height);
return filterSpaceRect;
}
gfxRect
nsFilterInstance::FilterSpaceToUserSpace(const gfxRect& aFilterSpaceRect) const
{
gfxRect userSpaceRect = aFilterSpaceRect;
userSpaceRect.Scale(mFilterSpaceToUserSpaceScale.width,
mFilterSpaceToUserSpaceScale.height);
return userSpaceRect;
}
nsresult
nsFilterInstance::BuildPrimitives(const nsTArray<nsStyleFilter>& aFilterChain,
nsIFrame* aTargetFrame,
bool aFilterInputIsTainted)
{
NS_ASSERTION(!mPrimitiveDescriptions.Length(),
"expected to start building primitives from scratch");
for (uint32_t i = 0; i < aFilterChain.Length(); i++) {
bool inputIsTainted =
mPrimitiveDescriptions.IsEmpty() ? aFilterInputIsTainted :
mPrimitiveDescriptions.LastElement().IsTainted();
nsresult rv = BuildPrimitivesForFilter(aFilterChain[i], aTargetFrame, inputIsTainted);
if (NS_FAILED(rv)) {
return rv;
}
}
mFilterDescription = FilterDescription(mPrimitiveDescriptions);
return NS_OK;
}
nsresult
nsFilterInstance::BuildPrimitivesForFilter(const nsStyleFilter& aFilter,
nsIFrame* aTargetFrame,
bool aInputIsTainted)
{
NS_ASSERTION(mUserSpaceToFilterSpaceScale.width > 0.0f &&
mFilterSpaceToUserSpaceScale.height > 0.0f,
"scale factors between spaces should be positive values");
if (aFilter.GetType() == NS_STYLE_FILTER_URL) {
// Build primitives for an SVG filter.
nsSVGFilterInstance svgFilterInstance(aFilter, aTargetFrame,
mTargetContent,
mMetrics, mTargetBBox,
mUserSpaceToFilterSpaceScale);
if (!svgFilterInstance.IsInitialized()) {
return NS_ERROR_FAILURE;
}
return svgFilterInstance.BuildPrimitives(mPrimitiveDescriptions, mInputImages,
aInputIsTainted);
}
// Build primitives for a CSS filter.
// If we don't have a frame, use opaque black for shadows with unspecified
// shadow colors.
nscolor shadowFallbackColor =
mTargetFrame ? mTargetFrame->StyleColor()->mColor : NS_RGB(0,0,0);
nsCSSFilterInstance cssFilterInstance(aFilter, shadowFallbackColor,
mTargetBounds,
mFrameSpaceInCSSPxToFilterSpaceTransform);
return cssFilterInstance.BuildPrimitives(mPrimitiveDescriptions, aInputIsTainted);
}
static void
UpdateNeededBounds(const nsIntRegion& aRegion, nsIntRect& aBounds)
{
aBounds = aRegion.GetBounds();
bool overflow;
IntSize surfaceSize =
nsSVGUtils::ConvertToSurfaceSize(SizeDouble(aBounds.Size()), &overflow);
if (overflow) {
aBounds.SizeTo(surfaceSize);
}
}
void
nsFilterInstance::ComputeNeededBoxes()
{
if (mPrimitiveDescriptions.IsEmpty())
return;
nsIntRegion sourceGraphicNeededRegion;
nsIntRegion fillPaintNeededRegion;
nsIntRegion strokePaintNeededRegion;
FilterSupport::ComputeSourceNeededRegions(
mFilterDescription, mPostFilterDirtyRegion,
sourceGraphicNeededRegion, fillPaintNeededRegion, strokePaintNeededRegion);
sourceGraphicNeededRegion.And(sourceGraphicNeededRegion, mTargetBounds);
UpdateNeededBounds(sourceGraphicNeededRegion, mSourceGraphic.mNeededBounds);
UpdateNeededBounds(fillPaintNeededRegion, mFillPaint.mNeededBounds);
UpdateNeededBounds(strokePaintNeededRegion, mStrokePaint.mNeededBounds);
}
void
nsFilterInstance::BuildSourcePaint(SourceInfo *aSource,
imgDrawingParams& aImgParams)
{
MOZ_ASSERT(mTargetFrame);
nsIntRect neededRect = aSource->mNeededBounds;
if (neededRect.IsEmpty()) {
return;
}
RefPtr<DrawTarget> offscreenDT =
gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
neededRect.Size(), SurfaceFormat::B8G8R8A8);
if (!offscreenDT || !offscreenDT->IsValid()) {
return;
}
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(offscreenDT);
MOZ_ASSERT(ctx); // already checked the draw target above
gfxContextAutoSaveRestore saver(ctx);
ctx->SetMatrix(mPaintTransform *
gfxMatrix::Translation(-neededRect.TopLeft()));
GeneralPattern pattern;
if (aSource == &mFillPaint) {
nsSVGUtils::MakeFillPatternFor(mTargetFrame, ctx, &pattern, aImgParams);
} else if (aSource == &mStrokePaint) {
nsSVGUtils::MakeStrokePatternFor(mTargetFrame, ctx, &pattern, aImgParams);
}
if (pattern.GetPattern()) {
offscreenDT->FillRect(ToRect(FilterSpaceToUserSpace(ThebesRect(neededRect))),
pattern);
}
aSource->mSourceSurface = offscreenDT->Snapshot();
aSource->mSurfaceRect = neededRect;
}
void
nsFilterInstance::BuildSourcePaints(imgDrawingParams& aImgParams)
{
if (!mFillPaint.mNeededBounds.IsEmpty()) {
BuildSourcePaint(&mFillPaint, aImgParams);
}
if (!mStrokePaint.mNeededBounds.IsEmpty()) {
BuildSourcePaint(&mStrokePaint, aImgParams);
}
}
void
nsFilterInstance::BuildSourceImage(imgDrawingParams& aImgParams)
{
MOZ_ASSERT(mTargetFrame);
nsIntRect neededRect = mSourceGraphic.mNeededBounds;
if (neededRect.IsEmpty()) {
return;
}
RefPtr<DrawTarget> offscreenDT =
gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
neededRect.Size(), SurfaceFormat::B8G8R8A8);
if (!offscreenDT || !offscreenDT->IsValid()) {
return;
}
gfxRect r = FilterSpaceToUserSpace(ThebesRect(neededRect));
r.RoundOut();
nsIntRect dirty;
if (!gfxUtils::GfxRectToIntRect(r, &dirty)){
return;
}
// SVG graphics paint to device space, so we need to set an initial device
// space to filter space transform on the gfxContext that SourceGraphic
// and SourceAlpha will paint to.
//
// (In theory it would be better to minimize error by having filtered SVG
// graphics temporarily paint to user space when painting the sources and
// only set a user space to filter space transform on the gfxContext
// (since that would eliminate the transform multiplications from user
// space to device space and back again). However, that would make the
// code more complex while being hard to get right without introducing
// subtle bugs, and in practice it probably makes no real difference.)
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(offscreenDT);
MOZ_ASSERT(ctx); // already checked the draw target above
gfxMatrix devPxToCssPxTM = nsSVGUtils::GetCSSPxToDevPxMatrix(mTargetFrame);
DebugOnly<bool> invertible = devPxToCssPxTM.Invert();
MOZ_ASSERT(invertible);
ctx->SetMatrix(devPxToCssPxTM * mPaintTransform *
gfxMatrix::Translation(-neededRect.TopLeft()));
mPaintCallback->Paint(*ctx, mTargetFrame, mPaintTransform, &dirty, aImgParams);
mSourceGraphic.mSourceSurface = offscreenDT->Snapshot();
mSourceGraphic.mSurfaceRect = neededRect;
}
void
nsFilterInstance::Render(gfxContext* aCtx, imgDrawingParams& aImgParams)
{
MOZ_ASSERT(mTargetFrame, "Need a frame for rendering");
if (mPrimitiveDescriptions.IsEmpty()) {
// An filter without any primitive. Treat it as success and paint nothing.
return;
}
nsIntRect filterRect =
mPostFilterDirtyRegion.GetBounds().Intersect(OutputFilterSpaceBounds());
if (filterRect.IsEmpty() || mPaintTransform.IsSingular()) {
return;
}
gfxContextMatrixAutoSaveRestore autoSR(aCtx);
aCtx->SetMatrix(aCtx->CurrentMatrix().PreTranslate(filterRect.x, filterRect.y));
ComputeNeededBoxes();
BuildSourceImage(aImgParams);
BuildSourcePaints(aImgParams);
FilterSupport::RenderFilterDescription(
aCtx->GetDrawTarget(), mFilterDescription, IntRectToRect(filterRect),
mSourceGraphic.mSourceSurface, mSourceGraphic.mSurfaceRect,
mFillPaint.mSourceSurface, mFillPaint.mSurfaceRect,
mStrokePaint.mSourceSurface, mStrokePaint.mSurfaceRect,
mInputImages, Point(0, 0));
}
nsRegion
nsFilterInstance::ComputePostFilterDirtyRegion()
{
if (mPreFilterDirtyRegion.IsEmpty() || mPrimitiveDescriptions.IsEmpty()) {
return nsRegion();
}
nsIntRegion resultChangeRegion =
FilterSupport::ComputeResultChangeRegion(mFilterDescription,
mPreFilterDirtyRegion, nsIntRegion(), nsIntRegion());
return FilterSpaceToFrameSpace(resultChangeRegion);
}
nsRect
nsFilterInstance::ComputePostFilterExtents()
{
if (mPrimitiveDescriptions.IsEmpty()) {
return nsRect();
}
nsIntRegion postFilterExtents =
FilterSupport::ComputePostFilterExtents(mFilterDescription, mTargetBounds);
return FilterSpaceToFrameSpace(postFilterExtents.GetBounds());
}
nsRect
nsFilterInstance::ComputeSourceNeededRect()
{
ComputeNeededBoxes();
return FilterSpaceToFrameSpace(mSourceGraphic.mNeededBounds);
}
nsIntRect
nsFilterInstance::OutputFilterSpaceBounds() const
{
uint32_t numPrimitives = mPrimitiveDescriptions.Length();
if (numPrimitives <= 0)
return nsIntRect();
return mPrimitiveDescriptions[numPrimitives - 1].PrimitiveSubregion();
}
nsIntRect
nsFilterInstance::FrameSpaceToFilterSpace(const nsRect* aRect) const
{
nsIntRect rect = OutputFilterSpaceBounds();
if (aRect) {
if (aRect->IsEmpty()) {
return nsIntRect();
}
gfxRect rectInCSSPx =
nsLayoutUtils::RectToGfxRect(*aRect, nsPresContext::AppUnitsPerCSSPixel());
gfxRect rectInFilterSpace =
mFrameSpaceInCSSPxToFilterSpaceTransform.TransformBounds(rectInCSSPx);
rectInFilterSpace.RoundOut();
nsIntRect intRect;
if (gfxUtils::GfxRectToIntRect(rectInFilterSpace, &intRect)) {
rect = intRect;
}
}
return rect;
}
nsRect
nsFilterInstance::FilterSpaceToFrameSpace(const nsIntRect& aRect) const
{
if (aRect.IsEmpty()) {
return nsRect();
}
gfxRect r(aRect.x, aRect.y, aRect.width, aRect.height);
r = mFilterSpaceToFrameSpaceInCSSPxTransform.TransformBounds(r);
// nsLayoutUtils::RoundGfxRectToAppRect rounds out.
return nsLayoutUtils::RoundGfxRectToAppRect(r, nsPresContext::AppUnitsPerCSSPixel());
}
nsIntRegion
nsFilterInstance::FrameSpaceToFilterSpace(const nsRegion* aRegion) const
{
if (!aRegion) {
return OutputFilterSpaceBounds();
}
nsIntRegion result;
for (auto iter = aRegion->RectIter(); !iter.Done(); iter.Next()) {
// FrameSpaceToFilterSpace rounds out, so this works.
result.Or(result, FrameSpaceToFilterSpace(&iter.Get()));
}
return result;
}
nsRegion
nsFilterInstance::FilterSpaceToFrameSpace(const nsIntRegion& aRegion) const
{
nsRegion result;
for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) {
// FilterSpaceToFrameSpace rounds out, so this works.
result.Or(result, FilterSpaceToFrameSpace(iter.Get()));
}
return result;
}
gfxMatrix
nsFilterInstance::GetUserSpaceToFrameSpaceInCSSPxTransform() const
{
if (!mTargetFrame) {
return gfxMatrix();
}
return gfxMatrix::Translation(-nsSVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(mTargetFrame));
}