PictureRenderer.cpp revision a9e3a369c18c6d5f41724e837e3ba0fa87d8c559
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 "PictureRenderer.h" 9#include "picture_utils.h" 10#include "SamplePipeControllers.h" 11#include "SkCanvas.h" 12#include "SkDevice.h" 13#include "SkGPipe.h" 14#if SK_SUPPORT_GPU 15#include "SkGpuDevice.h" 16#endif 17#include "SkGraphics.h" 18#include "SkImageEncoder.h" 19#include "SkMaskFilter.h" 20#include "SkMatrix.h" 21#include "SkPicture.h" 22#include "SkRTree.h" 23#include "SkScalar.h" 24#include "SkStream.h" 25#include "SkString.h" 26#include "SkTemplates.h" 27#include "SkTileGrid.h" 28#include "SkTDArray.h" 29#include "SkThreadUtils.h" 30#include "SkTypes.h" 31 32namespace sk_tools { 33 34enum { 35 kDefaultTileWidth = 256, 36 kDefaultTileHeight = 256 37}; 38 39void PictureRenderer::init(SkPicture* pict) { 40 SkASSERT(NULL == fPicture); 41 SkASSERT(NULL == fCanvas.get()); 42 if (fPicture != NULL || NULL != fCanvas.get()) { 43 return; 44 } 45 46 SkASSERT(pict != NULL); 47 if (NULL == pict) { 48 return; 49 } 50 51 fPicture = pict; 52 fPicture->ref(); 53 fCanvas.reset(this->setupCanvas()); 54} 55 56class FlagsDrawFilter : public SkDrawFilter { 57public: 58 FlagsDrawFilter(PictureRenderer::DrawFilterFlags* flags) : 59 fFlags(flags) {} 60 61 virtual void filter(SkPaint* paint, Type t) { 62 paint->setFlags(paint->getFlags() & ~fFlags[t] & SkPaint::kAllFlags); 63 if ((PictureRenderer::kBlur_DrawFilterFlag | PictureRenderer::kLowBlur_DrawFilterFlag) 64 & fFlags[t]) { 65 SkMaskFilter* maskFilter = paint->getMaskFilter(); 66 SkMaskFilter::BlurInfo blurInfo; 67 if (maskFilter && maskFilter->asABlur(&blurInfo)) { 68 if (PictureRenderer::kBlur_DrawFilterFlag & fFlags[t]) { 69 paint->setMaskFilter(NULL); 70 } else { 71 blurInfo.fHighQuality = false; 72 maskFilter->setAsABlur(blurInfo); 73 } 74 } 75 } 76 if (PictureRenderer::kHinting_DrawFilterFlag & fFlags[t]) { 77 paint->setHinting(SkPaint::kNo_Hinting); 78 } else if (PictureRenderer::kSlightHinting_DrawFilterFlag & fFlags[t]) { 79 paint->setHinting(SkPaint::kSlight_Hinting); 80 } 81 } 82 83private: 84 PictureRenderer::DrawFilterFlags* fFlags; 85}; 86 87static SkCanvas* setUpFilter(SkCanvas* canvas, PictureRenderer::DrawFilterFlags* drawFilters) { 88 if (drawFilters && !canvas->getDrawFilter()) { 89 canvas->setDrawFilter(SkNEW_ARGS(FlagsDrawFilter, (drawFilters)))->unref(); 90 if (drawFilters[0] & PictureRenderer::kAAClip_DrawFilterFlag) { 91 canvas->setAllowSoftClip(false); 92 } 93 } 94 return canvas; 95} 96 97SkCanvas* PictureRenderer::setupCanvas() { 98 return this->setupCanvas(fPicture->width(), fPicture->height()); 99} 100 101SkCanvas* PictureRenderer::setupCanvas(int width, int height) { 102 SkCanvas* canvas; 103 switch(fDeviceType) { 104 case kBitmap_DeviceType: { 105 SkBitmap bitmap; 106 sk_tools::setup_bitmap(&bitmap, width, height); 107 canvas = SkNEW_ARGS(SkCanvas, (bitmap)); 108 return setUpFilter(canvas, fDrawFilters); 109 } 110#if SK_SUPPORT_GPU 111 case kGPU_DeviceType: { 112 SkAutoTUnref<SkGpuDevice> device(SkNEW_ARGS(SkGpuDevice, 113 (fGrContext, SkBitmap::kARGB_8888_Config, 114 width, height))); 115 canvas = SkNEW_ARGS(SkCanvas, (device.get())); 116 return setUpFilter(canvas, fDrawFilters); 117 } 118#endif 119 default: 120 SkASSERT(0); 121 } 122 123 return NULL; 124} 125 126void PictureRenderer::end() { 127 this->resetState(); 128 SkSafeUnref(fPicture); 129 fPicture = NULL; 130 fCanvas.reset(NULL); 131} 132 133/** Converts fPicture to a picture that uses a BBoxHierarchy. 134 * PictureRenderer subclasses that are used to test picture playback 135 * should call this method during init. 136 */ 137void PictureRenderer::buildBBoxHierarchy() { 138 SkASSERT(NULL != fPicture); 139 if (kNone_BBoxHierarchyType != fBBoxHierarchyType && NULL != fPicture) { 140 SkPicture* newPicture = this->createPicture(); 141 SkCanvas* recorder = newPicture->beginRecording(fPicture->width(), fPicture->height(), 142 this->recordFlags()); 143 fPicture->draw(recorder); 144 newPicture->endRecording(); 145 fPicture->unref(); 146 fPicture = newPicture; 147 } 148} 149 150void PictureRenderer::resetState() { 151#if SK_SUPPORT_GPU 152 if (this->isUsingGpuDevice()) { 153 SkGLContext* glContext = fGrContextFactory.getGLContext( 154 GrContextFactory::kNative_GLContextType); 155 156 SkASSERT(glContext != NULL); 157 if (NULL == glContext) { 158 return; 159 } 160 161 fGrContext->flush(); 162 SK_GL(*glContext, Finish()); 163 } 164#endif 165} 166 167uint32_t PictureRenderer::recordFlags() { 168 return kNone_BBoxHierarchyType == fBBoxHierarchyType ? 0 : 169 SkPicture::kOptimizeForClippedPlayback_RecordingFlag; 170} 171 172/** 173 * Write the canvas to the specified path. 174 * @param canvas Must be non-null. Canvas to be written to a file. 175 * @param path Path for the file to be written. Should have no extension; write() will append 176 * an appropriate one. Passed in by value so it can be modified. 177 * @return bool True if the Canvas is written to a file. 178 */ 179static bool write(SkCanvas* canvas, SkString path) { 180 SkASSERT(canvas != NULL); 181 if (NULL == canvas) { 182 return false; 183 } 184 185 SkBitmap bitmap; 186 SkISize size = canvas->getDeviceSize(); 187 sk_tools::setup_bitmap(&bitmap, size.width(), size.height()); 188 189 canvas->readPixels(&bitmap, 0, 0); 190 sk_tools::force_all_opaque(bitmap); 191 192 // Since path is passed in by value, it is okay to modify it. 193 path.append(".png"); 194 return SkImageEncoder::EncodeFile(path.c_str(), bitmap, SkImageEncoder::kPNG_Type, 100); 195} 196 197/** 198 * If path is non NULL, append number to it, and call write(SkCanvas*, SkString) to write the 199 * provided canvas to a file. Returns true if path is NULL or if write() succeeds. 200 */ 201static bool writeAppendNumber(SkCanvas* canvas, const SkString* path, int number) { 202 if (NULL == path) { 203 return true; 204 } 205 SkString pathWithNumber(*path); 206 pathWithNumber.appendf("%i", number); 207 return write(canvas, pathWithNumber); 208} 209 210/////////////////////////////////////////////////////////////////////////////////////////////// 211 212SkCanvas* RecordPictureRenderer::setupCanvas(int width, int height) { 213 // defer the canvas setup until the render step 214 return NULL; 215} 216 217static bool PNGEncodeBitmapToStream(SkWStream* wStream, const SkBitmap& bm) { 218 return SkImageEncoder::EncodeStream(wStream, bm, SkImageEncoder::kPNG_Type, 100); 219} 220 221bool RecordPictureRenderer::render(const SkString* path) { 222 SkAutoTUnref<SkPicture> replayer(this->createPicture()); 223 SkCanvas* recorder = replayer->beginRecording(fPicture->width(), fPicture->height(), 224 this->recordFlags()); 225 fPicture->draw(recorder); 226 replayer->endRecording(); 227 if (path != NULL) { 228 // Record the new picture as a new SKP with PNG encoded bitmaps. 229 SkString skpPath(*path); 230 // ".skp" was removed from 'path' before being passed in here. 231 skpPath.append(".skp"); 232 SkFILEWStream stream(skpPath.c_str()); 233 replayer->serialize(&stream, &PNGEncodeBitmapToStream); 234 return true; 235 } 236 return false; 237} 238 239SkString RecordPictureRenderer::getConfigNameInternal() { 240 return SkString("record"); 241} 242 243/////////////////////////////////////////////////////////////////////////////////////////////// 244 245bool PipePictureRenderer::render(const SkString* path) { 246 SkASSERT(fCanvas.get() != NULL); 247 SkASSERT(fPicture != NULL); 248 if (NULL == fCanvas.get() || NULL == fPicture) { 249 return false; 250 } 251 252 PipeController pipeController(fCanvas.get()); 253 SkGPipeWriter writer; 254 SkCanvas* pipeCanvas = writer.startRecording(&pipeController); 255 pipeCanvas->drawPicture(*fPicture); 256 writer.endRecording(); 257 fCanvas->flush(); 258 if (NULL != path) { 259 return write(fCanvas, *path); 260 } 261 return true; 262} 263 264SkString PipePictureRenderer::getConfigNameInternal() { 265 return SkString("pipe"); 266} 267 268/////////////////////////////////////////////////////////////////////////////////////////////// 269 270void SimplePictureRenderer::init(SkPicture* picture) { 271 INHERITED::init(picture); 272 this->buildBBoxHierarchy(); 273} 274 275bool SimplePictureRenderer::render(const SkString* path) { 276 SkASSERT(fCanvas.get() != NULL); 277 SkASSERT(fPicture != NULL); 278 if (NULL == fCanvas.get() || NULL == fPicture) { 279 return false; 280 } 281 282 fCanvas->drawPicture(*fPicture); 283 fCanvas->flush(); 284 if (NULL != path) { 285 return write(fCanvas, *path); 286 } 287 return true; 288} 289 290SkString SimplePictureRenderer::getConfigNameInternal() { 291 return SkString("simple"); 292} 293 294/////////////////////////////////////////////////////////////////////////////////////////////// 295 296TiledPictureRenderer::TiledPictureRenderer() 297 : fTileWidth(kDefaultTileWidth) 298 , fTileHeight(kDefaultTileHeight) 299 , fTileWidthPercentage(0.0) 300 , fTileHeightPercentage(0.0) 301 , fTileMinPowerOf2Width(0) { } 302 303void TiledPictureRenderer::init(SkPicture* pict) { 304 SkASSERT(pict != NULL); 305 SkASSERT(0 == fTileRects.count()); 306 if (NULL == pict || fTileRects.count() != 0) { 307 return; 308 } 309 310 // Do not call INHERITED::init(), which would create a (potentially large) canvas which is not 311 // used by bench_pictures. 312 fPicture = pict; 313 fPicture->ref(); 314 this->buildBBoxHierarchy(); 315 316 if (fTileWidthPercentage > 0) { 317 fTileWidth = sk_float_ceil2int(float(fTileWidthPercentage * fPicture->width() / 100)); 318 } 319 if (fTileHeightPercentage > 0) { 320 fTileHeight = sk_float_ceil2int(float(fTileHeightPercentage * fPicture->height() / 100)); 321 } 322 323 if (fTileMinPowerOf2Width > 0) { 324 this->setupPowerOf2Tiles(); 325 } else { 326 this->setupTiles(); 327 } 328} 329 330void TiledPictureRenderer::end() { 331 fTileRects.reset(); 332 this->INHERITED::end(); 333} 334 335void TiledPictureRenderer::setupTiles() { 336 for (int tile_y_start = 0; tile_y_start < fPicture->height(); tile_y_start += fTileHeight) { 337 for (int tile_x_start = 0; tile_x_start < fPicture->width(); tile_x_start += fTileWidth) { 338 *fTileRects.append() = SkRect::MakeXYWH(SkIntToScalar(tile_x_start), 339 SkIntToScalar(tile_y_start), 340 SkIntToScalar(fTileWidth), 341 SkIntToScalar(fTileHeight)); 342 } 343 } 344} 345 346// The goal of the powers of two tiles is to minimize the amount of wasted tile 347// space in the width-wise direction and then minimize the number of tiles. The 348// constraints are that every tile must have a pixel width that is a power of 349// two and also be of some minimal width (that is also a power of two). 350// 351// This is solved by first taking our picture size and rounding it up to the 352// multiple of the minimal width. The binary representation of this rounded 353// value gives us the tiles we need: a bit of value one means we need a tile of 354// that size. 355void TiledPictureRenderer::setupPowerOf2Tiles() { 356 int rounded_value = fPicture->width(); 357 if (fPicture->width() % fTileMinPowerOf2Width != 0) { 358 rounded_value = fPicture->width() - (fPicture->width() % fTileMinPowerOf2Width) 359 + fTileMinPowerOf2Width; 360 } 361 362 int num_bits = SkScalarCeilToInt(SkScalarLog2(SkIntToScalar(fPicture->width()))); 363 int largest_possible_tile_size = 1 << num_bits; 364 365 // The tile height is constant for a particular picture. 366 for (int tile_y_start = 0; tile_y_start < fPicture->height(); tile_y_start += fTileHeight) { 367 int tile_x_start = 0; 368 int current_width = largest_possible_tile_size; 369 // Set fTileWidth to be the width of the widest tile, so that each canvas is large enough 370 // to draw each tile. 371 fTileWidth = current_width; 372 373 while (current_width >= fTileMinPowerOf2Width) { 374 // It is very important this is a bitwise AND. 375 if (current_width & rounded_value) { 376 *fTileRects.append() = SkRect::MakeXYWH(SkIntToScalar(tile_x_start), 377 SkIntToScalar(tile_y_start), 378 SkIntToScalar(current_width), 379 SkIntToScalar(fTileHeight)); 380 tile_x_start += current_width; 381 } 382 383 current_width >>= 1; 384 } 385 } 386} 387 388/** 389 * Draw the specified playback to the canvas translated to rectangle provided, so that this mini 390 * canvas represents the rectangle's portion of the overall picture. 391 * Saves and restores so that the initial clip and matrix return to their state before this function 392 * is called. 393 */ 394template<class T> 395static void DrawTileToCanvas(SkCanvas* canvas, const SkRect& tileRect, T* playback) { 396 int saveCount = canvas->save(); 397 // Translate so that we draw the correct portion of the picture 398 canvas->translate(-tileRect.fLeft, -tileRect.fTop); 399 playback->draw(canvas); 400 canvas->restoreToCount(saveCount); 401 canvas->flush(); 402} 403 404/////////////////////////////////////////////////////////////////////////////////////////////// 405 406bool TiledPictureRenderer::render(const SkString* path) { 407 SkASSERT(fPicture != NULL); 408 if (NULL == fPicture) { 409 return false; 410 } 411 412 // Reuse one canvas for all tiles. 413 SkCanvas* canvas = this->setupCanvas(fTileWidth, fTileHeight); 414 SkAutoUnref aur(canvas); 415 416 bool success = true; 417 for (int i = 0; i < fTileRects.count(); ++i) { 418 DrawTileToCanvas(canvas, fTileRects[i], fPicture); 419 if (NULL != path) { 420 success &= writeAppendNumber(canvas, path, i); 421 } 422 } 423 return success; 424} 425 426SkCanvas* TiledPictureRenderer::setupCanvas(int width, int height) { 427 SkCanvas* canvas = this->INHERITED::setupCanvas(width, height); 428 SkASSERT(fPicture != NULL); 429 // Clip the tile to an area that is completely in what the SkPicture says is the 430 // drawn-to area. This is mostly important for tiles on the right and bottom edges 431 // as they may go over this area and the picture may have some commands that 432 // draw outside of this area and so should not actually be written. 433 SkRect clip = SkRect::MakeWH(SkIntToScalar(fPicture->width()), 434 SkIntToScalar(fPicture->height())); 435 canvas->clipRect(clip); 436 return canvas; 437} 438 439SkString TiledPictureRenderer::getConfigNameInternal() { 440 SkString name; 441 if (fTileMinPowerOf2Width > 0) { 442 name.append("pow2tile_"); 443 name.appendf("%i", fTileMinPowerOf2Width); 444 } else { 445 name.append("tile_"); 446 if (fTileWidthPercentage > 0) { 447 name.appendf("%.f%%", fTileWidthPercentage); 448 } else { 449 name.appendf("%i", fTileWidth); 450 } 451 } 452 name.append("x"); 453 if (fTileHeightPercentage > 0) { 454 name.appendf("%.f%%", fTileHeightPercentage); 455 } else { 456 name.appendf("%i", fTileHeight); 457 } 458 return name; 459} 460 461/////////////////////////////////////////////////////////////////////////////////////////////// 462 463// Holds all of the information needed to draw a set of tiles. 464class CloneData : public SkRunnable { 465 466public: 467 CloneData(SkPicture* clone, SkCanvas* canvas, SkTDArray<SkRect>& rects, int start, int end, 468 SkRunnable* done) 469 : fClone(clone) 470 , fCanvas(canvas) 471 , fPath(NULL) 472 , fRects(rects) 473 , fStart(start) 474 , fEnd(end) 475 , fSuccess(NULL) 476 , fDone(done) { 477 SkASSERT(fDone != NULL); 478 } 479 480 virtual void run() SK_OVERRIDE { 481 SkGraphics::SetTLSFontCacheLimit(1024 * 1024); 482 for (int i = fStart; i < fEnd; i++) { 483 DrawTileToCanvas(fCanvas, fRects[i], fClone); 484 if (fPath != NULL && !writeAppendNumber(fCanvas, fPath, i) 485 && fSuccess != NULL) { 486 *fSuccess = false; 487 // If one tile fails to write to a file, do not continue drawing the rest. 488 break; 489 } 490 } 491 fDone->run(); 492 } 493 494 void setPathAndSuccess(const SkString* path, bool* success) { 495 fPath = path; 496 fSuccess = success; 497 } 498 499private: 500 // All pointers unowned. 501 SkPicture* fClone; // Picture to draw from. Each CloneData has a unique one which 502 // is threadsafe. 503 SkCanvas* fCanvas; // Canvas to draw to. Reused for each tile. 504 const SkString* fPath; // If non-null, path to write the result to as a PNG. 505 SkTDArray<SkRect>& fRects; // All tiles of the picture. 506 const int fStart; // Range of tiles drawn by this thread. 507 const int fEnd; 508 bool* fSuccess; // Only meaningful if path is non-null. Shared by all threads, 509 // and only set to false upon failure to write to a PNG. 510 SkRunnable* fDone; 511}; 512 513MultiCorePictureRenderer::MultiCorePictureRenderer(int threadCount) 514: fNumThreads(threadCount) 515, fThreadPool(threadCount) 516, fCountdown(threadCount) { 517 // Only need to create fNumThreads - 1 clones, since one thread will use the base 518 // picture. 519 fPictureClones = SkNEW_ARRAY(SkPicture, fNumThreads - 1); 520 fCloneData = SkNEW_ARRAY(CloneData*, fNumThreads); 521} 522 523void MultiCorePictureRenderer::init(SkPicture *pict) { 524 // Set fPicture and the tiles. 525 this->INHERITED::init(pict); 526 for (int i = 0; i < fNumThreads; ++i) { 527 *fCanvasPool.append() = this->setupCanvas(this->getTileWidth(), this->getTileHeight()); 528 } 529 // Only need to create fNumThreads - 1 clones, since one thread will use the base picture. 530 fPicture->clone(fPictureClones, fNumThreads - 1); 531 // Populate each thread with the appropriate data. 532 // Group the tiles into nearly equal size chunks, rounding up so we're sure to cover them all. 533 const int chunkSize = (fTileRects.count() + fNumThreads - 1) / fNumThreads; 534 535 for (int i = 0; i < fNumThreads; i++) { 536 SkPicture* pic; 537 if (i == fNumThreads-1) { 538 // The last set will use the original SkPicture. 539 pic = fPicture; 540 } else { 541 pic = &fPictureClones[i]; 542 } 543 const int start = i * chunkSize; 544 const int end = SkMin32(start + chunkSize, fTileRects.count()); 545 fCloneData[i] = SkNEW_ARGS(CloneData, 546 (pic, fCanvasPool[i], fTileRects, start, end, &fCountdown)); 547 } 548} 549 550bool MultiCorePictureRenderer::render(const SkString *path) { 551 bool success = true; 552 if (path != NULL) { 553 for (int i = 0; i < fNumThreads-1; i++) { 554 fCloneData[i]->setPathAndSuccess(path, &success); 555 } 556 } 557 558 fCountdown.reset(fNumThreads); 559 for (int i = 0; i < fNumThreads; i++) { 560 fThreadPool.add(fCloneData[i]); 561 } 562 fCountdown.wait(); 563 564 return success; 565} 566 567void MultiCorePictureRenderer::end() { 568 for (int i = 0; i < fNumThreads - 1; i++) { 569 SkDELETE(fCloneData[i]); 570 fCloneData[i] = NULL; 571 } 572 573 fCanvasPool.unrefAll(); 574 575 this->INHERITED::end(); 576} 577 578MultiCorePictureRenderer::~MultiCorePictureRenderer() { 579 // Each individual CloneData was deleted in end. 580 SkDELETE_ARRAY(fCloneData); 581 SkDELETE_ARRAY(fPictureClones); 582} 583 584SkString MultiCorePictureRenderer::getConfigNameInternal() { 585 SkString name = this->INHERITED::getConfigNameInternal(); 586 name.appendf("_multi_%i_threads", fNumThreads); 587 return name; 588} 589 590/////////////////////////////////////////////////////////////////////////////////////////////// 591 592void PlaybackCreationRenderer::setup() { 593 fReplayer.reset(this->createPicture()); 594 SkCanvas* recorder = fReplayer->beginRecording(fPicture->width(), fPicture->height(), 595 this->recordFlags()); 596 fPicture->draw(recorder); 597} 598 599bool PlaybackCreationRenderer::render(const SkString*) { 600 fReplayer->endRecording(); 601 // Since this class does not actually render, return false. 602 return false; 603} 604 605SkString PlaybackCreationRenderer::getConfigNameInternal() { 606 return SkString("playback_creation"); 607} 608 609/////////////////////////////////////////////////////////////////////////////////////////////// 610// SkPicture variants for each BBoxHierarchy type 611 612class RTreePicture : public SkPicture { 613public: 614 virtual SkBBoxHierarchy* createBBoxHierarchy() const SK_OVERRIDE{ 615 static const int kRTreeMinChildren = 6; 616 static const int kRTreeMaxChildren = 11; 617 SkScalar aspectRatio = SkScalarDiv(SkIntToScalar(fWidth), 618 SkIntToScalar(fHeight)); 619 return SkRTree::Create(kRTreeMinChildren, kRTreeMaxChildren, 620 aspectRatio); 621 } 622}; 623 624class TileGridPicture : public SkPicture { 625public: 626 TileGridPicture(int tileWidth, int tileHeight, int xTileCount, int yTileCount) { 627 fTileWidth = tileWidth; 628 fTileHeight = tileHeight; 629 fXTileCount = xTileCount; 630 fYTileCount = yTileCount; 631 } 632 633 virtual SkBBoxHierarchy* createBBoxHierarchy() const SK_OVERRIDE{ 634 return SkNEW_ARGS(SkTileGrid, (fTileWidth, fTileHeight, fXTileCount, fYTileCount)); 635 } 636private: 637 int fTileWidth, fTileHeight, fXTileCount, fYTileCount; 638}; 639 640SkPicture* PictureRenderer::createPicture() { 641 switch (fBBoxHierarchyType) { 642 case kNone_BBoxHierarchyType: 643 return SkNEW(SkPicture); 644 case kRTree_BBoxHierarchyType: 645 return SkNEW(RTreePicture); 646 case kTileGrid_BBoxHierarchyType: 647 { 648 int xTileCount = fPicture->width() / fGridWidth + 649 ((fPicture->width() % fGridWidth) ? 1 : 0); 650 int yTileCount = fPicture->height() / fGridHeight + 651 ((fPicture->height() % fGridHeight) ? 1 : 0); 652 return SkNEW_ARGS(TileGridPicture, (fGridWidth, fGridHeight, xTileCount, 653 yTileCount)); 654 } 655 } 656 SkASSERT(0); // invalid bbhType 657 return NULL; 658} 659 660} // namespace sk_tools 661