| OLD | NEW |
| 1 | 1 |
| 2 /* | 2 /* |
| 3 * Copyright 2006 The Android Open Source Project | 3 * Copyright 2006 The Android Open Source Project |
| 4 * | 4 * |
| 5 * Use of this source code is governed by a BSD-style license that can be | 5 * Use of this source code is governed by a BSD-style license that can be |
| 6 * found in the LICENSE file. | 6 * found in the LICENSE file. |
| 7 */ | 7 */ |
| 8 | 8 |
| 9 | 9 |
| 10 #include "SkBlurMask.h" | 10 #include "SkBlurMask.h" |
| 11 #include "SkMath.h" | 11 #include "SkMath.h" |
| 12 #include "SkTemplates.h" | 12 #include "SkTemplates.h" |
| 13 #include "SkEndian.h" | 13 #include "SkEndian.h" |
| 14 | 14 |
| 15 const SkScalar SkBlurMask::kBlurRadiusFudgeFactor = SkFloatToScalar(.57735f); | 15 |
| 16 SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) { |
| 17 // This constant approximates the scaling done in the software path's |
| 18 // "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)). |
| 19 // IMHO, it actually should be 1: we blur "less" than we should do |
| 20 // according to the CSS and canvas specs, simply because Safari does the sam
e. |
| 21 // Firefox used to do the same too, until 4.0 where they fixed it. So at so
me |
| 22 // point we should probably get rid of these scaling constants and rebaselin
e |
| 23 // all the blur tests. |
| 24 static const SkScalar kBLUR_SIGMA_SCALE = SkFloatToScalar(0.57735f); |
| 25 |
| 26 return radius ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f; |
| 27 } |
| 16 | 28 |
| 17 #define UNROLL_SEPARABLE_LOOPS | 29 #define UNROLL_SEPARABLE_LOOPS |
| 18 | 30 |
| 19 /** | 31 /** |
| 20 * This function performs a box blur in X, of the given radius. If the | 32 * This function performs a box blur in X, of the given radius. If the |
| 21 * "transpose" parameter is true, it will transpose the pixels on write, | 33 * "transpose" parameter is true, it will transpose the pixels on write, |
| 22 * such that X and Y are swapped. Reads are always performed from contiguous | 34 * such that X and Y are swapped. Reads are always performed from contiguous |
| 23 * memory in X, for speed. The destination buffer (dst) must be at least | 35 * memory in X, for speed. The destination buffer (dst) must be at least |
| 24 * (width + leftRadius + rightRadius) * height bytes in size. | 36 * (width + leftRadius + rightRadius) * height bytes in size. |
| 25 * | 37 * |
| (...skipping 440 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 466 | 478 |
| 467 // we use a local function to wrap the class static method to work around | 479 // we use a local function to wrap the class static method to work around |
| 468 // a bug in gcc98 | 480 // a bug in gcc98 |
| 469 void SkMask_FreeImage(uint8_t* image); | 481 void SkMask_FreeImage(uint8_t* image); |
| 470 void SkMask_FreeImage(uint8_t* image) { | 482 void SkMask_FreeImage(uint8_t* image) { |
| 471 SkMask::FreeImage(image); | 483 SkMask::FreeImage(image); |
| 472 } | 484 } |
| 473 | 485 |
| 474 bool SkBlurMask::Blur(SkMask* dst, const SkMask& src, | 486 bool SkBlurMask::Blur(SkMask* dst, const SkMask& src, |
| 475 SkScalar radius, Style style, Quality quality, | 487 SkScalar radius, Style style, Quality quality, |
| 476 SkIPoint* margin) | 488 SkIPoint* margin) { |
| 477 { | 489 return SkBlurMask::BoxBlur(dst, src, |
| 490 SkBlurMask::ConvertRadiusToSigma(radius), |
| 491 style, quality, margin); |
| 492 } |
| 493 |
| 494 bool SkBlurMask::BoxBlur(SkMask* dst, const SkMask& src, |
| 495 SkScalar sigma, Style style, Quality quality, |
| 496 SkIPoint* margin) { |
| 478 | 497 |
| 479 if (src.fFormat != SkMask::kA8_Format) { | 498 if (src.fFormat != SkMask::kA8_Format) { |
| 480 return false; | 499 return false; |
| 481 } | 500 } |
| 482 | 501 |
| 483 // Force high quality off for small radii (performance) | 502 // Force high quality off for small radii (performance) |
| 484 if (radius < SkIntToScalar(3)) { | 503 if (sigma <= SkIntToScalar(2)) { |
| 485 quality = kLow_Quality; | 504 quality = kLow_Quality; |
| 486 } | 505 } |
| 487 | 506 |
| 507 SkScalar passRadius; |
| 508 if (kHigh_Quality == quality) { |
| 509 // For the high quality path the 3 pass box blur kernel width is |
| 510 // 6*rad+1 while the full Gaussian width is 6*sigma. |
| 511 passRadius = sigma - (1/6.0f); |
| 512 } else { |
| 513 // For the low quality path we only attempt to cover 3*sigma of the |
| 514 // Gaussian blur area (1.5*sigma on each side). The single pass box |
| 515 // blur's kernel size is 2*rad+1. |
| 516 passRadius = 1.5f*sigma - 0.5f; |
| 517 } |
| 518 |
| 488 // highQuality: use three box blur passes as a cheap way | 519 // highQuality: use three box blur passes as a cheap way |
| 489 // to approximate a Gaussian blur | 520 // to approximate a Gaussian blur |
| 490 int passCount = (kHigh_Quality == quality) ? 3 : 1; | 521 int passCount = (kHigh_Quality == quality) ? 3 : 1; |
| 491 SkScalar passRadius = (kHigh_Quality == quality) ? | |
| 492 SkScalarMul( radius, kBlurRadiusFudgeFactor): | |
| 493 radius; | |
| 494 | 522 |
| 495 int rx = SkScalarCeil(passRadius); | 523 int rx = SkScalarCeil(passRadius); |
| 496 int outerWeight = 255 - SkScalarRound((SkIntToScalar(rx) - passRadius) * 255
); | 524 int outerWeight = 255 - SkScalarRound((SkIntToScalar(rx) - passRadius) * 255
); |
| 497 | 525 |
| 498 SkASSERT(rx >= 0); | 526 SkASSERT(rx >= 0); |
| 499 SkASSERT((unsigned)outerWeight <= 255); | 527 SkASSERT((unsigned)outerWeight <= 255); |
| 500 if (rx <= 0) { | 528 if (rx <= 0) { |
| 501 return false; | 529 return false; |
| 502 } | 530 } |
| 503 | 531 |
| 504 int ry = rx; // only do square blur for now | 532 int ry = rx; // only do square blur for now |
| 505 | 533 |
| 506 int padx = passCount * rx; | 534 int padx = passCount * rx; |
| 507 int pady = passCount * ry; | 535 int pady = passCount * ry; |
| 508 | 536 |
| 509 if (margin) { | 537 if (margin) { |
| 510 margin->set(padx, pady); | 538 margin->set(padx, pady); |
| 511 } | 539 } |
| 512 dst->fBounds.set(src.fBounds.fLeft - padx, src.fBounds.fTop - pady, | 540 dst->fBounds.set(src.fBounds.fLeft - padx, src.fBounds.fTop - pady, |
| 513 src.fBounds.fRight + padx, src.fBounds.fBottom + pady); | 541 src.fBounds.fRight + padx, src.fBounds.fBottom + pady); |
| 514 | 542 |
| 515 dst->fRowBytes = dst->fBounds.width(); | 543 dst->fRowBytes = dst->fBounds.width(); |
| 516 dst->fFormat = SkMask::kA8_Format; | 544 dst->fFormat = SkMask::kA8_Format; |
| 517 dst->fImage = NULL; | 545 dst->fImage = NULL; |
| 518 | 546 |
| 519 if (src.fImage) { | 547 if (src.fImage) { |
| 520 size_t dstSize = dst->computeImageSize(); | 548 size_t dstSize = dst->computeImageSize(); |
| 521 if (0 == dstSize) { | 549 if (0 == dstSize) { |
| 522 return false; // too big to allocate, abort | 550 return false; // too big to allocate, abort |
| 523 } | 551 } |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 644 | 672 |
| 645 if ( x > 0.5f ) { | 673 if ( x > 0.5f ) { |
| 646 return 0.5625f - (x3 / 6.0f - 3.0f * x2 * 0.25f + 1.125f * x); | 674 return 0.5625f - (x3 / 6.0f - 3.0f * x2 * 0.25f + 1.125f * x); |
| 647 } | 675 } |
| 648 if ( x > -0.5f ) { | 676 if ( x > -0.5f ) { |
| 649 return 0.5f - (0.75f * x - x3 / 3.0f); | 677 return 0.5f - (0.75f * x - x3 / 3.0f); |
| 650 } | 678 } |
| 651 return 0.4375f + (-x3 / 6.0f - 3.0f * x2 * 0.25f - 1.125f * x); | 679 return 0.4375f + (-x3 / 6.0f - 3.0f * x2 * 0.25f - 1.125f * x); |
| 652 } | 680 } |
| 653 | 681 |
| 654 // Compute the size of the array allocated for the profile. | |
| 655 | |
| 656 static int compute_profile_size(SkScalar radius) { | |
| 657 return SkScalarRoundToInt(radius * 3); | |
| 658 | |
| 659 } | |
| 660 | |
| 661 /* compute_profile allocates and fills in an array of floating | 682 /* compute_profile allocates and fills in an array of floating |
| 662 point values between 0 and 255 for the profile signature of | 683 point values between 0 and 255 for the profile signature of |
| 663 a blurred half-plane with the given blur radius. Since we're | 684 a blurred half-plane with the given blur radius. Since we're |
| 664 going to be doing screened multiplications (i.e., 1 - (1-x)(1-y)) | 685 going to be doing screened multiplications (i.e., 1 - (1-x)(1-y)) |
| 665 all the time, we actually fill in the profile pre-inverted | 686 all the time, we actually fill in the profile pre-inverted |
| 666 (already done 255-x). | 687 (already done 255-x). |
| 667 | 688 |
| 668 It's the responsibility of the caller to delete the | 689 It's the responsibility of the caller to delete the |
| 669 memory returned in profile_out. | 690 memory returned in profile_out. |
| 670 */ | 691 */ |
| 671 | 692 |
| 672 static void compute_profile(SkScalar radius, unsigned int **profile_out) { | 693 static void compute_profile(SkScalar sigma, unsigned int **profile_out) { |
| 673 int size = compute_profile_size(radius); | 694 int size = SkScalarCeilToInt(6*sigma); |
| 674 | 695 |
| 675 int center = size >> 1; | 696 int center = size >> 1; |
| 676 unsigned int *profile = SkNEW_ARRAY(unsigned int, size); | 697 unsigned int *profile = SkNEW_ARRAY(unsigned int, size); |
| 677 | 698 |
| 678 float invr = 1.f/radius; | 699 float invr = 1.f/(2*sigma); |
| 679 | 700 |
| 680 profile[0] = 255; | 701 profile[0] = 255; |
| 681 for (int x = 1 ; x < size ; ++x) { | 702 for (int x = 1 ; x < size ; ++x) { |
| 682 float scaled_x = (center - x - .5f) * invr; | 703 float scaled_x = (center - x - .5f) * invr; |
| 683 float gi = gaussianIntegral(scaled_x); | 704 float gi = gaussianIntegral(scaled_x); |
| 684 profile[x] = 255 - (uint8_t) (255.f * gi); | 705 profile[x] = 255 - (uint8_t) (255.f * gi); |
| 685 } | 706 } |
| 686 | 707 |
| 687 *profile_out = profile; | 708 *profile_out = profile; |
| 688 } | 709 } |
| 689 | 710 |
| 690 // TODO MAYBE: Maintain a profile cache to avoid recomputing this for | 711 // TODO MAYBE: Maintain a profile cache to avoid recomputing this for |
| 691 // commonly used radii. Consider baking some of the most common blur radii | 712 // commonly used radii. Consider baking some of the most common blur radii |
| 692 // directly in as static data? | 713 // directly in as static data? |
| 693 | 714 |
| 694 // Implementation adapted from Michael Herf's approach: | 715 // Implementation adapted from Michael Herf's approach: |
| 695 // http://stereopsis.com/shadowrect/ | 716 // http://stereopsis.com/shadowrect/ |
| 696 | 717 |
| 697 static inline unsigned int profile_lookup( unsigned int *profile, int loc, int b
lurred_width, int sharp_width ) { | 718 static inline unsigned int profile_lookup( unsigned int *profile, int loc, int b
lurred_width, int sharp_width ) { |
| 698 int dx = SkAbs32(((loc << 1) + 1) - blurred_width) - sharp_width; // how far
are we from the original edge? | 719 int dx = SkAbs32(((loc << 1) + 1) - blurred_width) - sharp_width; // how far
are we from the original edge? |
| 699 int ox = dx >> 1; | 720 int ox = dx >> 1; |
| 700 if (ox < 0) { | 721 if (ox < 0) { |
| 701 ox = 0; | 722 ox = 0; |
| 702 } | 723 } |
| 703 | 724 |
| 704 return profile[ox]; | 725 return profile[ox]; |
| 705 } | 726 } |
| 706 | 727 |
| 707 bool SkBlurMask::BlurRect(SkMask *dst, const SkRect &src, | 728 bool SkBlurMask::BlurRect(SkMask *dst, const SkRect &src, |
| 708 SkScalar provided_radius, Style style, | 729 SkScalar radius, Style style, |
| 709 SkIPoint *margin, SkMask::CreateMode createMode) { | 730 SkIPoint *margin, SkMask::CreateMode createMode) { |
| 710 int profile_size; | 731 return SkBlurMask::BlurRect(SkBlurMask::ConvertRadiusToSigma(radius), |
| 732 dst, src, |
| 733 style, margin, createMode); |
| 734 } |
| 711 | 735 |
| 712 float radius = SkScalarToFloat(SkScalarMul(provided_radius, kBlurRadiusFudge
Factor)); | 736 bool SkBlurMask::BlurRect(SkScalar sigma, SkMask *dst, |
| 713 | 737 const SkRect &src, Style style, |
| 714 // adjust blur radius to match interpretation from boxfilter code | 738 SkIPoint *margin, SkMask::CreateMode createMode) { |
| 715 radius = (radius + .5f) * 2.f; | 739 int profile_size = SkScalarCeilToInt(6*sigma); |
| 716 | |
| 717 profile_size = compute_profile_size(radius); | |
| 718 | 740 |
| 719 int pad = profile_size/2; | 741 int pad = profile_size/2; |
| 720 if (margin) { | 742 if (margin) { |
| 721 margin->set( pad, pad ); | 743 margin->set( pad, pad ); |
| 722 } | 744 } |
| 723 | 745 |
| 724 dst->fBounds.set(SkScalarRoundToInt(src.fLeft - pad), | 746 dst->fBounds.set(SkScalarRoundToInt(src.fLeft - pad), |
| 725 SkScalarRoundToInt(src.fTop - pad), | 747 SkScalarRoundToInt(src.fTop - pad), |
| 726 SkScalarRoundToInt(src.fRight + pad), | 748 SkScalarRoundToInt(src.fRight + pad), |
| 727 SkScalarRoundToInt(src.fBottom + pad)); | 749 SkScalarRoundToInt(src.fBottom + pad)); |
| (...skipping 10 matching lines...) Expand all Loading... |
| 738 dst->fBounds.set(SkScalarRoundToInt(src.fLeft), | 760 dst->fBounds.set(SkScalarRoundToInt(src.fLeft), |
| 739 SkScalarRoundToInt(src.fTop), | 761 SkScalarRoundToInt(src.fTop), |
| 740 SkScalarRoundToInt(src.fRight), | 762 SkScalarRoundToInt(src.fRight), |
| 741 SkScalarRoundToInt(src.fBottom)); // restore trimme
d bounds | 763 SkScalarRoundToInt(src.fBottom)); // restore trimme
d bounds |
| 742 dst->fRowBytes = sw; | 764 dst->fRowBytes = sw; |
| 743 } | 765 } |
| 744 return true; | 766 return true; |
| 745 } | 767 } |
| 746 unsigned int *profile = NULL; | 768 unsigned int *profile = NULL; |
| 747 | 769 |
| 748 compute_profile(radius, &profile); | 770 compute_profile(sigma, &profile); |
| 749 SkAutoTDeleteArray<unsigned int> ada(profile); | 771 SkAutoTDeleteArray<unsigned int> ada(profile); |
| 750 | 772 |
| 751 size_t dstSize = dst->computeImageSize(); | 773 size_t dstSize = dst->computeImageSize(); |
| 752 if (0 == dstSize) { | 774 if (0 == dstSize) { |
| 753 return false; // too big to allocate, abort | 775 return false; // too big to allocate, abort |
| 754 } | 776 } |
| 755 | 777 |
| 756 uint8_t* dp = SkMask::AllocImage(dstSize); | 778 uint8_t* dp = SkMask::AllocImage(dstSize); |
| 757 | 779 |
| 758 dst->fImage = dp; | 780 dst->fImage = dp; |
| 759 | 781 |
| 760 int dstHeight = dst->fBounds.height(); | 782 int dstHeight = dst->fBounds.height(); |
| 761 int dstWidth = dst->fBounds.width(); | 783 int dstWidth = dst->fBounds.width(); |
| 762 | 784 |
| 763 // nearest odd number less than the profile size represents the center | 785 // nearest odd number less than the profile size represents the center |
| 764 // of the (2x scaled) profile | 786 // of the (2x scaled) profile |
| 765 int center = ( profile_size & ~1 ) - 1; | 787 int center = ( profile_size & ~1 ) - 1; |
| 766 | 788 |
| 767 int w = sw - center; | 789 int w = sw - center; |
| 768 int h = sh - center; | 790 int h = sh - center; |
| 769 | 791 |
| 770 uint8_t *outptr = dp; | 792 uint8_t *outptr = dp; |
| 771 | 793 |
| 772 SkAutoTMalloc<uint8_t> horizontalScanline(dstWidth); | 794 SkAutoTMalloc<uint8_t> horizontalScanline(dstWidth); |
| 773 | 795 |
| 774 for (int x = 0 ; x < dstWidth ; ++x) { | 796 for (int x = 0 ; x < dstWidth ; ++x) { |
| 775 if (profile_size <= sw) { | 797 if (profile_size <= sw) { |
| 776 horizontalScanline[x] = profile_lookup(profile, x, dstWidth, w); | 798 horizontalScanline[x] = profile_lookup(profile, x, dstWidth, w); |
| 777 } else { | 799 } else { |
| 778 float span = float(sw)/radius; | 800 float span = float(sw)/(2*sigma); |
| 779 float giX = 1.5f - (x+.5f)/radius; | 801 float giX = 1.5f - (x+.5f)/(2*sigma); |
| 780 horizontalScanline[x] = (uint8_t) (255 * (gaussianIntegral(giX) - ga
ussianIntegral(giX + span))); | 802 horizontalScanline[x] = (uint8_t) (255 * (gaussianIntegral(giX) - ga
ussianIntegral(giX + span))); |
| 781 } | 803 } |
| 782 } | 804 } |
| 783 | 805 |
| 784 for (int y = 0 ; y < dstHeight ; ++y) { | 806 for (int y = 0 ; y < dstHeight ; ++y) { |
| 785 unsigned int profile_y; | 807 unsigned int profile_y; |
| 786 if (profile_size <= sh) { | 808 if (profile_size <= sh) { |
| 787 profile_y = profile_lookup(profile, y, dstHeight, h); | 809 profile_y = profile_lookup(profile, y, dstHeight, h); |
| 788 } else { | 810 } else { |
| 789 float span = float(sh)/radius; | 811 float span = float(sh)/(2*sigma); |
| 790 float giY = 1.5f - (y+.5f)/radius; | 812 float giY = 1.5f - (y+.5f)/(2*sigma); |
| 791 profile_y = (uint8_t) (255 * (gaussianIntegral(giY) - gaussianIntegr
al(giY + span))); | 813 profile_y = (uint8_t) (255 * (gaussianIntegral(giY) - gaussianIntegr
al(giY + span))); |
| 792 } | 814 } |
| 793 | 815 |
| 794 for (int x = 0 ; x < dstWidth ; x++) { | 816 for (int x = 0 ; x < dstWidth ; x++) { |
| 795 unsigned int maskval = SkMulDiv255Round(horizontalScanline[x], profi
le_y); | 817 unsigned int maskval = SkMulDiv255Round(horizontalScanline[x], profi
le_y); |
| 796 *(outptr++) = maskval; | 818 *(outptr++) = maskval; |
| 797 } | 819 } |
| 798 } | 820 } |
| 799 | 821 |
| 800 if (style == kInner_Style) { | 822 if (style == kInner_Style) { |
| (...skipping 26 matching lines...) Expand all Loading... |
| 827 uint8_t *dst_scanline = dp + y*dstWidth + pad; | 849 uint8_t *dst_scanline = dp + y*dstWidth + pad; |
| 828 memset(dst_scanline, 0xff, sw); | 850 memset(dst_scanline, 0xff, sw); |
| 829 } | 851 } |
| 830 } | 852 } |
| 831 // normal and solid styles are the same for analytic rect blurs, so don't | 853 // normal and solid styles are the same for analytic rect blurs, so don't |
| 832 // need to handle solid specially. | 854 // need to handle solid specially. |
| 833 | 855 |
| 834 return true; | 856 return true; |
| 835 } | 857 } |
| 836 | 858 |
| 859 bool SkBlurMask::BlurGroundTruth(SkMask* dst, const SkMask& src, SkScalar radius
, |
| 860 Style style, SkIPoint* margin) { |
| 861 return BlurGroundTruth(ConvertRadiusToSigma(radius), dst, src, style, margin
); |
| 862 } |
| 837 // The "simple" blur is a direct implementation of separable convolution with a
discrete | 863 // The "simple" blur is a direct implementation of separable convolution with a
discrete |
| 838 // gaussian kernel. It's "ground truth" in a sense; too slow to be used, but ve
ry | 864 // gaussian kernel. It's "ground truth" in a sense; too slow to be used, but ve
ry |
| 839 // useful for correctness comparisons. | 865 // useful for correctness comparisons. |
| 840 | 866 |
| 841 bool SkBlurMask::BlurGroundTruth(SkMask* dst, const SkMask& src, SkScalar provid
ed_radius, | 867 bool SkBlurMask::BlurGroundTruth(SkScalar sigma, SkMask* dst, const SkMask& src, |
| 842 Style style, SkIPoint* margin) { | 868 Style style, SkIPoint* margin) { |
| 843 | 869 |
| 844 if (src.fFormat != SkMask::kA8_Format) { | 870 if (src.fFormat != SkMask::kA8_Format) { |
| 845 return false; | 871 return false; |
| 846 } | 872 } |
| 847 | 873 |
| 848 float radius = SkScalarToFloat(SkScalarMul(provided_radius, kBlurRadiusFudge
Factor)); | 874 float variance = sigma * sigma; |
| 849 float stddev = SkScalarToFloat(radius) /2.0f; | |
| 850 float variance = stddev * stddev; | |
| 851 | 875 |
| 852 int windowSize = SkScalarCeil(stddev*4); | 876 int windowSize = SkScalarCeil(sigma*4); |
| 853 // round window size up to nearest odd number | 877 // round window size up to nearest odd number |
| 854 windowSize |= 1; | 878 windowSize |= 1; |
| 855 | 879 |
| 856 SkAutoTMalloc<float> gaussWindow(windowSize); | 880 SkAutoTMalloc<float> gaussWindow(windowSize); |
| 857 | 881 |
| 858 int halfWindow = windowSize >> 1; | 882 int halfWindow = windowSize >> 1; |
| 859 | 883 |
| 860 gaussWindow[halfWindow] = 1; | 884 gaussWindow[halfWindow] = 1; |
| 861 | 885 |
| 862 float windowSum = 1; | 886 float windowSum = 1; |
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 976 (void)autoCall.detach(); | 1000 (void)autoCall.detach(); |
| 977 } | 1001 } |
| 978 | 1002 |
| 979 if (style == kInner_Style) { | 1003 if (style == kInner_Style) { |
| 980 dst->fBounds = src.fBounds; // restore trimmed bounds | 1004 dst->fBounds = src.fBounds; // restore trimmed bounds |
| 981 dst->fRowBytes = src.fRowBytes; | 1005 dst->fRowBytes = src.fRowBytes; |
| 982 } | 1006 } |
| 983 | 1007 |
| 984 return true; | 1008 return true; |
| 985 } | 1009 } |
| OLD | NEW |