diff options
Diffstat (limited to 'gfx')
-rw-r--r-- | gfx/skbitmap_operations.cc | 322 | ||||
-rw-r--r-- | gfx/skbitmap_operations_unittest.cc | 116 |
2 files changed, 421 insertions, 17 deletions
diff --git a/gfx/skbitmap_operations.cc b/gfx/skbitmap_operations.cc index 5ce8ebd..27508ba 100644 --- a/gfx/skbitmap_operations.cc +++ b/gfx/skbitmap_operations.cc @@ -5,6 +5,7 @@ #include "gfx/skbitmap_operations.h" #include <algorithm> +#include <string.h> #include "base/logging.h" #include "third_party/skia/include/core/SkBitmap.h" @@ -209,11 +210,327 @@ SkBitmap SkBitmapOperations::CreateButtonBackground(SkColor color, return background; } +namespace { +namespace HSLShift { + +// TODO(viettrungluu): Some things have yet to be optimized at all. + +// Notes on and conventions used in the following code +// +// Conventions: +// - R, G, B, A = obvious; as variables: |r|, |g|, |b|, |a| (see also below) +// - H, S, L = obvious; as variables: |h|, |s|, |l| (see also below) +// - variables derived from S, L shift parameters: |sdec| and |sinc| for S +// increase and decrease factors, |ldec| and |linc| for L (see also below) +// +// To try to optimize HSL shifts, we do several things: +// - Avoid unpremultiplying (then processing) then premultiplying. This means +// that R, G, B values (and also L, but not H and S) should be treated as +// having a range of 0..A (where A is alpha). +// - Do things in integer/fixed-point. This avoids costly conversions between +// floating-point and integer, though I should study the tradeoff more +// carefully (presumably, at some point of processing complexity, converting +// and processing using simpler floating-point code will begin to win in +// performance). Also to be studied is the speed/type of floating point +// conversions; see, e.g., <http://www.stereopsis.com/sree/fpu2006.html>. +// +// Conventions for fixed-point arithmetic +// - Each function has a constant denominator (called |den|, which should be a +// power of 2), appropriate for the computations done in that function. +// - A value |x| is then typically represented by a numerator, named |x_num|, +// so that its actual value is |x_num / den| (casting to floating-point +// before division). +// - To obtain |x_num| from |x|, simply multiply by |den|, i.e., |x_num = x * +// den| (casting appropriately). +// - When necessary, a value |x| may also be represented as a numerator over +// the denominator squared (set |den2 = den * den|). In such a case, the +// corresponding variable is called |x_num2| (so that its actual value is +// |x_num^2 / den2|. +// - The representation of the product of |x| and |y| is be called |x_y_num| if +// |x * y == x_y_num / den|, and |xy_num2| if |x * y == x_y_num2 / den2|. In +// the latter case, notice that one can calculate |x_y_num2 = x_num * y_num|. + +// Routine used to process a line; typically specialized for specific kinds of +// HSL shifts (to optimize). +typedef void (*LineProcessor)(color_utils::HSL, + const SkPMColor*, + SkPMColor*, + int width); + +enum OperationOnH { kOpHNone = 0, kOpHShift, kNumHOps }; +enum OperationOnS { kOpSNone = 0, kOpSDec, kOpSInc, kNumSOps }; +enum OperationOnL { kOpLNone = 0, kOpLDec, kOpLInc, kNumLOps }; + +// Epsilon used to judge when shift values are close enough to various critical +// values (typically 0.5, which yields a no-op for S and L shifts. 1/256 should +// be small enough, but let's play it safe> +const double epsilon = 0.0005; + +// Line processor: default/universal (i.e., old-school). +void LineProcDefault(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + for (int x = 0; x < width; x++) { + out[x] = SkPreMultiplyColor(color_utils::HSLShift( + SkUnPreMultiply::PMColorToColor(in[x]), hsl_shift)); + } +} + +// Line processor: no-op (i.e., copy). +void LineProcCopy(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s < 0 || fabs(hsl_shift.s - 0.5) < HSLShift::epsilon); + DCHECK(hsl_shift.l < 0 || fabs(hsl_shift.l - 0.5) < HSLShift::epsilon); + memcpy(out, in, static_cast<size_t>(width) * sizeof(out[0])); +} + +// Line processor: H no-op, S no-op, L decrease. +void LineProcHnopSnopLdec(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + const uint32_t den = 65536; + + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s < 0 || fabs(hsl_shift.s - 0.5) < HSLShift::epsilon); + DCHECK(hsl_shift.l <= 0.5 - HSLShift::epsilon && hsl_shift.l >= 0); + + uint32_t ldec_num = static_cast<uint32_t>(hsl_shift.l * 2 * den); + for (int x = 0; x < width; x++) { + uint32_t a = SkGetPackedA32(in[x]); + uint32_t r = SkGetPackedR32(in[x]); + uint32_t g = SkGetPackedG32(in[x]); + uint32_t b = SkGetPackedB32(in[x]); + r = r * ldec_num / den; + g = g * ldec_num / den; + b = b * ldec_num / den; + out[x] = SkPackARGB32(a, r, g, b); + } +} + +// Line processor: H no-op, S no-op, L increase. +void LineProcHnopSnopLinc(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + const uint32_t den = 65536; + + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s < 0 || fabs(hsl_shift.s - 0.5) < HSLShift::epsilon); + DCHECK(hsl_shift.l >= 0.5 + HSLShift::epsilon && hsl_shift.l <= 1); + + uint32_t linc_num = static_cast<uint32_t>((hsl_shift.l - 0.5) * 2 * den); + for (int x = 0; x < width; x++) { + uint32_t a = SkGetPackedA32(in[x]); + uint32_t r = SkGetPackedR32(in[x]); + uint32_t g = SkGetPackedG32(in[x]); + uint32_t b = SkGetPackedB32(in[x]); + r += (a - r) * linc_num / den; + g += (a - g) * linc_num / den; + b += (a - b) * linc_num / den; + out[x] = SkPackARGB32(a, r, g, b); + } +} + +// Saturation changes modifications in RGB +// +// (Note that as a further complication, the values we deal in are +// premultiplied, so R/G/B values must be in the range 0..A. For mathematical +// purposes, one may as well use r=R/A, g=G/A, b=B/A. Without loss of +// generality, assume that R/G/B values are in the range 0..1.) +// +// Let Max = max(R,G,B), Min = min(R,G,B), and Med be the median value. Then L = +// (Max+Min)/2. If L is to remain constant, Max+Min must also remain constant. +// +// For H to remain constant, first, the (numerical) order of R/G/B (from +// smallest to largest) must remain the same. Second, all the ratios +// (R-G)/(Max-Min), (R-B)/(Max-Min), (G-B)/(Max-Min) must remain constant (of +// course, if Max = Min, then S = 0 and no saturation change is well-defined, +// since H is not well-defined). +// +// Let C_max be a colour with value Max, C_min be one with value Min, and C_med +// the remaining colour. Increasing saturation (to the maximum) is accomplished +// by increasing the value of C_max while simultaneously decreasing C_min and +// changing C_med so that the ratios are maintained; for the latter, it suffices +// to keep (C_med-C_min)/(C_max-C_min) constant (and equal to +// (Med-Min)/(Max-Min)). + +// Line processor: H no-op, S decrease, L no-op. +void LineProcHnopSdecLnop(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s >= 0 && hsl_shift.s <= 0.5 - HSLShift::epsilon); + DCHECK(hsl_shift.l < 0 || fabs(hsl_shift.l - 0.5) < HSLShift::epsilon); + + const int32_t denom = 65536; + int32_t s_numer = static_cast<int32_t>(hsl_shift.s * 2 * denom); + for (int x = 0; x < width; x++) { + int32_t a = static_cast<int32_t>(SkGetPackedA32(in[x])); + int32_t r = static_cast<int32_t>(SkGetPackedR32(in[x])); + int32_t g = static_cast<int32_t>(SkGetPackedG32(in[x])); + int32_t b = static_cast<int32_t>(SkGetPackedB32(in[x])); + + int32_t vmax, vmin; + if (r > g) { // This uses 3 compares rather than 4. + vmax = std::max(r, b); + vmin = std::min(g, b); + } else { + vmax = std::max(g, b); + vmin = std::min(r, b); + } + + // Use denom * L to avoid rounding. + int32_t denom_l = (vmax + vmin) * (denom / 2); + int32_t s_numer_l = (vmax + vmin) * s_numer / 2; + + r = (denom_l + r * s_numer - s_numer_l) / denom; + g = (denom_l + g * s_numer - s_numer_l) / denom; + b = (denom_l + b * s_numer - s_numer_l) / denom; + out[x] = SkPackARGB32(a, r, g, b); + } +} + +// Line processor: H no-op, S decrease, L decrease. +void LineProcHnopSdecLdec(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s >= 0 && hsl_shift.s <= 0.5 - HSLShift::epsilon); + DCHECK(hsl_shift.l >= 0 && hsl_shift.l <= 0.5 - HSLShift::epsilon); + + // Can't be too big since we need room for denom*denom and a bit for sign. + const int32_t denom = 1024; + int32_t l_numer = static_cast<int32_t>(hsl_shift.l * 2 * denom); + int32_t s_numer = static_cast<int32_t>(hsl_shift.s * 2 * denom); + for (int x = 0; x < width; x++) { + int32_t a = static_cast<int32_t>(SkGetPackedA32(in[x])); + int32_t r = static_cast<int32_t>(SkGetPackedR32(in[x])); + int32_t g = static_cast<int32_t>(SkGetPackedG32(in[x])); + int32_t b = static_cast<int32_t>(SkGetPackedB32(in[x])); + + int32_t vmax, vmin; + if (r > g) { // This uses 3 compares rather than 4. + vmax = std::max(r, b); + vmin = std::min(g, b); + } else { + vmax = std::max(g, b); + vmin = std::min(r, b); + } + + // Use denom * L to avoid rounding. + int32_t denom_l = (vmax + vmin) * (denom / 2); + int32_t s_numer_l = (vmax + vmin) * s_numer / 2; + + r = (denom_l + r * s_numer - s_numer_l) * l_numer / (denom * denom); + g = (denom_l + g * s_numer - s_numer_l) * l_numer / (denom * denom); + b = (denom_l + b * s_numer - s_numer_l) * l_numer / (denom * denom); + out[x] = SkPackARGB32(a, r, g, b); + } +} + +// Line processor: H no-op, S decrease, L increase. +void LineProcHnopSdecLinc(color_utils::HSL hsl_shift, const SkPMColor* in, + SkPMColor* out, int width) { + DCHECK(hsl_shift.h < 0); + DCHECK(hsl_shift.s >= 0 && hsl_shift.s <= 0.5 - HSLShift::epsilon); + DCHECK(hsl_shift.l >= 0.5 + HSLShift::epsilon && hsl_shift.l <= 1); + + // Can't be too big since we need room for denom*denom and a bit for sign. + const int32_t denom = 1024; + int32_t l_numer = static_cast<int32_t>((hsl_shift.l - 0.5) * 2 * denom); + int32_t s_numer = static_cast<int32_t>(hsl_shift.s * 2 * denom); + for (int x = 0; x < width; x++) { + int32_t a = static_cast<int32_t>(SkGetPackedA32(in[x])); + int32_t r = static_cast<int32_t>(SkGetPackedR32(in[x])); + int32_t g = static_cast<int32_t>(SkGetPackedG32(in[x])); + int32_t b = static_cast<int32_t>(SkGetPackedB32(in[x])); + + int32_t vmax, vmin; + if (r > g) { // This uses 3 compares rather than 4. + vmax = std::max(r, b); + vmin = std::min(g, b); + } else { + vmax = std::max(g, b); + vmin = std::min(r, b); + } + + // Use denom * L to avoid rounding. + int32_t denom_l = (vmax + vmin) * (denom / 2); + int32_t s_numer_l = (vmax + vmin) * s_numer / 2; + + r = denom_l + r * s_numer - s_numer_l; + g = denom_l + g * s_numer - s_numer_l; + b = denom_l + b * s_numer - s_numer_l; + + r = (r * denom + (a * denom - r) * l_numer) / (denom * denom); + g = (g * denom + (a * denom - g) * l_numer) / (denom * denom); + b = (b * denom + (a * denom - b) * l_numer) / (denom * denom); + out[x] = SkPackARGB32(a, r, g, b); + } +} + +const LineProcessor kLineProcessors[kNumHOps][kNumSOps][kNumLOps] = { + { // H: kOpHNone + { // S: kOpSNone + LineProcCopy, // L: kOpLNone + LineProcHnopSnopLdec, // L: kOpLDec + LineProcHnopSnopLinc // L: kOpLInc + }, + { // S: kOpSDec + LineProcHnopSdecLnop, // L: kOpLNone + LineProcHnopSdecLdec, // L: kOpLDec + LineProcHnopSdecLinc // L: kOpLInc + }, + { // S: kOpSInc + LineProcDefault, // L: kOpLNone + LineProcDefault, // L: kOpLDec + LineProcDefault // L: kOpLInc + } + }, + { // H: kOpHShift + { // S: kOpSNone + LineProcDefault, // L: kOpLNone + LineProcDefault, // L: kOpLDec + LineProcDefault // L: kOpLInc + }, + { // S: kOpSDec + LineProcDefault, // L: kOpLNone + LineProcDefault, // L: kOpLDec + LineProcDefault // L: kOpLInc + }, + { // S: kOpSInc + LineProcDefault, // L: kOpLNone + LineProcDefault, // L: kOpLDec + LineProcDefault // L: kOpLInc + } + } +}; + +} // namespace HSLShift +} // namespace // static SkBitmap SkBitmapOperations::CreateHSLShiftedBitmap( const SkBitmap& bitmap, color_utils::HSL hsl_shift) { + // Default to NOPs. + HSLShift::OperationOnH H_op = HSLShift::kOpHNone; + HSLShift::OperationOnS S_op = HSLShift::kOpSNone; + HSLShift::OperationOnL L_op = HSLShift::kOpLNone; + + if (hsl_shift.h >= 0 && hsl_shift.h <= 1) + H_op = HSLShift::kOpHShift; + + // Saturation shift: 0 -> fully desaturate, 0.5 -> NOP, 1 -> fully saturate. + if (hsl_shift.s >= 0 && hsl_shift.s <= (0.5 - HSLShift::epsilon)) + S_op = HSLShift::kOpSDec; + else if (hsl_shift.s >= (0.5 + HSLShift::epsilon)) + S_op = HSLShift::kOpSInc; + + // Lightness shift: 0 -> black, 0.5 -> NOP, 1 -> white. + if (hsl_shift.l >= 0 && hsl_shift.l <= (0.5 - HSLShift::epsilon)) + L_op = HSLShift::kOpLDec; + else if (hsl_shift.l >= (0.5 + HSLShift::epsilon)) + L_op = HSLShift::kOpLInc; + + HSLShift::LineProcessor line_proc = + HSLShift::kLineProcessors[H_op][S_op][L_op]; + DCHECK(bitmap.empty() == false); DCHECK(bitmap.config() == SkBitmap::kARGB_8888_Config); @@ -232,10 +549,7 @@ SkBitmap SkBitmapOperations::CreateHSLShiftedBitmap( SkPMColor* pixels = bitmap.getAddr32(0, y); SkPMColor* tinted_pixels = shifted.getAddr32(0, y); - for (int x = 0; x < bitmap.width(); ++x) { - tinted_pixels[x] = SkPreMultiplyColor(color_utils::HSLShift( - SkUnPreMultiply::PMColorToColor(pixels[x]), hsl_shift)); - } + (*line_proc)(hsl_shift, pixels, tinted_pixels, bitmap.width()); } return shifted; diff --git a/gfx/skbitmap_operations_unittest.cc b/gfx/skbitmap_operations_unittest.cc index 4082b22..e99f594 100644 --- a/gfx/skbitmap_operations_unittest.cc +++ b/gfx/skbitmap_operations_unittest.cc @@ -13,11 +13,31 @@ namespace { // Returns true if each channel of the given two colors are "close." This is // used for comparing colors where rounding errors may cause off-by-one. -bool ColorsClose(uint32_t a, uint32_t b) { - return abs(static_cast<int>(SkColorGetB(a) - SkColorGetB(b))) < 2 && - abs(static_cast<int>(SkColorGetG(a) - SkColorGetG(b))) < 2 && - abs(static_cast<int>(SkColorGetR(a) - SkColorGetR(b))) < 2 && - abs(static_cast<int>(SkColorGetA(a) - SkColorGetA(b))) < 2; +inline bool ColorsClose(uint32_t a, uint32_t b) { + return abs(static_cast<int>(SkColorGetB(a) - SkColorGetB(b))) <= 2 && + abs(static_cast<int>(SkColorGetG(a) - SkColorGetG(b))) <= 2 && + abs(static_cast<int>(SkColorGetR(a) - SkColorGetR(b))) <= 2 && + abs(static_cast<int>(SkColorGetA(a) - SkColorGetA(b))) <= 2; +} + +inline bool MultipliedColorsClose(uint32_t a, uint32_t b) { + return ColorsClose(SkUnPreMultiply::PMColorToColor(a), + SkUnPreMultiply::PMColorToColor(b)); +} + +bool BitmapsClose(const SkBitmap& a, const SkBitmap& b) { + SkAutoLockPixels a_lock(a); + SkAutoLockPixels b_lock(b); + + for (int y = 0; y < a.height(); y++) { + for (int x = 0; x < a.width(); x++) { + SkColor a_pixel = *a.getAddr32(x, y); + SkColor b_pixel = *b.getAddr32(x, y); + if (!ColorsClose(a_pixel, b_pixel)) + return false; + } + } + return true; } void FillDataToBitmap(int w, int h, SkBitmap* bmp) { @@ -34,6 +54,34 @@ void FillDataToBitmap(int w, int h, SkBitmap* bmp) { } } +// The reference (i.e., old) implementation of |CreateHSLShiftedBitmap()|. +SkBitmap ReferenceCreateHSLShiftedBitmap( + const SkBitmap& bitmap, + color_utils::HSL hsl_shift) { + SkBitmap shifted; + shifted.setConfig(SkBitmap::kARGB_8888_Config, bitmap.width(), + bitmap.height(), 0); + shifted.allocPixels(); + shifted.eraseARGB(0, 0, 0, 0); + shifted.setIsOpaque(false); + + SkAutoLockPixels lock_bitmap(bitmap); + SkAutoLockPixels lock_shifted(shifted); + + // Loop through the pixels of the original bitmap. + for (int y = 0; y < bitmap.height(); ++y) { + SkPMColor* pixels = bitmap.getAddr32(0, y); + SkPMColor* tinted_pixels = shifted.getAddr32(0, y); + + for (int x = 0; x < bitmap.width(); ++x) { + tinted_pixels[x] = SkPreMultiplyColor(color_utils::HSLShift( + SkUnPreMultiply::PMColorToColor(pixels[x]), hsl_shift)); + } + } + + return shifted; +} + } // namespace // Invert bitmap and verify the each pixel is inverted and the alpha value is @@ -165,31 +213,38 @@ TEST(SkBitmapOperationsTest, CreateMaskedBitmap) { // the end result is close enough to the original (rounding errors // notwithstanding). TEST(SkBitmapOperationsTest, CreateHSLShiftedBitmapToSame) { - int src_w = 4, src_h = 4; + int src_w = 16, src_h = 16; SkBitmap src; src.setConfig(SkBitmap::kARGB_8888_Config, src_w, src_h); src.allocPixels(); for (int y = 0, i = 0; y < src_h; y++) { for (int x = 0; x < src_w; x++) { - *src.getAddr32(x, y) = SkColorSetARGB(i + 128 % 255, - i + 128 % 255, i + 64 % 255, i + 0 % 255); + *src.getAddr32(x, y) = SkPreMultiplyColor(SkColorSetARGB((i + 128) % 255, + (i + 128) % 255, (i + 64) % 255, (i + 0) % 255)); i++; } } color_utils::HSL hsl = { -1, -1, -1 }; - - SkBitmap shifted = SkBitmapOperations::CreateHSLShiftedBitmap(src, hsl); + SkBitmap shifted = ReferenceCreateHSLShiftedBitmap(src, hsl); SkAutoLockPixels src_lock(src); SkAutoLockPixels shifted_lock(shifted); - for (int y = 0; y < src_w; y++) { - for (int x = 0; x < src_h; x++) { + for (int y = 0; y < src_h; y++) { + for (int x = 0; x < src_w; x++) { SkColor src_pixel = *src.getAddr32(x, y); SkColor shifted_pixel = *shifted.getAddr32(x, y); - EXPECT_TRUE(ColorsClose(src_pixel, shifted_pixel)); + EXPECT_TRUE(MultipliedColorsClose(src_pixel, shifted_pixel)) << + "source: (a,r,g,b) = (" << SkColorGetA(src_pixel) << "," << + SkColorGetR(src_pixel) << "," << + SkColorGetG(src_pixel) << "," << + SkColorGetB(src_pixel) << "); " << + "shifted: (a,r,g,b) = (" << SkColorGetA(shifted_pixel) << "," << + SkColorGetR(shifted_pixel) << "," << + SkColorGetG(shifted_pixel) << "," << + SkColorGetB(shifted_pixel) << ")"; } } } @@ -225,6 +280,41 @@ TEST(SkBitmapOperationsTest, CreateHSLShiftedBitmapHueOnly) { } } +// Validate HSL shift. +TEST(SkBitmapOperationsTest, ValidateHSLShift) { + // Note: 255/51 = 5 (exactly) => 6 including 0! + const int inc = 51; + const int dim = 255 / inc + 1; + SkBitmap src; + src.setConfig(SkBitmap::kARGB_8888_Config, dim*dim, dim*dim); + src.allocPixels(); + + for (int a = 0, y = 0; a <= 255; a += inc) { + for (int r = 0; r <= 255; r += inc, y++) { + for (int g = 0, x = 0; g <= 255; g += inc) { + for (int b = 0; b <= 255; b+= inc, x++) { + *src.getAddr32(x, y) = + SkPreMultiplyColor(SkColorSetARGB(a, r, g, b)); + } + } + } + } + + // Shhhh. The spec says I should set things to -1 for "no change", but + // actually -0.1 will do. Don't tell anyone I did this. + for (double h = -0.1; h <= 1.0001; h += 0.1) { + for (double s = -0.1; s <= 1.0001; s += 0.1) { + for (double l = -0.1; l <= 1.0001; l += 0.1) { + color_utils::HSL hsl = { h, s, l }; + SkBitmap ref_shifted = ReferenceCreateHSLShiftedBitmap(src, hsl); + SkBitmap shifted = SkBitmapOperations::CreateHSLShiftedBitmap(src, hsl); + EXPECT_TRUE(BitmapsClose(ref_shifted, shifted)) + << "h = " << h << ", s = " << s << ", l = " << l; + } + } + } +} + // Test our cropping. TEST(SkBitmapOperationsTest, CreateCroppedBitmap) { int src_w = 16, src_h = 16; |