1/* 2* Copyright 2012 Google Inc. 3* 4* Use of this source code is governed by a BSD-style license that can be 5* found in the LICENSE file. 6*/ 7 8#include <cmath> 9#include "gm/gm.h" 10#include "include/core/SkBitmap.h" 11#include "include/core/SkBlurTypes.h" 12#include "include/core/SkCanvas.h" 13#include "include/core/SkColor.h" 14#include "include/core/SkColorFilter.h" 15#include "include/core/SkImage.h" 16#include "include/core/SkMaskFilter.h" 17#include "include/core/SkMatrix.h" 18#include "include/core/SkPaint.h" 19#include "include/core/SkPathBuilder.h" 20#include "include/core/SkPoint.h" 21#include "include/core/SkRect.h" 22#include "include/core/SkRefCnt.h" 23#include "include/core/SkScalar.h" 24#include "include/core/SkShader.h" 25#include "include/core/SkSize.h" 26#include "include/core/SkString.h" 27#include "include/core/SkSurface.h" 28#include "include/core/SkTileMode.h" 29#include "include/core/SkTypes.h" 30#include "include/effects/SkGradientShader.h" 31#include "include/gpu/GrRecordingContext.h" 32#include "include/private/SkTo.h" 33#include "src/core/SkBlurMask.h" 34#include "src/core/SkMask.h" 35#include "src/gpu/GrRecordingContextPriv.h" 36#include "tools/timer/TimeUtils.h" 37 38#include <vector> 39 40#define STROKE_WIDTH SkIntToScalar(10) 41 42typedef void (*Proc)(SkCanvas*, const SkRect&, const SkPaint&); 43 44static void fill_rect(SkCanvas* canvas, const SkRect& r, const SkPaint& p) { 45 canvas->drawRect(r, p); 46} 47 48static void draw_donut(SkCanvas* canvas, const SkRect& r, const SkPaint& p) { 49 SkRect rect; 50 SkPathBuilder path; 51 52 rect = r; 53 rect.outset(STROKE_WIDTH/2, STROKE_WIDTH/2); 54 path.addRect(rect); 55 rect = r; 56 rect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2); 57 58 path.addRect(rect); 59 path.setFillType(SkPathFillType::kEvenOdd); 60 61 canvas->drawPath(path.detach(), p); 62} 63 64static void draw_donut_skewed(SkCanvas* canvas, const SkRect& r, const SkPaint& p) { 65 SkRect rect; 66 SkPathBuilder path; 67 68 rect = r; 69 rect.outset(STROKE_WIDTH/2, STROKE_WIDTH/2); 70 path.addRect(rect); 71 rect = r; 72 rect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2); 73 74 rect.offset(7, -7); 75 76 path.addRect(rect); 77 path.setFillType(SkPathFillType::kEvenOdd); 78 79 canvas->drawPath(path.detach(), p); 80} 81 82/* 83 * Spits out an arbitrary gradient to test blur with shader on paint 84 */ 85static sk_sp<SkShader> make_radial() { 86 SkPoint pts[2] = { 87 { 0, 0 }, 88 { SkIntToScalar(100), SkIntToScalar(100) } 89 }; 90 SkTileMode tm = SkTileMode::kClamp; 91 const SkColor colors[] = { SK_ColorRED, SK_ColorGREEN, }; 92 const SkScalar pos[] = { SK_Scalar1/4, SK_Scalar1*3/4 }; 93 SkMatrix scale; 94 scale.setScale(0.5f, 0.5f); 95 scale.postTranslate(25.f, 25.f); 96 SkPoint center0, center1; 97 center0.set(SkScalarAve(pts[0].fX, pts[1].fX), 98 SkScalarAve(pts[0].fY, pts[1].fY)); 99 center1.set(SkScalarInterp(pts[0].fX, pts[1].fX, SkIntToScalar(3)/5), 100 SkScalarInterp(pts[0].fY, pts[1].fY, SkIntToScalar(1)/4)); 101 return SkGradientShader::MakeTwoPointConical(center1, (pts[1].fX - pts[0].fX) / 7, 102 center0, (pts[1].fX - pts[0].fX) / 2, 103 colors, pos, SK_ARRAY_COUNT(colors), tm, 104 0, &scale); 105} 106 107typedef void (*PaintProc)(SkPaint*, SkScalar width); 108 109class BlurRectGM : public skiagm::GM { 110public: 111 BlurRectGM(const char name[], U8CPU alpha) : fName(name), fAlpha(SkToU8(alpha)) {} 112 113private: 114 sk_sp<SkMaskFilter> fMaskFilters[kLastEnum_SkBlurStyle + 1]; 115 const char* fName; 116 SkAlpha fAlpha; 117 118 void onOnceBeforeDraw() override { 119 for (int i = 0; i <= kLastEnum_SkBlurStyle; ++i) { 120 fMaskFilters[i] = SkMaskFilter::MakeBlur((SkBlurStyle)i, 121 SkBlurMask::ConvertRadiusToSigma(SkIntToScalar(STROKE_WIDTH/2))); 122 } 123 } 124 125 SkString onShortName() override { return SkString(fName); } 126 127 SkISize onISize() override { return {860, 820}; } 128 129 void onDraw(SkCanvas* canvas) override { 130 canvas->translate(STROKE_WIDTH*3/2, STROKE_WIDTH*3/2); 131 132 SkRect r = { 0, 0, 100, 50 }; 133 SkScalar scales[] = { SK_Scalar1, 0.6f }; 134 135 for (size_t s = 0; s < SK_ARRAY_COUNT(scales); ++s) { 136 canvas->save(); 137 for (size_t f = 0; f < SK_ARRAY_COUNT(fMaskFilters); ++f) { 138 SkPaint paint; 139 paint.setMaskFilter(fMaskFilters[f]); 140 paint.setAlpha(fAlpha); 141 142 SkPaint paintWithRadial = paint; 143 paintWithRadial.setShader(make_radial()); 144 145 constexpr Proc procs[] = { 146 fill_rect, draw_donut, draw_donut_skewed 147 }; 148 149 canvas->save(); 150 canvas->scale(scales[s], scales[s]); 151 this->drawProcs(canvas, r, paint, false, procs, SK_ARRAY_COUNT(procs)); 152 canvas->translate(r.width() * 4/3, 0); 153 this->drawProcs(canvas, r, paintWithRadial, false, procs, SK_ARRAY_COUNT(procs)); 154 canvas->translate(r.width() * 4/3, 0); 155 this->drawProcs(canvas, r, paint, true, procs, SK_ARRAY_COUNT(procs)); 156 canvas->translate(r.width() * 4/3, 0); 157 this->drawProcs(canvas, r, paintWithRadial, true, procs, SK_ARRAY_COUNT(procs)); 158 canvas->restore(); 159 160 canvas->translate(0, SK_ARRAY_COUNT(procs) * r.height() * 4/3 * scales[s]); 161 } 162 canvas->restore(); 163 canvas->translate(4 * r.width() * 4/3 * scales[s], 0); 164 } 165 } 166 167 void drawProcs(SkCanvas* canvas, const SkRect& r, const SkPaint& paint, 168 bool doClip, const Proc procs[], size_t procsCount) { 169 SkAutoCanvasRestore acr(canvas, true); 170 for (size_t i = 0; i < procsCount; ++i) { 171 if (doClip) { 172 SkRect clipRect(r); 173 clipRect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2); 174 canvas->save(); 175 canvas->clipRect(r); 176 } 177 procs[i](canvas, r, paint); 178 if (doClip) { 179 canvas->restore(); 180 } 181 canvas->translate(0, r.height() * 4/3); 182 } 183 } 184}; 185 186DEF_SIMPLE_GM(blurrect_gallery, canvas, 1200, 1024) { 187 const int fGMWidth = 1200; 188 const int fPadding = 10; 189 const int fMargin = 100; 190 191 const int widths[] = {25, 5, 5, 100, 150, 25}; 192 const int heights[] = {100, 100, 5, 25, 150, 25}; 193 const SkBlurStyle styles[] = {kNormal_SkBlurStyle, kInner_SkBlurStyle, kOuter_SkBlurStyle}; 194 const float radii[] = {20, 5, 10}; 195 196 canvas->translate(50,20); 197 198 int cur_x = 0; 199 int cur_y = 0; 200 201 int max_height = 0; 202 203 for (size_t i = 0 ; i < SK_ARRAY_COUNT(widths) ; i++) { 204 int width = widths[i]; 205 int height = heights[i]; 206 SkRect r; 207 r.setWH(SkIntToScalar(width), SkIntToScalar(height)); 208 SkAutoCanvasRestore autoRestore(canvas, true); 209 210 for (size_t j = 0 ; j < SK_ARRAY_COUNT(radii) ; j++) { 211 float radius = radii[j]; 212 for (size_t k = 0 ; k < SK_ARRAY_COUNT(styles) ; k++) { 213 SkBlurStyle style = styles[k]; 214 215 SkMask mask; 216 if (!SkBlurMask::BlurRect(SkBlurMask::ConvertRadiusToSigma(radius), 217 &mask, r, style)) { 218 continue; 219 } 220 221 SkAutoMaskFreeImage amfi(mask.fImage); 222 223 SkBitmap bm; 224 bm.installMaskPixels(mask); 225 226 if (cur_x + bm.width() >= fGMWidth - fMargin) { 227 cur_x = 0; 228 cur_y += max_height + fPadding; 229 max_height = 0; 230 } 231 232 canvas->save(); 233 canvas->translate((SkScalar)cur_x, (SkScalar)cur_y); 234 canvas->translate(-(bm.width() - r.width())/2, -(bm.height()-r.height())/2); 235 canvas->drawImage(bm.asImage(), 0.f, 0.f); 236 canvas->restore(); 237 238 cur_x += bm.width() + fPadding; 239 if (bm.height() > max_height) 240 max_height = bm.height(); 241 } 242 } 243 } 244} 245 246namespace skiagm { 247 248// Compares actual blur rects with reference masks created by the GM. Animates sigma in viewer. 249class BlurRectCompareGM : public GM { 250protected: 251 SkString onShortName() override { return SkString("blurrect_compare"); } 252 253 SkISize onISize() override { return {900, 1220}; } 254 255 void onOnceBeforeDraw() override { this->prepareReferenceMasks(); } 256 257 DrawResult onDraw(SkCanvas* canvas, SkString* errorMsg) override { 258 if (canvas->imageInfo().colorType() == kUnknown_SkColorType || 259 (canvas->recordingContext() && !canvas->recordingContext()->asDirectContext())) { 260 *errorMsg = "Not supported when recording, relies on canvas->makeSurface()"; 261 return DrawResult::kSkip; 262 } 263 int32_t ctxID = canvas->recordingContext() ? canvas->recordingContext()->priv().contextID() 264 : 0; 265 if (fRecalcMasksForAnimation || !fActualMasks[0][0][0] || ctxID != fLastContextUniqueID) { 266 if (fRecalcMasksForAnimation) { 267 // Sigma is changing so references must also be recalculated. 268 this->prepareReferenceMasks(); 269 } 270 this->prepareActualMasks(canvas); 271 this->prepareMaskDifferences(canvas); 272 fLastContextUniqueID = ctxID; 273 fRecalcMasksForAnimation = false; 274 } 275 canvas->clear(SK_ColorBLACK); 276 static constexpr float kMargin = 30; 277 float totalW = 0; 278 for (auto w : kSizes) { 279 totalW += w + kMargin; 280 } 281 canvas->translate(kMargin, kMargin); 282 for (int mode = 0; mode < 3; ++mode) { 283 canvas->save(); 284 for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) { 285 auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost; 286 for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) { 287 auto h = kSizes[heightIdx]; 288 canvas->save(); 289 for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) { 290 auto w = kSizes[widthIdx]; 291 SkPaint paint; 292 paint.setColor(SK_ColorWHITE); 293 SkImage* img; 294 switch (mode) { 295 case 0: 296 img = fReferenceMasks[sigmaIdx][heightIdx][widthIdx].get(); 297 break; 298 case 1: 299 img = fActualMasks[sigmaIdx][heightIdx][widthIdx].get(); 300 break; 301 case 2: 302 img = fMaskDifferences[sigmaIdx][heightIdx][widthIdx].get(); 303 // The error images are opaque, use kPlus so they are additive if 304 // the overlap between test cases. 305 paint.setBlendMode(SkBlendMode::kPlus); 306 break; 307 } 308 auto pad = PadForSigma(sigma); 309 canvas->drawImage(img, -pad, -pad, SkSamplingOptions(), &paint); 310#if 0 // Uncomment to hairline stroke around blurred rect in red on top of the blur result. 311 // The rect is defined at integer coords. We inset by 1/2 pixel so our stroke lies on top 312 // of the edge pixels. 313 SkPaint stroke; 314 stroke.setColor(SK_ColorRED); 315 stroke.setStrokeWidth(0.f); 316 stroke.setStyle(SkPaint::kStroke_Style); 317 canvas->drawRect(SkRect::MakeWH(w, h).makeInset(0.5, 0.5), stroke); 318#endif 319 canvas->translate(w + kMargin, 0.f); 320 } 321 canvas->restore(); 322 canvas->translate(0, h + kMargin); 323 } 324 } 325 canvas->restore(); 326 canvas->translate(totalW + 2 * kMargin, 0); 327 } 328 return DrawResult::kOk; 329 } 330 bool onAnimate(double nanos) override { 331 fSigmaAnimationBoost = TimeUtils::SineWave(nanos, 5, 2.5f, 0.f, 2.f); 332 fRecalcMasksForAnimation = true; 333 return true; 334 } 335 336private: 337 void prepareReferenceMasks() { 338 auto create_reference_mask = [](int w, int h, float sigma, int numSubpixels) { 339 int pad = PadForSigma(sigma); 340 int maskW = w + 2 * pad; 341 int maskH = h + 2 * pad; 342 // We'll do all our calculations at subpixel resolution, so adjust params 343 w *= numSubpixels; 344 h *= numSubpixels; 345 sigma *= numSubpixels; 346 auto scale = SK_ScalarRoot2Over2 / sigma; 347 auto def_integral_approx = [scale](float a, float b) { 348 return 0.5f * (std::erf(b * scale) - std::erf(a * scale)); 349 }; 350 // Do the x-pass. Above/below rect are rows of zero. All rows that intersect the rect 351 // are the same. The row is calculated and stored at subpixel resolution. 352 SkASSERT(!(numSubpixels & 0b1)); 353 std::unique_ptr<float[]> row(new float[maskW * numSubpixels]); 354 for (int col = 0; col < maskW * numSubpixels; ++col) { 355 // Compute distance to rect left in subpixel units 356 float ldiff = numSubpixels * pad - (col + 0.5f); 357 float rdiff = ldiff + w; 358 row[col] = def_integral_approx(ldiff, rdiff); 359 } 360 // y-pass 361 SkBitmap bmp; 362 bmp.allocPixels(SkImageInfo::MakeA8(maskW, maskH)); 363 std::unique_ptr<float[]> accums(new float[maskW]); 364 const float accumScale = 1.f / (numSubpixels * numSubpixels); 365 for (int y = 0; y < maskH; ++y) { 366 // Initialize subpixel accumulation buffer for this row. 367 std::fill_n(accums.get(), maskW, 0); 368 for (int ys = 0; ys < numSubpixels; ++ys) { 369 // At each subpixel we want to integrate over the kernel centered at the 370 // subpixel multiplied by the x-pass. The x-pass is zero above and below the 371 // rect and constant valued from rect top to rect bottom. So we can get the 372 // integral of just the kernel from rect top to rect bottom and multiply by 373 // the single x-pass value from our precomputed row. 374 float tdiff = numSubpixels * pad - (y * numSubpixels + ys + 0.5f); 375 float bdiff = tdiff + h; 376 auto integral = def_integral_approx(tdiff, bdiff); 377 for (int x = 0; x < maskW; ++x) { 378 for (int xs = 0; xs < numSubpixels; ++xs) { 379 int rowIdx = x * numSubpixels + xs; 380 accums[x] += integral * row[rowIdx]; 381 } 382 } 383 } 384 for (int x = 0; x < maskW; ++x) { 385 auto result = accums[x] * accumScale; 386 *bmp.getAddr8(x, y) = SkToU8(sk_float_round2int(255.f * result)); 387 } 388 } 389 return bmp.asImage(); 390 }; 391 392 // Number of times to subsample (in both X and Y). If fRecalcMasksForAnimation is true 393 // then we're animating, don't subsample as much to keep fps higher. 394 const int numSubpixels = fRecalcMasksForAnimation ? 2 : 8; 395 396 for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) { 397 auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost; 398 for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) { 399 auto h = kSizes[heightIdx]; 400 for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) { 401 auto w = kSizes[widthIdx]; 402 fReferenceMasks[sigmaIdx][heightIdx][widthIdx] = 403 create_reference_mask(w, h, sigma, numSubpixels); 404 } 405 } 406 } 407 } 408 409 void prepareActualMasks(SkCanvas* canvas) { 410 for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) { 411 auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost; 412 for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) { 413 auto h = kSizes[heightIdx]; 414 for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) { 415 auto w = kSizes[widthIdx]; 416 auto pad = PadForSigma(sigma); 417 auto ii = SkImageInfo::MakeA8(w + 2 * pad, h + 2 * pad); 418 auto surf = canvas->makeSurface(ii); 419 if (!surf) { 420 // Some GPUs don't have renderable A8 :( 421 surf = canvas->makeSurface(ii.makeColorType(kRGBA_8888_SkColorType)); 422 if (!surf) { 423 return; 424 } 425 } 426 auto rect = SkRect::MakeXYWH(pad, pad, w, h); 427 SkPaint paint; 428 // Color doesn't matter if we're rendering to A8 but does if we promoted to 429 // RGBA above. 430 paint.setColor(SK_ColorWHITE); 431 paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, sigma)); 432 surf->getCanvas()->drawRect(rect, paint); 433 fActualMasks[sigmaIdx][heightIdx][widthIdx] = surf->makeImageSnapshot(); 434 } 435 } 436 } 437 } 438 439 void prepareMaskDifferences(SkCanvas* canvas) { 440 for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) { 441 for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) { 442 for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) { 443 const auto& r = fReferenceMasks[sigmaIdx][heightIdx][widthIdx]; 444 const auto& a = fActualMasks[sigmaIdx][heightIdx][widthIdx]; 445 auto& d = fMaskDifferences[sigmaIdx][heightIdx][widthIdx]; 446 // The actual image might not be present if we're on an abandoned GrContext. 447 if (!a) { 448 d.reset(); 449 continue; 450 } 451 SkASSERT(r->width() == a->width()); 452 SkASSERT(r->height() == a->height()); 453 auto ii = SkImageInfo::Make(r->width(), r->height(), 454 kRGBA_8888_SkColorType, kPremul_SkAlphaType); 455 auto surf = canvas->makeSurface(ii); 456 if (!surf) { 457 return; 458 } 459 // We visualize the difference by turning both the alpha masks into opaque green 460 // images (where alpha becomes the green channel) and then perform a 461 // SkBlendMode::kDifference between them. 462 SkPaint filterPaint; 463 filterPaint.setColor(SK_ColorWHITE); 464 // Actually 8 * alpha becomes green to really highlight differences. 465 static constexpr float kGreenifyM[] = {0, 0, 0, 0, 0, 466 0, 0, 0, 8, 0, 467 0, 0, 0, 0, 0, 468 0, 0, 0, 0, 1}; 469 auto greenifyCF = SkColorFilters::Matrix(kGreenifyM); 470 SkPaint paint; 471 paint.setBlendMode(SkBlendMode::kSrc); 472 paint.setColorFilter(std::move(greenifyCF)); 473 surf->getCanvas()->drawImage(a, 0, 0, SkSamplingOptions(), &paint); 474 paint.setBlendMode(SkBlendMode::kDifference); 475 surf->getCanvas()->drawImage(r, 0, 0, SkSamplingOptions(), &paint); 476 d = surf->makeImageSnapshot(); 477 } 478 } 479 } 480 } 481 482 // Per side padding around mask images for a sigma. Make this overly generous to ensure bugs 483 // related to big blurs are fully visible. 484 static int PadForSigma(float sigma) { return sk_float_ceil2int(4 * sigma); } 485 486 inline static constexpr int kSizes[] = {1, 2, 4, 8, 16, 32}; 487 inline static constexpr float kSigmas[] = {0.5f, 1.2f, 2.3f, 3.9f, 7.4f}; 488 inline static constexpr size_t kNumSizes = SK_ARRAY_COUNT(kSizes); 489 inline static constexpr size_t kNumSigmas = SK_ARRAY_COUNT(kSigmas); 490 491 sk_sp<SkImage> fReferenceMasks[kNumSigmas][kNumSizes][kNumSizes]; 492 sk_sp<SkImage> fActualMasks[kNumSigmas][kNumSizes][kNumSizes]; 493 sk_sp<SkImage> fMaskDifferences[kNumSigmas][kNumSizes][kNumSizes]; 494 int32_t fLastContextUniqueID; 495 // These are used only when animating. 496 float fSigmaAnimationBoost = 0; 497 bool fRecalcMasksForAnimation = false; 498}; 499 500} // namespace skiagm 501 502////////////////////////////////////////////////////////////////////////////// 503 504DEF_GM(return new BlurRectGM("blurrects", 0xFF);) 505DEF_GM(return new skiagm::BlurRectCompareGM();) 506 507////////////////////////////////////////////////////////////////////////////// 508 509DEF_SIMPLE_GM(blur_matrix_rect, canvas, 650, 685) { 510 static constexpr auto kRect = SkRect::MakeWH(14, 60); 511 static constexpr float kSigmas[] = {0.5f, 1.2f, 2.3f, 3.9f, 7.4f}; 512 static constexpr size_t kNumSigmas = SK_ARRAY_COUNT(kSigmas); 513 514 const SkPoint c = {kRect.centerX(), kRect.centerY()}; 515 516 std::vector<SkMatrix> matrices; 517 518 matrices.push_back(SkMatrix::RotateDeg(4.f, c)); 519 520 matrices.push_back(SkMatrix::RotateDeg(63.f, c)); 521 522 matrices.push_back(SkMatrix::RotateDeg(30.f, c)); 523 matrices.back().preScale(1.1f, .5f); 524 525 matrices.push_back(SkMatrix::RotateDeg(147.f, c)); 526 matrices.back().preScale(3.f, .1f); 527 528 SkMatrix mirror; 529 mirror.setAll(0, 1, 0, 530 1, 0, 0, 531 0, 0, 1); 532 matrices.push_back(SkMatrix::Concat(mirror, matrices.back())); 533 534 matrices.push_back(SkMatrix::RotateDeg(197.f, c)); 535 matrices.back().preSkew(.3f, -.5f); 536 537 auto bounds = SkRect::MakeEmpty(); 538 for (const auto& m : matrices) { 539 SkRect mapped; 540 m.mapRect(&mapped, kRect); 541 bounds.joinNonEmptyArg(mapped.makeSorted()); 542 } 543 float blurPad = 2.f*kSigmas[kNumSigmas - 1]; 544 bounds.outset(blurPad, blurPad); 545 canvas->translate(-bounds.left(), -bounds.top()); 546 for (auto sigma : kSigmas) { 547 SkPaint paint; 548 paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, sigma)); 549 canvas->save(); 550 for (const auto& m : matrices) { 551 canvas->save(); 552 canvas->concat(m); 553 canvas->drawRect(kRect, paint); 554 canvas->restore(); 555 canvas->translate(0, bounds.height()); 556 } 557 canvas->restore(); 558 canvas->translate(bounds.width(), 0); 559 } 560} 561