1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
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 "gfxBlur.h"
7
8 #include "gfx2DGlue.h"
9 #include "gfxContext.h"
10 #include "gfxPlatform.h"
11 #include "mozilla/gfx/2D.h"
12 #include "mozilla/gfx/Blur.h"
13 #include "mozilla/gfx/PathHelpers.h"
14 #include "mozilla/UniquePtr.h"
15 #include "mozilla/UniquePtrExtensions.h"
16 #include "nsExpirationTracker.h"
17 #include "nsClassHashtable.h"
18 #include "gfxUtils.h"
19
20 using namespace mozilla;
21 using namespace mozilla::gfx;
22
gfxAlphaBoxBlur()23 gfxAlphaBoxBlur::gfxAlphaBoxBlur()
24 {
25 }
26
~gfxAlphaBoxBlur()27 gfxAlphaBoxBlur::~gfxAlphaBoxBlur()
28 {
29 mContext = nullptr;
30 }
31
32 gfxContext*
Init(const gfxRect & aRect,const IntSize & aSpreadRadius,const IntSize & aBlurRadius,const gfxRect * aDirtyRect,const gfxRect * aSkipRect)33 gfxAlphaBoxBlur::Init(const gfxRect& aRect,
34 const IntSize& aSpreadRadius,
35 const IntSize& aBlurRadius,
36 const gfxRect* aDirtyRect,
37 const gfxRect* aSkipRect)
38 {
39 mozilla::gfx::Rect rect(Float(aRect.x), Float(aRect.y),
40 Float(aRect.width), Float(aRect.height));
41 IntSize spreadRadius(aSpreadRadius.width, aSpreadRadius.height);
42 IntSize blurRadius(aBlurRadius.width, aBlurRadius.height);
43 UniquePtr<Rect> dirtyRect;
44 if (aDirtyRect) {
45 dirtyRect = MakeUnique<Rect>(Float(aDirtyRect->x),
46 Float(aDirtyRect->y),
47 Float(aDirtyRect->width),
48 Float(aDirtyRect->height));
49 }
50 UniquePtr<Rect> skipRect;
51 if (aSkipRect) {
52 skipRect = MakeUnique<Rect>(Float(aSkipRect->x),
53 Float(aSkipRect->y),
54 Float(aSkipRect->width),
55 Float(aSkipRect->height));
56 }
57
58 mBlur = MakeUnique<AlphaBoxBlur>(rect, spreadRadius, blurRadius, dirtyRect.get(), skipRect.get());
59 size_t blurDataSize = mBlur->GetSurfaceAllocationSize();
60 if (blurDataSize == 0)
61 return nullptr;
62
63 IntSize size = mBlur->GetSize();
64
65 // Make an alpha-only surface to draw on. We will play with the data after
66 // everything is drawn to create a blur effect.
67 mData = MakeUniqueFallible<unsigned char[]>(blurDataSize);
68 if (!mData) {
69 return nullptr;
70 }
71 memset(mData.get(), 0, blurDataSize);
72
73 RefPtr<DrawTarget> dt =
74 gfxPlatform::CreateDrawTargetForData(mData.get(), size,
75 mBlur->GetStride(),
76 SurfaceFormat::A8);
77 if (!dt || !dt->IsValid()) {
78 return nullptr;
79 }
80
81 IntRect irect = mBlur->GetRect();
82 gfxPoint topleft(irect.TopLeft().x, irect.TopLeft().y);
83
84 mContext = gfxContext::CreateOrNull(dt);
85 MOZ_ASSERT(mContext); // already checked for target above
86 mContext->SetMatrix(gfxMatrix::Translation(-topleft));
87
88 return mContext;
89 }
90
91 void
DrawBlur(gfxContext * aDestinationCtx,SourceSurface * aBlur,const IntPoint & aTopLeft,const Rect * aDirtyRect)92 DrawBlur(gfxContext* aDestinationCtx,
93 SourceSurface* aBlur,
94 const IntPoint& aTopLeft,
95 const Rect* aDirtyRect)
96 {
97 DrawTarget *dest = aDestinationCtx->GetDrawTarget();
98
99 RefPtr<gfxPattern> thebesPat = aDestinationCtx->GetPattern();
100 Pattern* pat = thebesPat->GetPattern(dest, nullptr);
101
102 Matrix oldTransform = dest->GetTransform();
103 Matrix newTransform = oldTransform;
104 newTransform.PreTranslate(aTopLeft.x, aTopLeft.y);
105
106 // Avoid a semi-expensive clip operation if we can, otherwise
107 // clip to the dirty rect
108 if (aDirtyRect) {
109 dest->PushClipRect(*aDirtyRect);
110 }
111
112 dest->SetTransform(newTransform);
113 dest->MaskSurface(*pat, aBlur, Point(0, 0));
114 dest->SetTransform(oldTransform);
115
116 if (aDirtyRect) {
117 dest->PopClip();
118 }
119 }
120
121 already_AddRefed<SourceSurface>
DoBlur(DrawTarget * aDT,IntPoint * aTopLeft)122 gfxAlphaBoxBlur::DoBlur(DrawTarget* aDT, IntPoint* aTopLeft)
123 {
124 mBlur->Blur(mData.get());
125
126 *aTopLeft = mBlur->GetRect().TopLeft();
127
128 return aDT->CreateSourceSurfaceFromData(mData.get(),
129 mBlur->GetSize(),
130 mBlur->GetStride(),
131 SurfaceFormat::A8);
132 }
133
134 void
Paint(gfxContext * aDestinationCtx)135 gfxAlphaBoxBlur::Paint(gfxContext* aDestinationCtx)
136 {
137 if (!mContext)
138 return;
139
140 DrawTarget *dest = aDestinationCtx->GetDrawTarget();
141 if (!dest) {
142 NS_WARNING("Blurring not supported for Thebes contexts!");
143 return;
144 }
145
146 Rect* dirtyRect = mBlur->GetDirtyRect();
147
148 IntPoint topLeft;
149 RefPtr<SourceSurface> mask = DoBlur(dest, &topLeft);
150 if (!mask) {
151 NS_ERROR("Failed to create mask!");
152 return;
153 }
154
155 DrawBlur(aDestinationCtx, mask, topLeft, dirtyRect);
156 }
157
CalculateBlurRadius(const gfxPoint & aStd)158 IntSize gfxAlphaBoxBlur::CalculateBlurRadius(const gfxPoint& aStd)
159 {
160 mozilla::gfx::Point std(Float(aStd.x), Float(aStd.y));
161 IntSize size = AlphaBoxBlur::CalculateBlurRadius(std);
162 return IntSize(size.width, size.height);
163 }
164
165 struct BlurCacheKey : public PLDHashEntryHdr {
166 typedef const BlurCacheKey& KeyType;
167 typedef const BlurCacheKey* KeyTypePointer;
168 enum { ALLOW_MEMMOVE = true };
169
170 IntSize mMinSize;
171 IntSize mBlurRadius;
172 Color mShadowColor;
173 BackendType mBackend;
174 RectCornerRadii mCornerRadii;
175 bool mIsInset;
176
177 // Only used for inset blurs
178 bool mHasBorderRadius;
179 IntSize mInnerMinSize;
180
BlurCacheKeyBlurCacheKey181 BlurCacheKey(IntSize aMinSize, IntSize aBlurRadius,
182 RectCornerRadii* aCornerRadii, const Color& aShadowColor,
183 BackendType aBackendType)
184 : BlurCacheKey(aMinSize, IntSize(0, 0),
185 aBlurRadius, aCornerRadii,
186 aShadowColor, false,
187 false, aBackendType)
188 {}
189
BlurCacheKeyBlurCacheKey190 explicit BlurCacheKey(const BlurCacheKey* aOther)
191 : mMinSize(aOther->mMinSize)
192 , mBlurRadius(aOther->mBlurRadius)
193 , mShadowColor(aOther->mShadowColor)
194 , mBackend(aOther->mBackend)
195 , mCornerRadii(aOther->mCornerRadii)
196 , mIsInset(aOther->mIsInset)
197 , mHasBorderRadius(aOther->mHasBorderRadius)
198 , mInnerMinSize(aOther->mInnerMinSize)
199 { }
200
BlurCacheKeyBlurCacheKey201 explicit BlurCacheKey(IntSize aOuterMinSize, IntSize aInnerMinSize,
202 IntSize aBlurRadius,
203 const RectCornerRadii* aCornerRadii,
204 const Color& aShadowColor, bool aIsInset,
205 bool aHasBorderRadius, BackendType aBackendType)
206 : mMinSize(aOuterMinSize)
207 , mBlurRadius(aBlurRadius)
208 , mShadowColor(aShadowColor)
209 , mBackend(aBackendType)
210 , mCornerRadii(aCornerRadii ? *aCornerRadii : RectCornerRadii())
211 , mIsInset(aIsInset)
212 , mHasBorderRadius(aHasBorderRadius)
213 , mInnerMinSize(aInnerMinSize)
214 { }
215
216 static PLDHashNumber
HashKeyBlurCacheKey217 HashKey(const KeyTypePointer aKey)
218 {
219 PLDHashNumber hash = 0;
220 hash = AddToHash(hash, aKey->mMinSize.width, aKey->mMinSize.height);
221 hash = AddToHash(hash, aKey->mBlurRadius.width, aKey->mBlurRadius.height);
222
223 hash = AddToHash(hash, HashBytes(&aKey->mShadowColor.r,
224 sizeof(aKey->mShadowColor.r)));
225 hash = AddToHash(hash, HashBytes(&aKey->mShadowColor.g,
226 sizeof(aKey->mShadowColor.g)));
227 hash = AddToHash(hash, HashBytes(&aKey->mShadowColor.b,
228 sizeof(aKey->mShadowColor.b)));
229 hash = AddToHash(hash, HashBytes(&aKey->mShadowColor.a,
230 sizeof(aKey->mShadowColor.a)));
231
232 for (int i = 0; i < 4; i++) {
233 hash = AddToHash(hash, aKey->mCornerRadii[i].width, aKey->mCornerRadii[i].height);
234 }
235
236 hash = AddToHash(hash, (uint32_t)aKey->mBackend);
237
238 if (aKey->mIsInset) {
239 hash = AddToHash(hash, aKey->mInnerMinSize.width, aKey->mInnerMinSize.height);
240 hash = AddToHash(hash, HashBytes(&aKey->mHasBorderRadius, sizeof(bool)));
241 }
242 return hash;
243 }
244
245 bool
KeyEqualsBlurCacheKey246 KeyEquals(KeyTypePointer aKey) const
247 {
248 if (aKey->mMinSize == mMinSize &&
249 aKey->mBlurRadius == mBlurRadius &&
250 aKey->mCornerRadii == mCornerRadii &&
251 aKey->mShadowColor == mShadowColor &&
252 aKey->mBackend == mBackend) {
253
254 if (mIsInset) {
255 return (mHasBorderRadius == aKey->mHasBorderRadius) &&
256 (mInnerMinSize == aKey->mInnerMinSize);
257 }
258
259 return true;
260 }
261
262 return false;
263 }
264
265 static KeyTypePointer
KeyToPointerBlurCacheKey266 KeyToPointer(KeyType aKey)
267 {
268 return &aKey;
269 }
270 };
271
272 /**
273 * This class is what is cached. It need to be allocated in an object separated
274 * to the cache entry to be able to be tracked by the nsExpirationTracker.
275 * */
276 struct BlurCacheData {
BlurCacheDataBlurCacheData277 BlurCacheData(SourceSurface* aBlur, IntMargin aExtendDestBy, const BlurCacheKey& aKey)
278 : mBlur(aBlur)
279 , mExtendDest(aExtendDestBy)
280 , mKey(aKey)
281 {}
282
BlurCacheDataBlurCacheData283 BlurCacheData(const BlurCacheData& aOther)
284 : mBlur(aOther.mBlur)
285 , mExtendDest(aOther.mExtendDest)
286 , mKey(aOther.mKey)
287 { }
288
GetExpirationStateBlurCacheData289 nsExpirationState *GetExpirationState() {
290 return &mExpirationState;
291 }
292
293 nsExpirationState mExpirationState;
294 RefPtr<SourceSurface> mBlur;
295 IntMargin mExtendDest;
296 BlurCacheKey mKey;
297 };
298
299 /**
300 * This class implements a cache with no maximum size, that retains the
301 * SourceSurfaces used to draw the blurs.
302 *
303 * An entry stays in the cache as long as it is used often.
304 */
305 class BlurCache final : public nsExpirationTracker<BlurCacheData,4>
306 {
307 public:
BlurCache()308 BlurCache()
309 : nsExpirationTracker<BlurCacheData, 4>(GENERATION_MS, "BlurCache")
310 {
311 }
312
NotifyExpired(BlurCacheData * aObject)313 virtual void NotifyExpired(BlurCacheData* aObject)
314 {
315 RemoveObject(aObject);
316 mHashEntries.Remove(aObject->mKey);
317 }
318
Lookup(const IntSize aMinSize,const IntSize & aBlurRadius,RectCornerRadii * aCornerRadii,const Color & aShadowColor,BackendType aBackendType)319 BlurCacheData* Lookup(const IntSize aMinSize,
320 const IntSize& aBlurRadius,
321 RectCornerRadii* aCornerRadii,
322 const Color& aShadowColor,
323 BackendType aBackendType)
324 {
325 BlurCacheData* blur =
326 mHashEntries.Get(BlurCacheKey(aMinSize, aBlurRadius,
327 aCornerRadii, aShadowColor,
328 aBackendType));
329 if (blur) {
330 MarkUsed(blur);
331 }
332
333 return blur;
334 }
335
LookupInsetBoxShadow(const IntSize aOuterMinSize,const IntSize aInnerMinSize,const IntSize & aBlurRadius,const RectCornerRadii * aCornerRadii,const Color & aShadowColor,const bool & aHasBorderRadius,BackendType aBackendType)336 BlurCacheData* LookupInsetBoxShadow(const IntSize aOuterMinSize,
337 const IntSize aInnerMinSize,
338 const IntSize& aBlurRadius,
339 const RectCornerRadii* aCornerRadii,
340 const Color& aShadowColor,
341 const bool& aHasBorderRadius,
342 BackendType aBackendType)
343 {
344 bool insetBoxShadow = true;
345 BlurCacheKey key(aOuterMinSize, aInnerMinSize,
346 aBlurRadius, aCornerRadii,
347 aShadowColor, insetBoxShadow,
348 aHasBorderRadius, aBackendType);
349 BlurCacheData* blur = mHashEntries.Get(key);
350 if (blur) {
351 MarkUsed(blur);
352 }
353
354 return blur;
355 }
356
357 // Returns true if we successfully register the blur in the cache, false
358 // otherwise.
RegisterEntry(BlurCacheData * aValue)359 bool RegisterEntry(BlurCacheData* aValue)
360 {
361 nsresult rv = AddObject(aValue);
362 if (NS_FAILED(rv)) {
363 // We are OOM, and we cannot track this object. We don't want stall
364 // entries in the hash table (since the expiration tracker is responsible
365 // for removing the cache entries), so we avoid putting that entry in the
366 // table, which is a good things considering we are short on memory
367 // anyway, we probably don't want to retain things.
368 return false;
369 }
370 mHashEntries.Put(aValue->mKey, aValue);
371 return true;
372 }
373
374 protected:
375 static const uint32_t GENERATION_MS = 1000;
376 /**
377 * FIXME use nsTHashtable to avoid duplicating the BlurCacheKey.
378 * https://bugzilla.mozilla.org/show_bug.cgi?id=761393#c47
379 */
380 nsClassHashtable<BlurCacheKey, BlurCacheData> mHashEntries;
381 };
382
383 static BlurCache* gBlurCache = nullptr;
384
385 static IntSize
ComputeMinSizeForShadowShape(RectCornerRadii * aCornerRadii,IntSize aBlurRadius,IntMargin & aSlice,const IntSize & aRectSize)386 ComputeMinSizeForShadowShape(RectCornerRadii* aCornerRadii,
387 IntSize aBlurRadius,
388 IntMargin& aSlice,
389 const IntSize& aRectSize)
390 {
391 float cornerWidth = 0;
392 float cornerHeight = 0;
393 if (aCornerRadii) {
394 RectCornerRadii corners = *aCornerRadii;
395 for (size_t i = 0; i < 4; i++) {
396 cornerWidth = std::max(cornerWidth, corners[i].width);
397 cornerHeight = std::max(cornerHeight, corners[i].height);
398 }
399 }
400
401 aSlice = IntMargin(ceil(cornerHeight) + aBlurRadius.height,
402 ceil(cornerWidth) + aBlurRadius.width,
403 ceil(cornerHeight) + aBlurRadius.height,
404 ceil(cornerWidth) + aBlurRadius.width);
405
406 IntSize minSize(aSlice.LeftRight() + 1,
407 aSlice.TopBottom() + 1);
408
409 // If aRectSize is smaller than minSize, the border-image approach won't
410 // work; there's no way to squeeze parts of the min box-shadow source
411 // image such that the result looks correct. So we need to adjust minSize
412 // in such a way that we can later draw it without stretching in the affected
413 // dimension. We also need to adjust "slice" to ensure that we're not trying
414 // to slice away more than we have.
415 if (aRectSize.width < minSize.width) {
416 minSize.width = aRectSize.width;
417 aSlice.left = 0;
418 aSlice.right = 0;
419 }
420 if (aRectSize.height < minSize.height) {
421 minSize.height = aRectSize.height;
422 aSlice.top = 0;
423 aSlice.bottom = 0;
424 }
425
426 MOZ_ASSERT(aSlice.LeftRight() <= minSize.width);
427 MOZ_ASSERT(aSlice.TopBottom() <= minSize.height);
428 return minSize;
429 }
430
431 void
CacheBlur(DrawTarget & aDT,const IntSize & aMinSize,const IntSize & aBlurRadius,RectCornerRadii * aCornerRadii,const Color & aShadowColor,IntMargin aExtendDest,SourceSurface * aBoxShadow)432 CacheBlur(DrawTarget& aDT,
433 const IntSize& aMinSize,
434 const IntSize& aBlurRadius,
435 RectCornerRadii* aCornerRadii,
436 const Color& aShadowColor,
437 IntMargin aExtendDest,
438 SourceSurface* aBoxShadow)
439 {
440 BlurCacheKey key(aMinSize, aBlurRadius, aCornerRadii, aShadowColor, aDT.GetBackendType());
441 BlurCacheData* data = new BlurCacheData(aBoxShadow, aExtendDest, key);
442 if (!gBlurCache->RegisterEntry(data)) {
443 delete data;
444 }
445 }
446
447 // Blurs a small surface and creates the mask.
448 static already_AddRefed<SourceSurface>
CreateBlurMask(const IntSize & aMinSize,RectCornerRadii * aCornerRadii,IntSize aBlurRadius,IntMargin & aExtendDestBy,IntMargin & aSliceBorder,DrawTarget & aDestDrawTarget)449 CreateBlurMask(const IntSize& aMinSize,
450 RectCornerRadii* aCornerRadii,
451 IntSize aBlurRadius,
452 IntMargin& aExtendDestBy,
453 IntMargin& aSliceBorder,
454 DrawTarget& aDestDrawTarget)
455 {
456 gfxAlphaBoxBlur blur;
457 IntRect minRect(IntPoint(), aMinSize);
458
459 gfxContext* blurCtx = blur.Init(ThebesRect(Rect(minRect)), IntSize(),
460 aBlurRadius, nullptr, nullptr);
461
462 if (!blurCtx) {
463 return nullptr;
464 }
465
466 DrawTarget* blurDT = blurCtx->GetDrawTarget();
467 ColorPattern black(Color(0.f, 0.f, 0.f, 1.f));
468
469 if (aCornerRadii) {
470 RefPtr<Path> roundedRect =
471 MakePathForRoundedRect(*blurDT, Rect(minRect), *aCornerRadii);
472 blurDT->Fill(roundedRect, black);
473 } else {
474 blurDT->FillRect(Rect(minRect), black);
475 }
476
477 IntPoint topLeft;
478 RefPtr<SourceSurface> result = blur.DoBlur(&aDestDrawTarget, &topLeft);
479 if (!result) {
480 return nullptr;
481 }
482
483 IntRect expandedMinRect(topLeft, result->GetSize());
484 aExtendDestBy = expandedMinRect - minRect;
485 aSliceBorder += aExtendDestBy;
486
487 MOZ_ASSERT(aSliceBorder.LeftRight() <= expandedMinRect.width);
488 MOZ_ASSERT(aSliceBorder.TopBottom() <= expandedMinRect.height);
489
490 return result.forget();
491 }
492
493 static already_AddRefed<SourceSurface>
CreateBoxShadow(DrawTarget & aDestDT,SourceSurface * aBlurMask,const Color & aShadowColor)494 CreateBoxShadow(DrawTarget& aDestDT, SourceSurface* aBlurMask, const Color& aShadowColor)
495 {
496 IntSize blurredSize = aBlurMask->GetSize();
497 RefPtr<DrawTarget> boxShadowDT =
498 Factory::CreateDrawTarget(aDestDT.GetBackendType(), blurredSize, SurfaceFormat::B8G8R8A8);
499
500 if (!boxShadowDT) {
501 return nullptr;
502 }
503
504 ColorPattern shadowColor(ToDeviceColor(aShadowColor));
505 boxShadowDT->MaskSurface(shadowColor, aBlurMask, Point(0, 0));
506 return boxShadowDT->Snapshot();
507 }
508
509 static already_AddRefed<SourceSurface>
GetBlur(gfxContext * aDestinationCtx,const IntSize & aRectSize,const IntSize & aBlurRadius,RectCornerRadii * aCornerRadii,const Color & aShadowColor,IntMargin & aExtendDestBy,IntMargin & aSlice)510 GetBlur(gfxContext* aDestinationCtx,
511 const IntSize& aRectSize,
512 const IntSize& aBlurRadius,
513 RectCornerRadii* aCornerRadii,
514 const Color& aShadowColor,
515 IntMargin& aExtendDestBy,
516 IntMargin& aSlice)
517 {
518 if (!gBlurCache) {
519 gBlurCache = new BlurCache();
520 }
521
522 IntSize minSize =
523 ComputeMinSizeForShadowShape(aCornerRadii, aBlurRadius, aSlice, aRectSize);
524
525 // We can get seams using the min size rect when drawing to the destination rect
526 // if we have a non-pixel aligned destination transformation. In those cases,
527 // fallback to just rendering the destination rect.
528 Matrix destMatrix = ToMatrix(aDestinationCtx->CurrentMatrix());
529 bool useDestRect = !destMatrix.IsRectilinear() || destMatrix.HasNonIntegerTranslation();
530 if (useDestRect) {
531 minSize = aRectSize;
532 }
533
534 DrawTarget& destDT = *aDestinationCtx->GetDrawTarget();
535
536 BlurCacheData* cached = gBlurCache->Lookup(minSize, aBlurRadius,
537 aCornerRadii, aShadowColor,
538 destDT.GetBackendType());
539 if (cached && !useDestRect) {
540 // See CreateBlurMask() for these values
541 aExtendDestBy = cached->mExtendDest;
542 aSlice = aSlice + aExtendDestBy;
543 RefPtr<SourceSurface> blur = cached->mBlur;
544 return blur.forget();
545 }
546
547 RefPtr<SourceSurface> blurMask =
548 CreateBlurMask(minSize, aCornerRadii, aBlurRadius, aExtendDestBy, aSlice,
549 destDT);
550
551 if (!blurMask) {
552 return nullptr;
553 }
554
555 RefPtr<SourceSurface> boxShadow = CreateBoxShadow(destDT, blurMask, aShadowColor);
556 if (!boxShadow) {
557 return nullptr;
558 }
559
560 if (useDestRect) {
561 // Since we're just going to paint the actual rect to the destination
562 aSlice.SizeTo(0, 0, 0, 0);
563 } else {
564 CacheBlur(destDT, minSize, aBlurRadius, aCornerRadii, aShadowColor,
565 aExtendDestBy, boxShadow);
566 }
567 return boxShadow.forget();
568 }
569
570 void
ShutdownBlurCache()571 gfxAlphaBoxBlur::ShutdownBlurCache()
572 {
573 delete gBlurCache;
574 gBlurCache = nullptr;
575 }
576
577 static Rect
RectWithEdgesTRBL(Float aTop,Float aRight,Float aBottom,Float aLeft)578 RectWithEdgesTRBL(Float aTop, Float aRight, Float aBottom, Float aLeft)
579 {
580 return Rect(aLeft, aTop, aRight - aLeft, aBottom - aTop);
581 }
582
583 static void
RepeatOrStretchSurface(DrawTarget & aDT,SourceSurface * aSurface,const Rect & aDest,const Rect & aSrc,Rect & aSkipRect)584 RepeatOrStretchSurface(DrawTarget& aDT, SourceSurface* aSurface,
585 const Rect& aDest, const Rect& aSrc, Rect& aSkipRect)
586 {
587 if (aSkipRect.Contains(aDest)) {
588 return;
589 }
590
591 if ((!aDT.GetTransform().IsRectilinear() &&
592 aDT.GetBackendType() != BackendType::CAIRO) ||
593 (aDT.GetBackendType() == BackendType::DIRECT2D1_1)) {
594 // Use stretching if possible, since it leads to less seams when the
595 // destination is transformed. However, don't do this if we're using cairo,
596 // because if cairo is using pixman it won't render anything for large
597 // stretch factors because pixman's internal fixed point precision is not
598 // high enough to handle those scale factors.
599 // Calling FillRect on a D2D backend with a repeating pattern is much slower
600 // than DrawSurface, so special case the D2D backend here.
601 aDT.DrawSurface(aSurface, aDest, aSrc);
602 return;
603 }
604
605 SurfacePattern pattern(aSurface, ExtendMode::REPEAT,
606 Matrix::Translation(aDest.TopLeft() - aSrc.TopLeft()),
607 SamplingFilter::GOOD, RoundedToInt(aSrc));
608 aDT.FillRect(aDest, pattern);
609 }
610
611 static void
DrawCorner(DrawTarget & aDT,SourceSurface * aSurface,const Rect & aDest,const Rect & aSrc,Rect & aSkipRect)612 DrawCorner(DrawTarget& aDT, SourceSurface* aSurface,
613 const Rect& aDest, const Rect& aSrc, Rect& aSkipRect)
614 {
615 if (aSkipRect.Contains(aDest)) {
616 return;
617 }
618
619 aDT.DrawSurface(aSurface, aDest, aSrc);
620 }
621
622 static void
DrawBoxShadows(DrawTarget & aDestDrawTarget,SourceSurface * aSourceBlur,Rect aDstOuter,Rect aDstInner,Rect aSrcOuter,Rect aSrcInner,Rect aSkipRect)623 DrawBoxShadows(DrawTarget& aDestDrawTarget, SourceSurface* aSourceBlur,
624 Rect aDstOuter, Rect aDstInner, Rect aSrcOuter, Rect aSrcInner,
625 Rect aSkipRect)
626 {
627 // Corners: top left, top right, bottom left, bottom right
628 DrawCorner(aDestDrawTarget, aSourceBlur,
629 RectWithEdgesTRBL(aDstOuter.Y(), aDstInner.X(),
630 aDstInner.Y(), aDstOuter.X()),
631 RectWithEdgesTRBL(aSrcOuter.Y(), aSrcInner.X(),
632 aSrcInner.Y(), aSrcOuter.X()),
633 aSkipRect);
634
635 DrawCorner(aDestDrawTarget, aSourceBlur,
636 RectWithEdgesTRBL(aDstOuter.Y(), aDstOuter.XMost(),
637 aDstInner.Y(), aDstInner.XMost()),
638 RectWithEdgesTRBL(aSrcOuter.Y(), aSrcOuter.XMost(),
639 aSrcInner.Y(), aSrcInner.XMost()),
640 aSkipRect);
641
642 DrawCorner(aDestDrawTarget, aSourceBlur,
643 RectWithEdgesTRBL(aDstInner.YMost(), aDstInner.X(),
644 aDstOuter.YMost(), aDstOuter.X()),
645 RectWithEdgesTRBL(aSrcInner.YMost(), aSrcInner.X(),
646 aSrcOuter.YMost(), aSrcOuter.X()),
647 aSkipRect);
648
649 DrawCorner(aDestDrawTarget, aSourceBlur,
650 RectWithEdgesTRBL(aDstInner.YMost(), aDstOuter.XMost(),
651 aDstOuter.YMost(), aDstInner.XMost()),
652 RectWithEdgesTRBL(aSrcInner.YMost(), aSrcOuter.XMost(),
653 aSrcOuter.YMost(), aSrcInner.XMost()),
654 aSkipRect);
655
656 // Edges: top, left, right, bottom
657 RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
658 RectWithEdgesTRBL(aDstOuter.Y(), aDstInner.XMost(),
659 aDstInner.Y(), aDstInner.X()),
660 RectWithEdgesTRBL(aSrcOuter.Y(), aSrcInner.XMost(),
661 aSrcInner.Y(), aSrcInner.X()),
662 aSkipRect);
663 RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
664 RectWithEdgesTRBL(aDstInner.Y(), aDstInner.X(),
665 aDstInner.YMost(), aDstOuter.X()),
666 RectWithEdgesTRBL(aSrcInner.Y(), aSrcInner.X(),
667 aSrcInner.YMost(), aSrcOuter.X()),
668 aSkipRect);
669
670 RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
671 RectWithEdgesTRBL(aDstInner.Y(), aDstOuter.XMost(),
672 aDstInner.YMost(), aDstInner.XMost()),
673 RectWithEdgesTRBL(aSrcInner.Y(), aSrcOuter.XMost(),
674 aSrcInner.YMost(), aSrcInner.XMost()),
675 aSkipRect);
676 RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
677 RectWithEdgesTRBL(aDstInner.YMost(), aDstInner.XMost(),
678 aDstOuter.YMost(), aDstInner.X()),
679 RectWithEdgesTRBL(aSrcInner.YMost(), aSrcInner.XMost(),
680 aSrcOuter.YMost(), aSrcInner.X()),
681 aSkipRect);
682 }
683
684 /***
685 * We draw a blurred a rectangle by only blurring a smaller rectangle and
686 * splitting the rectangle into 9 parts.
687 * First, a small minimum source rect is calculated and used to create a blur
688 * mask since the actual blurring itself is expensive. Next, we use the mask
689 * with the given shadow color to create a minimally-sized box shadow of the
690 * right color. Finally, we cut out the 9 parts from the box-shadow source and
691 * paint each part in the right place, stretching the non-corner parts to fill
692 * the space between the corners.
693 */
694
695 /* static */ void
BlurRectangle(gfxContext * aDestinationCtx,const gfxRect & aRect,RectCornerRadii * aCornerRadii,const gfxPoint & aBlurStdDev,const Color & aShadowColor,const gfxRect & aDirtyRect,const gfxRect & aSkipRect)696 gfxAlphaBoxBlur::BlurRectangle(gfxContext* aDestinationCtx,
697 const gfxRect& aRect,
698 RectCornerRadii* aCornerRadii,
699 const gfxPoint& aBlurStdDev,
700 const Color& aShadowColor,
701 const gfxRect& aDirtyRect,
702 const gfxRect& aSkipRect)
703 {
704 IntSize blurRadius = CalculateBlurRadius(aBlurStdDev);
705
706 IntRect rect = RoundedToInt(ToRect(aRect));
707 IntMargin extendDestBy;
708 IntMargin slice;
709
710 RefPtr<SourceSurface> boxShadow = GetBlur(aDestinationCtx,
711 rect.Size(), blurRadius,
712 aCornerRadii, aShadowColor,
713 extendDestBy, slice);
714 if (!boxShadow) {
715 return;
716 }
717
718 DrawTarget& destDrawTarget = *aDestinationCtx->GetDrawTarget();
719 destDrawTarget.PushClipRect(ToRect(aDirtyRect));
720
721 // Copy the right parts from boxShadow into destDrawTarget. The middle parts
722 // will be stretched, border-image style.
723
724 Rect srcOuter(Point(), Size(boxShadow->GetSize()));
725 Rect srcInner = srcOuter;
726 srcInner.Deflate(Margin(slice));
727
728 rect.Inflate(extendDestBy);
729 Rect dstOuter(rect);
730 Rect dstInner(rect);
731 dstInner.Deflate(Margin(slice));
732
733 Rect skipRect = ToRect(aSkipRect);
734
735 if (srcInner.IsEqualInterior(srcOuter)) {
736 MOZ_ASSERT(dstInner.IsEqualInterior(dstOuter));
737 // The target rect is smaller than the minimal size so just draw the surface
738 destDrawTarget.DrawSurface(boxShadow, dstInner, srcInner);
739 } else {
740 DrawBoxShadows(destDrawTarget, boxShadow, dstOuter, dstInner,
741 srcOuter, srcInner, skipRect);
742
743 // Middle part
744 RepeatOrStretchSurface(destDrawTarget, boxShadow,
745 RectWithEdgesTRBL(dstInner.Y(), dstInner.XMost(),
746 dstInner.YMost(), dstInner.X()),
747 RectWithEdgesTRBL(srcInner.Y(), srcInner.XMost(),
748 srcInner.YMost(), srcInner.X()),
749 skipRect);
750 }
751
752 // A note about anti-aliasing and seems between adjacent parts:
753 // We don't explicitly disable anti-aliasing in the DrawSurface calls above,
754 // so if there's a transform on destDrawTarget that is not pixel-aligned,
755 // there will be seams between adjacent parts of the box-shadow. It's hard to
756 // avoid those without the use of an intermediate surface.
757 // You might think that we could avoid those by just turning of AA, but there
758 // is a problem with that: Box-shadow rendering needs to clip out the
759 // element's border box, and we'd like that clip to have anti-aliasing -
760 // especially if the element has rounded corners! So we can't do that unless
761 // we have a way to say "Please anti-alias the clip, but don't antialias the
762 // destination rect of the DrawSurface call".
763 // On OS X there is an additional problem with turning off AA: CoreGraphics
764 // will not just fill the pixels that have their pixel center inside the
765 // filled shape. Instead, it will fill all the pixels which are partially
766 // covered by the shape. So for pixels on the edge between two adjacent parts,
767 // all those pixels will be painted to by both parts, which looks very bad.
768
769 destDrawTarget.PopClip();
770 }
771
772 static already_AddRefed<Path>
GetBoxShadowInsetPath(DrawTarget * aDrawTarget,const Rect aOuterRect,const Rect aInnerRect,const bool aHasBorderRadius,const RectCornerRadii & aInnerClipRadii)773 GetBoxShadowInsetPath(DrawTarget* aDrawTarget,
774 const Rect aOuterRect, const Rect aInnerRect,
775 const bool aHasBorderRadius, const RectCornerRadii& aInnerClipRadii)
776 {
777 /***
778 * We create an inset path by having two rects.
779 *
780 * -----------------------
781 * | ________________ |
782 * | | | |
783 * | | | |
784 * | ------------------ |
785 * |_____________________|
786 *
787 * The outer rect and the inside rect. The path
788 * creates a frame around the content where we draw the inset shadow.
789 */
790 RefPtr<PathBuilder> builder =
791 aDrawTarget->CreatePathBuilder(FillRule::FILL_EVEN_ODD);
792 AppendRectToPath(builder, aOuterRect, true);
793
794 if (aHasBorderRadius) {
795 AppendRoundedRectToPath(builder, aInnerRect, aInnerClipRadii, false);
796 } else {
797 AppendRectToPath(builder, aInnerRect, false);
798 }
799 return builder->Finish();
800 }
801
802 static void
FillDestinationPath(gfxContext * aDestinationCtx,const Rect aDestinationRect,const Rect aShadowClipRect,const Color & aShadowColor,const bool aHasBorderRadius,const RectCornerRadii & aInnerClipRadii)803 FillDestinationPath(gfxContext* aDestinationCtx,
804 const Rect aDestinationRect,
805 const Rect aShadowClipRect,
806 const Color& aShadowColor,
807 const bool aHasBorderRadius,
808 const RectCornerRadii& aInnerClipRadii)
809 {
810 // When there is no blur radius, fill the path onto the destination
811 // surface.
812 aDestinationCtx->SetColor(aShadowColor);
813 DrawTarget* destDrawTarget = aDestinationCtx->GetDrawTarget();
814 RefPtr<Path> shadowPath = GetBoxShadowInsetPath(destDrawTarget, aDestinationRect,
815 aShadowClipRect, aHasBorderRadius,
816 aInnerClipRadii);
817
818 aDestinationCtx->SetPath(shadowPath);
819 aDestinationCtx->Fill();
820 }
821
822 static void
CacheInsetBlur(const IntSize aMinOuterSize,const IntSize aMinInnerSize,const IntSize & aBlurRadius,const RectCornerRadii * aCornerRadii,const Color & aShadowColor,const bool & aHasBorderRadius,BackendType aBackendType,SourceSurface * aBoxShadow)823 CacheInsetBlur(const IntSize aMinOuterSize,
824 const IntSize aMinInnerSize,
825 const IntSize& aBlurRadius,
826 const RectCornerRadii* aCornerRadii,
827 const Color& aShadowColor,
828 const bool& aHasBorderRadius,
829 BackendType aBackendType,
830 SourceSurface* aBoxShadow)
831 {
832 bool isInsetBlur = true;
833 BlurCacheKey key(aMinOuterSize, aMinInnerSize,
834 aBlurRadius, aCornerRadii,
835 aShadowColor, isInsetBlur,
836 aHasBorderRadius, aBackendType);
837 IntMargin extendDestBy(0, 0, 0, 0);
838 BlurCacheData* data = new BlurCacheData(aBoxShadow, extendDestBy, key);
839 if (!gBlurCache->RegisterEntry(data)) {
840 delete data;
841 }
842 }
843
844 already_AddRefed<mozilla::gfx::SourceSurface>
GetInsetBlur(const mozilla::gfx::Rect aOuterRect,const mozilla::gfx::Rect aWhitespaceRect,const bool aIsDestRect,const mozilla::gfx::Color & aShadowColor,const mozilla::gfx::IntSize & aBlurRadius,const bool aHasBorderRadius,const RectCornerRadii & aInnerClipRadii,DrawTarget * aDestDrawTarget)845 gfxAlphaBoxBlur::GetInsetBlur(const mozilla::gfx::Rect aOuterRect,
846 const mozilla::gfx::Rect aWhitespaceRect,
847 const bool aIsDestRect,
848 const mozilla::gfx::Color& aShadowColor,
849 const mozilla::gfx::IntSize& aBlurRadius,
850 const bool aHasBorderRadius,
851 const RectCornerRadii& aInnerClipRadii,
852 DrawTarget* aDestDrawTarget)
853 {
854 if (!gBlurCache) {
855 gBlurCache = new BlurCache();
856 }
857
858 IntSize outerSize((int)aOuterRect.width, (int)aOuterRect.height);
859 IntSize whitespaceSize((int)aWhitespaceRect.width, (int)aWhitespaceRect.height);
860 BlurCacheData* cached =
861 gBlurCache->LookupInsetBoxShadow(outerSize, whitespaceSize,
862 aBlurRadius, &aInnerClipRadii,
863 aShadowColor, aHasBorderRadius,
864 aDestDrawTarget->GetBackendType());
865
866 if (cached && !aIsDestRect) {
867 // So we don't forget the actual cached blur
868 RefPtr<SourceSurface> cachedBlur = cached->mBlur;
869 return cachedBlur.forget();
870 }
871
872 // If we can do a min rect, the whitespace rect will be expanded in Init to
873 // aOuterRect.
874 Rect blurRect = aIsDestRect ? aOuterRect : aWhitespaceRect;
875 IntSize zeroSpread(0, 0);
876 gfxContext* minGfxContext = Init(ThebesRect(blurRect),
877 zeroSpread, aBlurRadius,
878 nullptr, nullptr);
879 if (!minGfxContext) {
880 return nullptr;
881 }
882
883 // This is really annoying. When we create the AlphaBoxBlur, the gfxContext
884 // has a translation applied to it that is the topLeft point. This is actually
885 // the rect we gave it plus the blur radius. The rects we give this for the outer
886 // and whitespace rects are based at (0, 0). We could either translate those rects
887 // when we don't have a destination rect or ignore the translation when using
888 // the dest rect. The dest rects layout gives us expect this translation.
889 if (!aIsDestRect) {
890 minGfxContext->SetMatrix(gfxMatrix());
891 }
892
893 DrawTarget* minDrawTarget = minGfxContext->GetDrawTarget();
894
895 // Fill in the path between the inside white space / outer rects
896 // NOT the inner frame
897 RefPtr<Path> maskPath =
898 GetBoxShadowInsetPath(minDrawTarget, aOuterRect,
899 aWhitespaceRect, aHasBorderRadius,
900 aInnerClipRadii);
901
902 Color black(0.f, 0.f, 0.f, 1.f);
903 minGfxContext->SetColor(black);
904 minGfxContext->SetPath(maskPath);
905 minGfxContext->Fill();
906
907 // Create the A8 mask
908 IntPoint topLeft;
909 RefPtr<SourceSurface> minMask = DoBlur(minDrawTarget, &topLeft);
910 if (!minMask) {
911 return nullptr;
912 }
913
914 // Fill in with the color we actually wanted
915 RefPtr<SourceSurface> minInsetBlur = CreateBoxShadow(*aDestDrawTarget, minMask, aShadowColor);
916 if (!minInsetBlur) {
917 return nullptr;
918 }
919
920 if (!aIsDestRect) {
921 CacheInsetBlur(outerSize, whitespaceSize,
922 aBlurRadius, &aInnerClipRadii,
923 aShadowColor, aHasBorderRadius,
924 aDestDrawTarget->GetBackendType(),
925 minInsetBlur);
926 }
927
928 return minInsetBlur.forget();
929 }
930
931 /***
932 * We create our minimal rect with 2 rects.
933 * The first is the inside whitespace rect, that is "cut out"
934 * from the box. This is (1). This must be the size
935 * of the blur radius + corner radius so we can have a big enough
936 * inside cut.
937 *
938 * The second (2) is one blur radius surrounding the inner
939 * frame of (1). This is the amount of blur space required
940 * to get a proper blend.
941 *
942 * B = one blur size
943 * W = one blur + corner radii - known as inner margin
944 * ___________________________________
945 * | |
946 * | | | |
947 * | (2) | (1) | (2) |
948 * | B | W | B |
949 * | | | |
950 * | | | |
951 * | | |
952 * |________________________________|
953 */
GetBlurMargins(const bool aHasBorderRadius,const RectCornerRadii & aInnerClipRadii,const IntSize aBlurRadius,Margin & aOutBlurMargin,Margin & aOutInnerMargin)954 static void GetBlurMargins(const bool aHasBorderRadius,
955 const RectCornerRadii& aInnerClipRadii,
956 const IntSize aBlurRadius,
957 Margin& aOutBlurMargin,
958 Margin& aOutInnerMargin)
959 {
960 float cornerWidth = 0;
961 float cornerHeight = 0;
962 if (aHasBorderRadius) {
963 for (size_t i = 0; i < 4; i++) {
964 cornerWidth = std::max(cornerWidth, aInnerClipRadii[i].width);
965 cornerHeight = std::max(cornerHeight, aInnerClipRadii[i].height);
966 }
967 }
968
969 // Only the inside whitespace size cares about the border radius size.
970 // Outer sizes only care about blur.
971 int width = cornerWidth + aBlurRadius.width;
972 int height = cornerHeight + aBlurRadius.height;
973
974 aOutInnerMargin.SizeTo(height, width, height, width);
975 aOutBlurMargin.SizeTo(aBlurRadius.height, aBlurRadius.width,
976 aBlurRadius.height, aBlurRadius.width);
977 }
978
979 static bool
GetInsetBoxShadowRects(const Margin aBlurMargin,const Margin aInnerMargin,const Rect aShadowClipRect,const Rect aDestinationRect,Rect & aOutWhitespaceRect,Rect & aOutOuterRect)980 GetInsetBoxShadowRects(const Margin aBlurMargin,
981 const Margin aInnerMargin,
982 const Rect aShadowClipRect,
983 const Rect aDestinationRect,
984 Rect& aOutWhitespaceRect,
985 Rect& aOutOuterRect)
986 {
987 // We always copy (2 * blur radius) + corner radius worth of data to the destination rect
988 // This covers the blend of the path + the actual blur
989 // Need +1 so that we copy the edges correctly as we'll copy
990 // over the min box shadow corners then the +1 for the edges between
991 // Note, the (x,y) coordinates are from the blur margin
992 // since the frame outside the whitespace rect is 1 blur radius extra space.
993 Rect insideWhiteSpace(aBlurMargin.left,
994 aBlurMargin.top,
995 aInnerMargin.LeftRight() + 1,
996 aInnerMargin.TopBottom() + 1);
997
998 // If the inner white space rect is larger than the shadow clip rect
999 // our approach does not work as we'll just copy one corner
1000 // and cover the destination. In those cases, fallback to the destination rect
1001 bool useDestRect = (aShadowClipRect.width <= aInnerMargin.LeftRight()) ||
1002 (aShadowClipRect.height <= aInnerMargin.TopBottom());
1003
1004 if (useDestRect) {
1005 aOutWhitespaceRect = aShadowClipRect;
1006 aOutOuterRect = aDestinationRect;
1007 } else {
1008 aOutWhitespaceRect = insideWhiteSpace;
1009 aOutOuterRect = aOutWhitespaceRect;
1010 aOutOuterRect.Inflate(aBlurMargin);
1011 }
1012
1013 return useDestRect;
1014 }
1015
1016 void
BlurInsetBox(gfxContext * aDestinationCtx,const Rect aDestinationRect,const Rect aShadowClipRect,const IntSize aBlurRadius,const IntSize aSpreadRadius,const Color & aShadowColor,bool aHasBorderRadius,const RectCornerRadii & aInnerClipRadii,const Rect aSkipRect,const Point aShadowOffset)1017 gfxAlphaBoxBlur::BlurInsetBox(gfxContext* aDestinationCtx,
1018 const Rect aDestinationRect,
1019 const Rect aShadowClipRect,
1020 const IntSize aBlurRadius,
1021 const IntSize aSpreadRadius,
1022 const Color& aShadowColor,
1023 bool aHasBorderRadius,
1024 const RectCornerRadii& aInnerClipRadii,
1025 const Rect aSkipRect,
1026 const Point aShadowOffset)
1027 {
1028 if ((aBlurRadius.width == 0 && aBlurRadius.height == 0)
1029 || aShadowClipRect.IsEmpty()) {
1030 FillDestinationPath(aDestinationCtx, aDestinationRect, aShadowClipRect,
1031 aShadowColor, aHasBorderRadius, aInnerClipRadii);
1032 return;
1033 }
1034
1035 DrawTarget* destDrawTarget = aDestinationCtx->GetDrawTarget();
1036
1037 Margin innerMargin;
1038 Margin blurMargin;
1039 GetBlurMargins(aHasBorderRadius, aInnerClipRadii, aBlurRadius,
1040 blurMargin, innerMargin);
1041
1042 Rect whitespaceRect;
1043 Rect outerRect;
1044 bool useDestRect = GetInsetBoxShadowRects(blurMargin, innerMargin, aShadowClipRect,
1045 aDestinationRect, whitespaceRect, outerRect);
1046
1047 RefPtr<SourceSurface> minBlur = GetInsetBlur(outerRect, whitespaceRect, useDestRect, aShadowColor,
1048 aBlurRadius, aHasBorderRadius, aInnerClipRadii,
1049 destDrawTarget);
1050 if (!minBlur) {
1051 return;
1052 }
1053
1054 if (useDestRect) {
1055 IntSize blurSize = minBlur->GetSize();
1056 Rect srcBlur(0, 0, blurSize.width, blurSize.height);
1057 Rect destBlur = aDestinationRect;
1058
1059 // The blur itself expands the rect by the blur margin, so we
1060 // have to mimic that here.
1061 destBlur.Inflate(blurMargin);
1062 MOZ_ASSERT(srcBlur.Size() == destBlur.Size());
1063 destDrawTarget->DrawSurface(minBlur, destBlur, srcBlur);
1064 } else {
1065 Rect srcOuter(outerRect);
1066 Rect srcInner(srcOuter);
1067 srcInner.Deflate(blurMargin); // The outer color fill
1068 srcInner.Deflate(innerMargin); // The inner whitespace
1069
1070 // The shadow clip rect already takes into account the spread radius
1071 Rect outerFillRect(aShadowClipRect);
1072 outerFillRect.Inflate(blurMargin);
1073 FillDestinationPath(aDestinationCtx, aDestinationRect, outerFillRect, aShadowColor, false, RectCornerRadii());
1074
1075 // Inflate once for the frame around the whitespace
1076 Rect destRect(aShadowClipRect);
1077 destRect.Inflate(blurMargin);
1078
1079 // Deflate for the blurred in white space
1080 Rect destInnerRect(aShadowClipRect);
1081 destInnerRect.Deflate(innerMargin);
1082
1083 DrawBoxShadows(*destDrawTarget, minBlur,
1084 destRect, destInnerRect,
1085 srcOuter, srcInner,
1086 aSkipRect);
1087 }
1088 }
1089