| /* |
| * Copyright 2011 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "include/core/SkAlphaType.h" |
| #include "include/core/SkBitmap.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkClipOp.h" |
| #include "include/core/SkColor.h" |
| #include "include/core/SkColorType.h" |
| #include "include/core/SkImageInfo.h" |
| #include "include/core/SkMatrix.h" |
| #include "include/core/SkPath.h" |
| #include "include/core/SkRRect.h" |
| #include "include/core/SkRect.h" |
| #include "include/core/SkRegion.h" |
| #include "include/core/SkScalar.h" |
| #include "include/core/SkTypes.h" |
| #include "include/private/base/SkTemplates.h" |
| #include "include/private/base/SkMalloc.h" |
| #include "include/utils/SkRandom.h" |
| #include "src/core/SkAAClip.h" |
| #include "src/core/SkMask.h" |
| #include "src/core/SkRasterClip.h" |
| #include "tests/Test.h" |
| |
| #include <cstdint> |
| #include <cstring> |
| #include <initializer_list> |
| #include <string> |
| |
| static bool operator==(const SkMask& a, const SkMask& b) { |
| if (a.fFormat != b.fFormat || a.fBounds != b.fBounds) { |
| return false; |
| } |
| if (!a.fImage && !b.fImage) { |
| return true; |
| } |
| if (!a.fImage || !b.fImage) { |
| return false; |
| } |
| |
| size_t wbytes = a.fBounds.width(); |
| switch (a.fFormat) { |
| case SkMask::kBW_Format: |
| wbytes = (wbytes + 7) >> 3; |
| break; |
| case SkMask::kA8_Format: |
| case SkMask::k3D_Format: |
| break; |
| case SkMask::kLCD16_Format: |
| wbytes <<= 1; |
| break; |
| case SkMask::kARGB32_Format: |
| wbytes <<= 2; |
| break; |
| default: |
| SkDEBUGFAIL("unknown mask format"); |
| return false; |
| } |
| |
| const int h = a.fBounds.height(); |
| const char* aptr = (const char*)a.fImage; |
| const char* bptr = (const char*)b.fImage; |
| for (int y = 0; y < h; ++y) { |
| if (0 != memcmp(aptr, bptr, wbytes)) { |
| return false; |
| } |
| aptr += wbytes; |
| bptr += wbytes; |
| } |
| return true; |
| } |
| |
| static void copyToMask(const SkRegion& rgn, SkMask* mask) { |
| mask->fFormat = SkMask::kA8_Format; |
| |
| if (rgn.isEmpty()) { |
| mask->fBounds.setEmpty(); |
| mask->fRowBytes = 0; |
| mask->fImage = nullptr; |
| return; |
| } |
| |
| mask->fBounds = rgn.getBounds(); |
| mask->fRowBytes = mask->fBounds.width(); |
| mask->fImage = SkMask::AllocImage(mask->computeImageSize()); |
| sk_bzero(mask->fImage, mask->computeImageSize()); |
| |
| SkImageInfo info = SkImageInfo::Make(mask->fBounds.width(), |
| mask->fBounds.height(), |
| kAlpha_8_SkColorType, |
| kPremul_SkAlphaType); |
| SkBitmap bitmap; |
| bitmap.installPixels(info, mask->fImage, mask->fRowBytes); |
| |
| // canvas expects its coordinate system to always be 0,0 in the top/left |
| // so we translate the rgn to match that before drawing into the mask. |
| // |
| SkRegion tmpRgn(rgn); |
| tmpRgn.translate(-rgn.getBounds().fLeft, -rgn.getBounds().fTop); |
| |
| SkCanvas canvas(bitmap); |
| canvas.clipRegion(tmpRgn); |
| canvas.drawColor(SK_ColorBLACK); |
| } |
| |
| static void copyToMask(const SkRasterClip& rc, SkMask* mask) { |
| if (rc.isBW()) { |
| copyToMask(rc.bwRgn(), mask); |
| } else { |
| rc.aaRgn().copyToMask(mask); |
| } |
| } |
| |
| static bool operator==(const SkRasterClip& a, const SkRasterClip& b) { |
| if (a.isEmpty() && b.isEmpty()) { |
| return true; |
| } else if (a.isEmpty() != b.isEmpty() || a.isBW() != b.isBW() || a.isRect() != b.isRect()) { |
| return false; |
| } |
| |
| SkMask mask0, mask1; |
| copyToMask(a, &mask0); |
| copyToMask(b, &mask1); |
| SkAutoMaskFreeImage free0(mask0.fImage); |
| SkAutoMaskFreeImage free1(mask1.fImage); |
| return mask0 == mask1; |
| } |
| |
| static SkIRect rand_rect(SkRandom& rand, int n) { |
| int x = rand.nextS() % n; |
| int y = rand.nextS() % n; |
| int w = rand.nextU() % n; |
| int h = rand.nextU() % n; |
| return SkIRect::MakeXYWH(x, y, w, h); |
| } |
| |
| static void make_rand_rgn(SkRegion* rgn, SkRandom& rand) { |
| int count = rand.nextU() % 20; |
| for (int i = 0; i < count; ++i) { |
| rgn->op(rand_rect(rand, 100), SkRegion::kXOR_Op); |
| } |
| } |
| |
| static bool operator==(const SkRegion& rgn, const SkAAClip& aaclip) { |
| SkMask mask0, mask1; |
| |
| copyToMask(rgn, &mask0); |
| aaclip.copyToMask(&mask1); |
| SkAutoMaskFreeImage free0(mask0.fImage); |
| SkAutoMaskFreeImage free1(mask1.fImage); |
| return mask0 == mask1; |
| } |
| |
| static bool equalsAAClip(const SkRegion& rgn) { |
| SkAAClip aaclip; |
| aaclip.setRegion(rgn); |
| return rgn == aaclip; |
| } |
| |
| static void setRgnToPath(SkRegion* rgn, const SkPath& path) { |
| SkIRect ir; |
| path.getBounds().round(&ir); |
| rgn->setPath(path, SkRegion(ir)); |
| } |
| |
| // aaclip.setRegion should create idential masks to the region |
| static void test_rgn(skiatest::Reporter* reporter) { |
| SkRandom rand; |
| for (int i = 0; i < 1000; i++) { |
| SkRegion rgn; |
| make_rand_rgn(&rgn, rand); |
| REPORTER_ASSERT(reporter, equalsAAClip(rgn)); |
| } |
| |
| { |
| SkRegion rgn; |
| SkPath path; |
| path.addCircle(0, 0, SkIntToScalar(30)); |
| setRgnToPath(&rgn, path); |
| REPORTER_ASSERT(reporter, equalsAAClip(rgn)); |
| |
| path.reset(); |
| path.moveTo(0, 0); |
| path.lineTo(SkIntToScalar(100), 0); |
| path.lineTo(SkIntToScalar(100 - 20), SkIntToScalar(20)); |
| path.lineTo(SkIntToScalar(20), SkIntToScalar(20)); |
| setRgnToPath(&rgn, path); |
| REPORTER_ASSERT(reporter, equalsAAClip(rgn)); |
| } |
| } |
| |
| static void imoveTo(SkPath& path, int x, int y) { |
| path.moveTo(SkIntToScalar(x), SkIntToScalar(y)); |
| } |
| |
| static void icubicTo(SkPath& path, int x0, int y0, int x1, int y1, int x2, int y2) { |
| path.cubicTo(SkIntToScalar(x0), SkIntToScalar(y0), |
| SkIntToScalar(x1), SkIntToScalar(y1), |
| SkIntToScalar(x2), SkIntToScalar(y2)); |
| } |
| |
| static void test_path_bounds(skiatest::Reporter* reporter) { |
| SkPath path; |
| SkAAClip clip; |
| const int height = 40; |
| const SkScalar sheight = SkIntToScalar(height); |
| |
| path.addOval(SkRect::MakeWH(sheight, sheight)); |
| REPORTER_ASSERT(reporter, sheight == path.getBounds().height()); |
| clip.setPath(path, path.getBounds().roundOut(), true); |
| REPORTER_ASSERT(reporter, height == clip.getBounds().height()); |
| |
| // this is the trimmed height of this cubic (with aa). The critical thing |
| // for this test is that it is less than height, which represents just |
| // the bounds of the path's control-points. |
| // |
| // This used to fail until we tracked the MinY in the BuilderBlitter. |
| // |
| const int teardrop_height = 12; |
| path.reset(); |
| imoveTo(path, 0, 20); |
| icubicTo(path, 40, 40, 40, 0, 0, 20); |
| REPORTER_ASSERT(reporter, sheight == path.getBounds().height()); |
| clip.setPath(path, path.getBounds().roundOut(), true); |
| REPORTER_ASSERT(reporter, teardrop_height == clip.getBounds().height()); |
| } |
| |
| static void test_empty(skiatest::Reporter* reporter) { |
| SkAAClip clip; |
| |
| REPORTER_ASSERT(reporter, clip.isEmpty()); |
| REPORTER_ASSERT(reporter, clip.getBounds().isEmpty()); |
| |
| clip.translate(10, 10, &clip); // should have no effect on empty |
| REPORTER_ASSERT(reporter, clip.isEmpty()); |
| REPORTER_ASSERT(reporter, clip.getBounds().isEmpty()); |
| |
| SkIRect r = { 10, 10, 40, 50 }; |
| clip.setRect(r); |
| REPORTER_ASSERT(reporter, !clip.isEmpty()); |
| REPORTER_ASSERT(reporter, !clip.getBounds().isEmpty()); |
| REPORTER_ASSERT(reporter, clip.getBounds() == r); |
| |
| clip.setEmpty(); |
| REPORTER_ASSERT(reporter, clip.isEmpty()); |
| REPORTER_ASSERT(reporter, clip.getBounds().isEmpty()); |
| |
| SkMask mask; |
| clip.copyToMask(&mask); |
| REPORTER_ASSERT(reporter, nullptr == mask.fImage); |
| REPORTER_ASSERT(reporter, mask.fBounds.isEmpty()); |
| } |
| |
| static void rand_irect(SkIRect* r, int N, SkRandom& rand) { |
| r->setXYWH(0, 0, rand.nextU() % N, rand.nextU() % N); |
| int dx = rand.nextU() % (2*N); |
| int dy = rand.nextU() % (2*N); |
| // use int dx,dy to make the subtract be signed |
| r->offset(N - dx, N - dy); |
| } |
| |
| static void test_irect(skiatest::Reporter* reporter) { |
| SkRandom rand; |
| |
| for (int i = 0; i < 10000; i++) { |
| SkAAClip clip0, clip1; |
| SkRegion rgn0, rgn1; |
| SkIRect r0, r1; |
| |
| rand_irect(&r0, 10, rand); |
| rand_irect(&r1, 10, rand); |
| clip0.setRect(r0); |
| clip1.setRect(r1); |
| rgn0.setRect(r0); |
| rgn1.setRect(r1); |
| for (SkClipOp op : {SkClipOp::kDifference, SkClipOp::kIntersect}) { |
| SkAAClip clip2 = clip0; // leave clip0 unchanged for future iterations |
| SkRegion rgn2; |
| bool nonEmptyAA = clip2.op(clip1, op); |
| bool nonEmptyBW = rgn2.op(rgn0, rgn1, (SkRegion::Op) op); |
| if (nonEmptyAA != nonEmptyBW || clip2.getBounds() != rgn2.getBounds()) { |
| ERRORF(reporter, "%s %s " |
| "[%d %d %d %d] %s [%d %d %d %d] = BW:[%d %d %d %d] AA:[%d %d %d %d]\n", |
| nonEmptyAA == nonEmptyBW ? "true" : "false", |
| clip2.getBounds() == rgn2.getBounds() ? "true" : "false", |
| r0.fLeft, r0.fTop, r0.right(), r0.bottom(), |
| op == SkClipOp::kDifference ? "DIFF" : "INTERSECT", |
| r1.fLeft, r1.fTop, r1.right(), r1.bottom(), |
| rgn2.getBounds().fLeft, rgn2.getBounds().fTop, |
| rgn2.getBounds().right(), rgn2.getBounds().bottom(), |
| clip2.getBounds().fLeft, clip2.getBounds().fTop, |
| clip2.getBounds().right(), clip2.getBounds().bottom()); |
| } |
| |
| SkMask maskBW, maskAA; |
| copyToMask(rgn2, &maskBW); |
| clip2.copyToMask(&maskAA); |
| SkAutoMaskFreeImage freeBW(maskBW.fImage); |
| SkAutoMaskFreeImage freeAA(maskAA.fImage); |
| REPORTER_ASSERT(reporter, maskBW == maskAA); |
| } |
| } |
| } |
| |
| static void test_path_with_hole(skiatest::Reporter* reporter) { |
| static const uint8_t gExpectedImage[] = { |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| 0xFF, 0xFF, 0xFF, 0xFF, |
| }; |
| SkMask expected; |
| expected.fBounds.setWH(4, 6); |
| expected.fRowBytes = 4; |
| expected.fFormat = SkMask::kA8_Format; |
| expected.fImage = (uint8_t*)gExpectedImage; |
| |
| SkPath path; |
| path.addRect(SkRect::MakeXYWH(0, 0, |
| SkIntToScalar(4), SkIntToScalar(2))); |
| path.addRect(SkRect::MakeXYWH(0, SkIntToScalar(4), |
| SkIntToScalar(4), SkIntToScalar(2))); |
| |
| for (int i = 0; i < 2; ++i) { |
| SkAAClip clip; |
| clip.setPath(path, path.getBounds().roundOut(), 1 == i); |
| |
| SkMask mask; |
| clip.copyToMask(&mask); |
| SkAutoMaskFreeImage freeM(mask.fImage); |
| |
| REPORTER_ASSERT(reporter, expected == mask); |
| } |
| } |
| |
| static void test_really_a_rect(skiatest::Reporter* reporter) { |
| SkRRect rrect; |
| rrect.setRectXY(SkRect::MakeWH(100, 100), 5, 5); |
| |
| SkPath path; |
| path.addRRect(rrect); |
| |
| SkAAClip clip; |
| clip.setPath(path, path.getBounds().roundOut(), true); |
| |
| REPORTER_ASSERT(reporter, clip.getBounds() == SkIRect::MakeWH(100, 100)); |
| REPORTER_ASSERT(reporter, !clip.isRect()); |
| |
| // This rect should intersect the clip, but slice-out all of the "soft" parts, |
| // leaving just a rect. |
| const SkIRect ir = SkIRect::MakeLTRB(10, -10, 50, 90); |
| |
| clip.op(ir, SkClipOp::kIntersect); |
| |
| REPORTER_ASSERT(reporter, clip.getBounds() == SkIRect::MakeLTRB(10, 0, 50, 90)); |
| // the clip recognized that that it is just a rect! |
| REPORTER_ASSERT(reporter, clip.isRect()); |
| } |
| |
| static void did_dx_affect(skiatest::Reporter* reporter, const SkScalar dx[], |
| size_t count, bool changed) { |
| SkIRect ir = { 0, 0, 10, 10 }; |
| |
| for (size_t i = 0; i < count; ++i) { |
| SkRect r; |
| r.set(ir); |
| |
| SkRasterClip rc0(ir); |
| SkRasterClip rc1(ir); |
| SkRasterClip rc2(ir); |
| |
| rc0.op(r, SkMatrix::I(), SkClipOp::kIntersect, false); |
| r.offset(dx[i], 0); |
| rc1.op(r, SkMatrix::I(), SkClipOp::kIntersect, true); |
| r.offset(-2*dx[i], 0); |
| rc2.op(r, SkMatrix::I(), SkClipOp::kIntersect, true); |
| |
| REPORTER_ASSERT(reporter, changed != (rc0 == rc1)); |
| REPORTER_ASSERT(reporter, changed != (rc0 == rc2)); |
| } |
| } |
| |
| static void test_nearly_integral(skiatest::Reporter* reporter) { |
| // All of these should generate equivalent rasterclips |
| |
| static const SkScalar gSafeX[] = { |
| 0, SK_Scalar1/1000, SK_Scalar1/100, SK_Scalar1/10, |
| }; |
| did_dx_affect(reporter, gSafeX, std::size(gSafeX), false); |
| |
| static const SkScalar gUnsafeX[] = { |
| SK_Scalar1/4, SK_Scalar1/3, |
| }; |
| did_dx_affect(reporter, gUnsafeX, std::size(gUnsafeX), true); |
| } |
| |
| static void test_regressions() { |
| // these should not assert in the debug build |
| // bug was introduced in rev. 3209 |
| { |
| SkAAClip clip; |
| SkRect r; |
| r.fLeft = 129.892181f; |
| r.fTop = 10.3999996f; |
| r.fRight = 130.892181f; |
| r.fBottom = 20.3999996f; |
| clip.setPath(SkPath::Rect(r), r.roundOut(), true); |
| } |
| } |
| |
| // Building aaclip meant aa-scan-convert a path into a huge clip. |
| // the old algorithm sized the supersampler to the size of the clip, which overflowed |
| // its internal 16bit coordinates. The fix was to intersect the clip+path_bounds before |
| // sizing the supersampler. |
| // |
| // Before the fix, the following code would assert in debug builds. |
| // |
| static void test_crbug_422693(skiatest::Reporter* reporter) { |
| SkRasterClip rc(SkIRect::MakeLTRB(-25000, -25000, 25000, 25000)); |
| SkPath path; |
| path.addCircle(50, 50, 50); |
| rc.op(path, SkMatrix::I(), SkClipOp::kIntersect, true); |
| } |
| |
| static void test_huge(skiatest::Reporter* reporter) { |
| SkAAClip clip; |
| int big = 0x70000000; |
| SkIRect r = { -big, -big, big, big }; |
| SkASSERT(r.width() < 0 && r.height() < 0); |
| |
| clip.setRect(r); |
| } |
| |
| DEF_TEST(AAClip, reporter) { |
| test_empty(reporter); |
| test_path_bounds(reporter); |
| test_irect(reporter); |
| test_rgn(reporter); |
| test_path_with_hole(reporter); |
| test_regressions(); |
| test_nearly_integral(reporter); |
| test_really_a_rect(reporter); |
| test_crbug_422693(reporter); |
| test_huge(reporter); |
| } |