1 /* 2 * Copyright 2015 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 "GrTextUtils.h" 9 10 #include "GrAtlasTextBlob.h" 11 #include "GrBatchFontCache.h" 12 #include "GrBlurUtils.h" 13 #include "GrCaps.h" 14 #include "GrContext.h" 15 #include "GrDrawContext.h" 16 17 #include "SkDistanceFieldGen.h" 18 #include "SkDrawProcs.h" 19 #include "SkFindAndPlaceGlyph.h" 20 #include "SkGlyphCache.h" 21 #include "SkPaint.h" 22 #include "SkRect.h" 23 #include "SkTextMapStateProc.h" 24 #include "SkTextToPathIter.h" 25 26 namespace { 27 static const int kMinDFFontSize = 18; 28 static const int kSmallDFFontSize = 32; 29 static const int kSmallDFFontLimit = 32; 30 static const int kMediumDFFontSize = 72; 31 static const int kMediumDFFontLimit = 72; 32 static const int kLargeDFFontSize = 162; 33 #ifdef SK_BUILD_FOR_ANDROID 34 static const int kLargeDFFontLimit = 384; 35 #else 36 static const int kLargeDFFontLimit = 2 * kLargeDFFontSize; 37 #endif 38 }; 39 40 void GrTextUtils::DrawBmpText(GrAtlasTextBlob* blob, int runIndex, 41 GrBatchFontCache* fontCache, 42 const SkSurfaceProps& props, const SkPaint& skPaint, 43 GrColor color, 44 const SkMatrix& viewMatrix, 45 const char text[], size_t byteLength, 46 SkScalar x, SkScalar y) { 47 SkASSERT(byteLength == 0 || text != nullptr); 48 49 // nothing to draw 50 if (text == nullptr || byteLength == 0) { 51 return; 52 } 53 54 // Ensure the blob is set for bitmaptext 55 blob->setHasBitmap(); 56 57 GrBatchTextStrike* currStrike = nullptr; 58 59 // Get GrFontScaler from cache 60 SkGlyphCache* cache = blob->setupCache(runIndex, props, SkPaint::FakeGamma::On, 61 skPaint, &viewMatrix); 62 GrFontScaler* fontScaler = GrTextUtils::GetGrFontScaler(cache); 63 64 SkFindAndPlaceGlyph::ProcessText( 65 skPaint.getTextEncoding(), text, byteLength, 66 {x, y}, viewMatrix, skPaint.getTextAlign(), 67 cache, 68 [&](const SkGlyph& glyph, SkPoint position, SkPoint rounding) { 69 position += rounding; 70 BmpAppendGlyph( 71 blob, runIndex, fontCache, &currStrike, glyph, 72 SkScalarFloorToInt(position.fX), SkScalarFloorToInt(position.fY), 73 color, fontScaler); 74 } 75 ); 76 77 SkGlyphCache::AttachCache(cache); 78 } 79 80 void GrTextUtils::DrawBmpPosText(GrAtlasTextBlob* blob, int runIndex, 81 GrBatchFontCache* fontCache, 82 const SkSurfaceProps& props, const SkPaint& skPaint, 83 GrColor color, 84 const SkMatrix& viewMatrix, 85 const char text[], size_t byteLength, 86 const SkScalar pos[], int scalarsPerPosition, 87 const SkPoint& offset) { 88 SkASSERT(byteLength == 0 || text != nullptr); 89 SkASSERT(1 == scalarsPerPosition || 2 == scalarsPerPosition); 90 91 // nothing to draw 92 if (text == nullptr || byteLength == 0) { 93 return; 94 } 95 96 // Ensure the blob is set for bitmaptext 97 blob->setHasBitmap(); 98 99 GrBatchTextStrike* currStrike = nullptr; 100 101 // Get GrFontScaler from cache 102 SkGlyphCache* cache = blob->setupCache(runIndex, props, SkPaint::FakeGamma::On, 103 skPaint, &viewMatrix); 104 GrFontScaler* fontScaler = GrTextUtils::GetGrFontScaler(cache); 105 106 SkFindAndPlaceGlyph::ProcessPosText( 107 skPaint.getTextEncoding(), text, byteLength, 108 offset, viewMatrix, pos, scalarsPerPosition, 109 skPaint.getTextAlign(), cache, 110 [&](const SkGlyph& glyph, SkPoint position, SkPoint rounding) { 111 position += rounding; 112 BmpAppendGlyph( 113 blob, runIndex, fontCache, &currStrike, glyph, 114 SkScalarFloorToInt(position.fX), SkScalarFloorToInt(position.fY), 115 color, fontScaler); 116 } 117 ); 118 119 SkGlyphCache::AttachCache(cache); 120 } 121 122 void GrTextUtils::BmpAppendGlyph(GrAtlasTextBlob* blob, int runIndex, 123 GrBatchFontCache* fontCache, 124 GrBatchTextStrike** strike, const SkGlyph& skGlyph, 125 int vx, int vy, GrColor color, GrFontScaler* scaler) { 126 if (!*strike) { 127 *strike = fontCache->getStrike(scaler); 128 } 129 130 GrGlyph::PackedID id = GrGlyph::Pack(skGlyph.getGlyphID(), 131 skGlyph.getSubXFixed(), 132 skGlyph.getSubYFixed(), 133 GrGlyph::kCoverage_MaskStyle); 134 GrGlyph* glyph = (*strike)->getGlyph(skGlyph, id, scaler); 135 if (!glyph) { 136 return; 137 } 138 139 int x = vx + glyph->fBounds.fLeft; 140 int y = vy + glyph->fBounds.fTop; 141 142 // keep them as ints until we've done the clip-test 143 int width = glyph->fBounds.width(); 144 int height = glyph->fBounds.height(); 145 146 SkRect r; 147 r.fLeft = SkIntToScalar(x); 148 r.fTop = SkIntToScalar(y); 149 r.fRight = r.fLeft + SkIntToScalar(width); 150 r.fBottom = r.fTop + SkIntToScalar(height); 151 152 blob->appendGlyph(runIndex, r, color, *strike, glyph, scaler, skGlyph, 153 SkIntToScalar(vx), SkIntToScalar(vy), 1.0f, false); 154 } 155 156 bool GrTextUtils::CanDrawAsDistanceFields(const SkPaint& skPaint, const SkMatrix& viewMatrix, 157 const SkSurfaceProps& props, const GrShaderCaps& caps) { 158 // TODO: support perspective (need getMaxScale replacement) 159 if (viewMatrix.hasPerspective()) { 160 return false; 161 } 162 163 SkScalar maxScale = viewMatrix.getMaxScale(); 164 SkScalar scaledTextSize = maxScale*skPaint.getTextSize(); 165 // Hinted text looks far better at small resolutions 166 // Scaling up beyond 2x yields undesireable artifacts 167 if (scaledTextSize < kMinDFFontSize || 168 scaledTextSize > kLargeDFFontLimit) { 169 return false; 170 } 171 172 bool useDFT = props.isUseDeviceIndependentFonts(); 173 #if SK_FORCE_DISTANCE_FIELD_TEXT 174 useDFT = true; 175 #endif 176 177 if (!useDFT && scaledTextSize < kLargeDFFontSize) { 178 return false; 179 } 180 181 // rasterizers and mask filters modify alpha, which doesn't 182 // translate well to distance 183 if (skPaint.getRasterizer() || skPaint.getMaskFilter() || !caps.shaderDerivativeSupport()) { 184 return false; 185 } 186 187 // TODO: add some stroking support 188 if (skPaint.getStyle() != SkPaint::kFill_Style) { 189 return false; 190 } 191 192 return true; 193 } 194 195 void GrTextUtils::InitDistanceFieldPaint(GrAtlasTextBlob* blob, 196 SkPaint* skPaint, 197 SkScalar* textRatio, 198 const SkMatrix& viewMatrix) { 199 // getMaxScale doesn't support perspective, so neither do we at the moment 200 SkASSERT(!viewMatrix.hasPerspective()); 201 SkScalar maxScale = viewMatrix.getMaxScale(); 202 SkScalar textSize = skPaint->getTextSize(); 203 SkScalar scaledTextSize = textSize; 204 // if we have non-unity scale, we need to choose our base text size 205 // based on the SkPaint's text size multiplied by the max scale factor 206 // TODO: do we need to do this if we're scaling down (i.e. maxScale < 1)? 207 if (maxScale > 0 && !SkScalarNearlyEqual(maxScale, SK_Scalar1)) { 208 scaledTextSize *= maxScale; 209 } 210 211 // We have three sizes of distance field text, and within each size 'bucket' there is a floor 212 // and ceiling. A scale outside of this range would require regenerating the distance fields 213 SkScalar dfMaskScaleFloor; 214 SkScalar dfMaskScaleCeil; 215 if (scaledTextSize <= kSmallDFFontLimit) { 216 dfMaskScaleFloor = kMinDFFontSize; 217 dfMaskScaleCeil = kSmallDFFontLimit; 218 *textRatio = textSize / kSmallDFFontSize; 219 skPaint->setTextSize(SkIntToScalar(kSmallDFFontSize)); 220 } else if (scaledTextSize <= kMediumDFFontLimit) { 221 dfMaskScaleFloor = kSmallDFFontLimit; 222 dfMaskScaleCeil = kMediumDFFontLimit; 223 *textRatio = textSize / kMediumDFFontSize; 224 skPaint->setTextSize(SkIntToScalar(kMediumDFFontSize)); 225 } else { 226 dfMaskScaleFloor = kMediumDFFontLimit; 227 dfMaskScaleCeil = kLargeDFFontLimit; 228 *textRatio = textSize / kLargeDFFontSize; 229 skPaint->setTextSize(SkIntToScalar(kLargeDFFontSize)); 230 } 231 232 // Because there can be multiple runs in the blob, we want the overall maxMinScale, and 233 // minMaxScale to make regeneration decisions. Specifically, we want the maximum minimum scale 234 // we can tolerate before we'd drop to a lower mip size, and the minimum maximum scale we can 235 // tolerate before we'd have to move to a large mip size. When we actually test these values 236 // we look at the delta in scale between the new viewmatrix and the old viewmatrix, and test 237 // against these values to decide if we can reuse or not(ie, will a given scale change our mip 238 // level) 239 SkASSERT(dfMaskScaleFloor <= scaledTextSize && scaledTextSize <= dfMaskScaleCeil); 240 blob->setMinAndMaxScale(dfMaskScaleFloor / scaledTextSize, dfMaskScaleCeil / scaledTextSize); 241 242 skPaint->setLCDRenderText(false); 243 skPaint->setAutohinted(false); 244 skPaint->setHinting(SkPaint::kNormal_Hinting); 245 skPaint->setSubpixelText(true); 246 } 247 248 void GrTextUtils::DrawDFText(GrAtlasTextBlob* blob, int runIndex, 249 GrBatchFontCache* fontCache, const SkSurfaceProps& props, 250 const SkPaint& skPaint, GrColor color, 251 const SkMatrix& viewMatrix, 252 const char text[], size_t byteLength, 253 SkScalar x, SkScalar y) { 254 SkASSERT(byteLength == 0 || text != nullptr); 255 256 // nothing to draw 257 if (text == nullptr || byteLength == 0) { 258 return; 259 } 260 261 SkPaint::GlyphCacheProc glyphCacheProc = skPaint.getGlyphCacheProc(true); 262 SkAutoDescriptor desc; 263 skPaint.getScalerContextDescriptor(&desc, props, SkPaint::FakeGamma::Off, nullptr); 264 SkGlyphCache* origPaintCache = SkGlyphCache::DetachCache(skPaint.getTypeface(), 265 desc.getDesc()); 266 267 SkTArray<SkScalar> positions; 268 269 const char* textPtr = text; 270 SkFixed stopX = 0; 271 SkFixed stopY = 0; 272 SkFixed origin = 0; 273 switch (skPaint.getTextAlign()) { 274 case SkPaint::kRight_Align: origin = SK_Fixed1; break; 275 case SkPaint::kCenter_Align: origin = SK_FixedHalf; break; 276 case SkPaint::kLeft_Align: origin = 0; break; 277 } 278 279 SkAutoKern autokern; 280 const char* stop = text + byteLength; 281 while (textPtr < stop) { 282 // don't need x, y here, since all subpixel variants will have the 283 // same advance 284 const SkGlyph& glyph = glyphCacheProc(origPaintCache, &textPtr); 285 286 SkFixed width = glyph.fAdvanceX + autokern.adjust(glyph); 287 positions.push_back(SkFixedToScalar(stopX + SkFixedMul(origin, width))); 288 289 SkFixed height = glyph.fAdvanceY; 290 positions.push_back(SkFixedToScalar(stopY + SkFixedMul(origin, height))); 291 292 stopX += width; 293 stopY += height; 294 } 295 SkASSERT(textPtr == stop); 296 297 SkGlyphCache::AttachCache(origPaintCache); 298 299 // now adjust starting point depending on alignment 300 SkScalar alignX = SkFixedToScalar(stopX); 301 SkScalar alignY = SkFixedToScalar(stopY); 302 if (skPaint.getTextAlign() == SkPaint::kCenter_Align) { 303 alignX = SkScalarHalf(alignX); 304 alignY = SkScalarHalf(alignY); 305 } else if (skPaint.getTextAlign() == SkPaint::kLeft_Align) { 306 alignX = 0; 307 alignY = 0; 308 } 309 x -= alignX; 310 y -= alignY; 311 SkPoint offset = SkPoint::Make(x, y); 312 313 DrawDFPosText(blob, runIndex, fontCache, props, skPaint, color, viewMatrix, text, byteLength, 314 positions.begin(), 2, offset); 315 } 316 317 void GrTextUtils::DrawDFPosText(GrAtlasTextBlob* blob, int runIndex, 318 GrBatchFontCache* fontCache, const SkSurfaceProps& props, 319 const SkPaint& origPaint, 320 GrColor color, const SkMatrix& viewMatrix, 321 const char text[], size_t byteLength, 322 const SkScalar pos[], int scalarsPerPosition, 323 const SkPoint& offset) { 324 SkASSERT(byteLength == 0 || text != nullptr); 325 SkASSERT(1 == scalarsPerPosition || 2 == scalarsPerPosition); 326 327 // nothing to draw 328 if (text == nullptr || byteLength == 0) { 329 return; 330 } 331 332 SkTDArray<char> fallbackTxt; 333 SkTDArray<SkScalar> fallbackPos; 334 335 // Setup distance field paint and text ratio 336 SkScalar textRatio; 337 SkPaint dfPaint(origPaint); 338 GrTextUtils::InitDistanceFieldPaint(blob, &dfPaint, &textRatio, viewMatrix); 339 blob->setHasDistanceField(); 340 blob->setSubRunHasDistanceFields(runIndex, origPaint.isLCDRenderText()); 341 342 GrBatchTextStrike* currStrike = nullptr; 343 344 SkGlyphCache* cache = blob->setupCache(runIndex, props, SkPaint::FakeGamma::Off, 345 dfPaint, nullptr); 346 SkPaint::GlyphCacheProc glyphCacheProc = dfPaint.getGlyphCacheProc(true); 347 GrFontScaler* fontScaler = GrTextUtils::GetGrFontScaler(cache); 348 349 const char* stop = text + byteLength; 350 351 if (SkPaint::kLeft_Align == dfPaint.getTextAlign()) { 352 while (text < stop) { 353 const char* lastText = text; 354 // the last 2 parameters are ignored 355 const SkGlyph& glyph = glyphCacheProc(cache, &text); 356 357 if (glyph.fWidth) { 358 SkScalar x = offset.x() + pos[0]; 359 SkScalar y = offset.y() + (2 == scalarsPerPosition ? pos[1] : 0); 360 361 if (!DfAppendGlyph(blob, 362 runIndex, 363 fontCache, 364 &currStrike, 365 glyph, 366 x, y, color, fontScaler, 367 textRatio, viewMatrix)) { 368 // couldn't append, send to fallback 369 fallbackTxt.append(SkToInt(text-lastText), lastText); 370 *fallbackPos.append() = pos[0]; 371 if (2 == scalarsPerPosition) { 372 *fallbackPos.append() = pos[1]; 373 } 374 } 375 } 376 pos += scalarsPerPosition; 377 } 378 } else { 379 SkScalar alignMul = SkPaint::kCenter_Align == dfPaint.getTextAlign() ? SK_ScalarHalf 380 : SK_Scalar1; 381 while (text < stop) { 382 const char* lastText = text; 383 // the last 2 parameters are ignored 384 const SkGlyph& glyph = glyphCacheProc(cache, &text); 385 386 if (glyph.fWidth) { 387 SkScalar x = offset.x() + pos[0]; 388 SkScalar y = offset.y() + (2 == scalarsPerPosition ? pos[1] : 0); 389 390 SkScalar advanceX = SkFixedToScalar(glyph.fAdvanceX) * alignMul * textRatio; 391 SkScalar advanceY = SkFixedToScalar(glyph.fAdvanceY) * alignMul * textRatio; 392 393 if (!DfAppendGlyph(blob, 394 runIndex, 395 fontCache, 396 &currStrike, 397 glyph, 398 x - advanceX, y - advanceY, color, 399 fontScaler, 400 textRatio, 401 viewMatrix)) { 402 // couldn't append, send to fallback 403 fallbackTxt.append(SkToInt(text-lastText), lastText); 404 *fallbackPos.append() = pos[0]; 405 if (2 == scalarsPerPosition) { 406 *fallbackPos.append() = pos[1]; 407 } 408 } 409 } 410 pos += scalarsPerPosition; 411 } 412 } 413 414 SkGlyphCache::AttachCache(cache); 415 if (fallbackTxt.count()) { 416 blob->initOverride(runIndex); 417 GrTextUtils::DrawBmpPosText(blob, runIndex, fontCache, props, 418 origPaint, origPaint.getColor(), viewMatrix, 419 fallbackTxt.begin(), fallbackTxt.count(), 420 fallbackPos.begin(), scalarsPerPosition, offset); 421 } 422 } 423 424 bool GrTextUtils::DfAppendGlyph(GrAtlasTextBlob* blob, int runIndex, GrBatchFontCache* cache, 425 GrBatchTextStrike** strike, const SkGlyph& skGlyph, 426 SkScalar sx, SkScalar sy, GrColor color, 427 GrFontScaler* scaler, 428 SkScalar textRatio, const SkMatrix& viewMatrix) { 429 if (!*strike) { 430 *strike = cache->getStrike(scaler); 431 } 432 433 GrGlyph::PackedID id = GrGlyph::Pack(skGlyph.getGlyphID(), 434 skGlyph.getSubXFixed(), 435 skGlyph.getSubYFixed(), 436 GrGlyph::kDistance_MaskStyle); 437 GrGlyph* glyph = (*strike)->getGlyph(skGlyph, id, scaler); 438 if (!glyph) { 439 return true; 440 } 441 442 // fallback to color glyph support 443 if (kA8_GrMaskFormat != glyph->fMaskFormat) { 444 return false; 445 } 446 447 SkScalar dx = SkIntToScalar(glyph->fBounds.fLeft + SK_DistanceFieldInset); 448 SkScalar dy = SkIntToScalar(glyph->fBounds.fTop + SK_DistanceFieldInset); 449 SkScalar width = SkIntToScalar(glyph->fBounds.width() - 2 * SK_DistanceFieldInset); 450 SkScalar height = SkIntToScalar(glyph->fBounds.height() - 2 * SK_DistanceFieldInset); 451 452 SkScalar scale = textRatio; 453 dx *= scale; 454 dy *= scale; 455 width *= scale; 456 height *= scale; 457 sx += dx; 458 sy += dy; 459 SkRect glyphRect = SkRect::MakeXYWH(sx, sy, width, height); 460 461 blob->appendGlyph(runIndex, glyphRect, color, *strike, glyph, scaler, skGlyph, 462 sx - dx, sy - dy, scale, true); 463 return true; 464 } 465 466 void GrTextUtils::DrawTextAsPath(GrContext* context, GrDrawContext* dc, 467 const GrClip& clip, 468 const SkPaint& skPaint, const SkMatrix& viewMatrix, 469 const char text[], size_t byteLength, SkScalar x, SkScalar y, 470 const SkIRect& clipBounds) { 471 SkTextToPathIter iter(text, byteLength, skPaint, true); 472 473 SkMatrix matrix; 474 matrix.setScale(iter.getPathScale(), iter.getPathScale()); 475 matrix.postTranslate(x, y); 476 477 const SkPath* iterPath; 478 SkScalar xpos, prevXPos = 0; 479 480 while (iter.next(&iterPath, &xpos)) { 481 matrix.postTranslate(xpos - prevXPos, 0); 482 if (iterPath) { 483 const SkPaint& pnt = iter.getPaint(); 484 GrBlurUtils::drawPathWithMaskFilter(context, dc, clip, *iterPath, 485 pnt, viewMatrix, &matrix, clipBounds, false); 486 } 487 prevXPos = xpos; 488 } 489 } 490 491 void GrTextUtils::DrawPosTextAsPath(GrContext* context, 492 GrDrawContext* dc, 493 const SkSurfaceProps& props, 494 const GrClip& clip, 495 const SkPaint& origPaint, const SkMatrix& viewMatrix, 496 const char text[], size_t byteLength, 497 const SkScalar pos[], int scalarsPerPosition, 498 const SkPoint& offset, const SkIRect& clipBounds) { 499 // setup our std paint, in hopes of getting hits in the cache 500 SkPaint paint(origPaint); 501 SkScalar matrixScale = paint.setupForAsPaths(); 502 503 SkMatrix matrix; 504 matrix.setScale(matrixScale, matrixScale); 505 506 // Temporarily jam in kFill, so we only ever ask for the raw outline from the cache. 507 paint.setStyle(SkPaint::kFill_Style); 508 paint.setPathEffect(nullptr); 509 510 SkPaint::GlyphCacheProc glyphCacheProc = paint.getGlyphCacheProc(true); 511 SkAutoGlyphCache autoCache(paint, &props, nullptr); 512 SkGlyphCache* cache = autoCache.getCache(); 513 514 const char* stop = text + byteLength; 515 SkTextAlignProc alignProc(paint.getTextAlign()); 516 SkTextMapStateProc tmsProc(SkMatrix::I(), offset, scalarsPerPosition); 517 518 // Now restore the original settings, so we "draw" with whatever style/stroking. 519 paint.setStyle(origPaint.getStyle()); 520 paint.setPathEffect(origPaint.getPathEffect()); 521 522 while (text < stop) { 523 const SkGlyph& glyph = glyphCacheProc(cache, &text); 524 if (glyph.fWidth) { 525 const SkPath* path = cache->findPath(glyph); 526 if (path) { 527 SkPoint tmsLoc; 528 tmsProc(pos, &tmsLoc); 529 SkPoint loc; 530 alignProc(tmsLoc, glyph, &loc); 531 532 matrix[SkMatrix::kMTransX] = loc.fX; 533 matrix[SkMatrix::kMTransY] = loc.fY; 534 GrBlurUtils::drawPathWithMaskFilter(context, dc, clip, *path, paint, 535 viewMatrix, &matrix, clipBounds, false); 536 } 537 } 538 pos += scalarsPerPosition; 539 } 540 } 541 542 bool GrTextUtils::ShouldDisableLCD(const SkPaint& paint) { 543 return !SkXfermode::AsMode(paint.getXfermode(), nullptr) || 544 paint.getMaskFilter() || 545 paint.getRasterizer() || 546 paint.getPathEffect() || 547 paint.isFakeBoldText() || 548 paint.getStyle() != SkPaint::kFill_Style; 549 } 550 551 uint32_t GrTextUtils::FilterTextFlags(const SkSurfaceProps& surfaceProps, const SkPaint& paint) { 552 uint32_t flags = paint.getFlags(); 553 554 if (!paint.isLCDRenderText() || !paint.isAntiAlias()) { 555 return flags; 556 } 557 558 if (kUnknown_SkPixelGeometry == surfaceProps.pixelGeometry() || ShouldDisableLCD(paint)) { 559 flags &= ~SkPaint::kLCDRenderText_Flag; 560 flags |= SkPaint::kGenA8FromLCD_Flag; 561 } 562 563 return flags; 564 } 565 566 static void glyph_cache_aux_proc(void* data) { 567 GrFontScaler* scaler = (GrFontScaler*)data; 568 SkSafeUnref(scaler); 569 } 570 571 GrFontScaler* GrTextUtils::GetGrFontScaler(SkGlyphCache* cache) { 572 void* auxData; 573 GrFontScaler* scaler = nullptr; 574 575 if (cache->getAuxProcData(glyph_cache_aux_proc, &auxData)) { 576 scaler = (GrFontScaler*)auxData; 577 } 578 if (nullptr == scaler) { 579 scaler = new GrFontScaler(cache); 580 cache->setAuxProc(glyph_cache_aux_proc, scaler); 581 } 582 583 return scaler; 584 } 585