Commit e60e1e6e authored by Becca Hughes's avatar Becca Hughes Committed by Commit Bot

[Media Notification] Derive background color from artwork

The media notification should derive its background
color from prominent colors in the artwork. This
uses as similar algorithm from Android.

Also breaks out some of the logic of CalculateProminentColors
so we can use it without the reset of the function. This
is because we want uninteresting colors and do not want
the color profile matching.

BUG=944598

Change-Id: I60df7f600f6fc881dfb38d2057c903a5679845df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1534923
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Reviewed-by: default avatarSadrul Chowdhury <sadrul@chromium.org>
Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Reviewed-by: default avatarTommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#644985}
parent af05d331
...@@ -5,8 +5,12 @@ ...@@ -5,8 +5,12 @@
#include "ash/media/media_notification_background.h" #include "ash/media/media_notification_background.h"
#include <algorithm> #include <algorithm>
#include <vector>
#include "skia/ext/image_operations.h"
#include "ui/gfx/canvas.h" #include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/scoped_canvas.h" #include "ui/gfx/scoped_canvas.h"
#include "ui/views/view.h" #include "ui/views/view.h"
...@@ -16,7 +20,66 @@ namespace { ...@@ -16,7 +20,66 @@ namespace {
constexpr int kMediaImageGradientWidth = 40; constexpr int kMediaImageGradientWidth = 40;
constexpr SkColor kMediaNotificationBackgroundColor = SK_ColorWHITE; constexpr SkColor kMediaNotificationDefaultBackgroundColor = SK_ColorWHITE;
// The ratio for a background color option to be considered very popular.
constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
bool IsNearlyWhiteOrBlack(SkColor color) {
color_utils::HSL hsl;
color_utils::SkColorToHSL(color, &hsl);
return hsl.l >= 0.9 || hsl.l <= 0.08;
}
base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
if (!source || source->empty())
return base::nullopt;
std::vector<color_utils::Swatch> swatches =
color_utils::CalculateColorSwatches(
*source, 16, gfx::Rect(source->width() / 2, source->height()),
false /* exclude_uninteresting */);
if (swatches.empty())
return base::nullopt;
base::Optional<color_utils::Swatch> most_popular;
base::Optional<color_utils::Swatch> non_white_black;
// Find the most popular color with the most weight and the color which
// is the color with the most weight that is not white or black.
for (auto& swatch : swatches) {
if (!IsNearlyWhiteOrBlack(swatch.color) &&
(!non_white_black || swatch.population > non_white_black->population)) {
non_white_black = swatch;
}
if (most_popular && swatch.population < most_popular->population)
continue;
most_popular = swatch;
}
DCHECK(most_popular);
// If the most popular color is not white or black then we should use that.
if (!IsNearlyWhiteOrBlack(most_popular->color))
return most_popular->color;
// If we could not find a color that is not white or black then we should
// use the most popular color.
if (!non_white_black)
return most_popular->color;
// If the most popular color is very popular then we should use that color.
if (static_cast<double>(most_popular->population) /
non_white_black->population >
kMediaNotificationBackgroundColorVeryPopularRatio) {
return most_popular->color;
}
return non_white_black->color;
}
} // namespace } // namespace
...@@ -32,6 +95,8 @@ MediaNotificationBackground::MediaNotificationBackground( ...@@ -32,6 +95,8 @@ MediaNotificationBackground::MediaNotificationBackground(
DCHECK(owner); DCHECK(owner);
} }
MediaNotificationBackground::~MediaNotificationBackground() = default;
void MediaNotificationBackground::Paint(gfx::Canvas* canvas, void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
views::View* view) const { views::View* view) const {
DCHECK(view); DCHECK(view);
...@@ -70,18 +135,16 @@ void MediaNotificationBackground::Paint(gfx::Canvas* canvas, ...@@ -70,18 +135,16 @@ void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
// Draw a filled rectangle which will act as the main background of the // Draw a filled rectangle which will act as the main background of the
// notification. This may cover up some of the artwork. // notification. This may cover up some of the artwork.
canvas->FillRect(GetFilledBackgroundBounds(bounds), const SkColor background_color =
kMediaNotificationBackgroundColor); background_color_.value_or(kMediaNotificationDefaultBackgroundColor);
canvas->FillRect(GetFilledBackgroundBounds(bounds), background_color);
{ {
// Draw a gradient to fade the color background and the image together. // Draw a gradient to fade the color background and the image together.
gfx::Rect draw_bounds = GetGradientBounds(bounds); gfx::Rect draw_bounds = GetGradientBounds(bounds);
const SkColor transparent = const SkColor colors[2] = {
SkColorSetA(kMediaNotificationBackgroundColor, 0); background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
const SkColor colors[2] = {kMediaNotificationBackgroundColor, transparent};
const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.left_center()), const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.left_center()),
gfx::PointToSkPoint(draw_bounds.right_center())}; gfx::PointToSkPoint(draw_bounds.right_center())};
...@@ -100,6 +163,7 @@ void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) { ...@@ -100,6 +163,7 @@ void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) {
return; return;
artwork_ = image; artwork_ = image;
background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
owner_->SchedulePaint(); owner_->SchedulePaint();
} }
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#define ASH_MEDIA_MEDIA_NOTIFICATION_BACKGROUND_H_ #define ASH_MEDIA_MEDIA_NOTIFICATION_BACKGROUND_H_
#include "ash/ash_export.h" #include "ash/ash_export.h"
#include "base/optional.h"
#include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_skia.h"
#include "ui/views/background.h" #include "ui/views/background.h"
...@@ -29,6 +30,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background { ...@@ -29,6 +30,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
int top_radius, int top_radius,
int bottom_radius, int bottom_radius,
double artwork_max_width_pct); double artwork_max_width_pct);
~MediaNotificationBackground() override;
// views::Background // views::Background
void Paint(gfx::Canvas* canvas, views::View* view) const override; void Paint(gfx::Canvas* canvas, views::View* view) const override;
...@@ -38,6 +40,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background { ...@@ -38,6 +40,7 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
void UpdateArtworkMaxWidthPct(double max_width_pct); void UpdateArtworkMaxWidthPct(double max_width_pct);
private: private:
friend class MediaNotificationBackgroundTest;
friend class MediaNotificationViewTest; friend class MediaNotificationViewTest;
FRIEND_TEST_ALL_PREFIXES(MediaNotificationBackgroundTest, BoundsSanityCheck); FRIEND_TEST_ALL_PREFIXES(MediaNotificationBackgroundTest, BoundsSanityCheck);
...@@ -56,6 +59,8 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background { ...@@ -56,6 +59,8 @@ class ASH_EXPORT MediaNotificationBackground : public views::Background {
gfx::ImageSkia artwork_; gfx::ImageSkia artwork_;
double artwork_max_width_pct_; double artwork_max_width_pct_;
base::Optional<SkColor> background_color_;
DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackground); DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackground);
}; };
......
...@@ -11,53 +11,172 @@ ...@@ -11,53 +11,172 @@
namespace ash { namespace ash {
namespace {
gfx::ImageSkia CreateTestBackgroundImage(SkColor first_color,
SkColor second_color,
int second_height) {
constexpr SkColor kRightHandSideColor = SK_ColorMAGENTA;
DCHECK_NE(kRightHandSideColor, first_color);
DCHECK_NE(kRightHandSideColor, second_color);
SkBitmap bitmap;
bitmap.allocN32Pixels(100, 100);
int first_height = bitmap.height() - second_height;
int right_width = bitmap.width() / 2;
// Fill the right hand side of the image with a constant color. The color
// derivation algorithm does not look at the right hand side so we should
// never see |kRightHandSideColor|.
bitmap.erase(kRightHandSideColor,
{right_width, 0, bitmap.width(), bitmap.height()});
// Fill the left hand side with |first_color|.
bitmap.erase(first_color, {0, 0, right_width, first_height});
// Fill the left hand side with |second_color|.
bitmap.erase(second_color, {0, first_height, right_width, bitmap.height()});
return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
}
gfx::ImageSkia CreateTestBackgroundImage(SkColor color) {
return CreateTestBackgroundImage(color, SK_ColorTRANSPARENT, 0);
}
} // namespace
class MediaNotificationBackgroundTest : public AshTestBase { class MediaNotificationBackgroundTest : public AshTestBase {
public: public:
MediaNotificationBackgroundTest() = default; MediaNotificationBackgroundTest() = default;
~MediaNotificationBackgroundTest() override = default; ~MediaNotificationBackgroundTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
owner_ = std::make_unique<views::StaticSizedView>();
background_ = std::make_unique<MediaNotificationBackground>(owner_.get(),
10, 10, 0.1);
EXPECT_FALSE(GetBackgroundColor().has_value());
}
void TearDown() override {
background_.reset();
owner_.reset();
AshTestBase::TearDown();
}
MediaNotificationBackground* background() const { return background_.get(); }
base::Optional<SkColor> GetBackgroundColor() const {
return background_->background_color_;
}
private: private:
std::unique_ptr<views::StaticSizedView> owner_;
std::unique_ptr<MediaNotificationBackground> background_;
DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundTest); DISALLOW_COPY_AND_ASSIGN(MediaNotificationBackgroundTest);
}; };
TEST_F(MediaNotificationBackgroundTest, BoundsSanityCheck) { TEST_F(MediaNotificationBackgroundTest, BoundsSanityCheck) {
views::StaticSizedView owner;
MediaNotificationBackground background(&owner, 10, 10, 0.1);
// The test notification will have a width of 200 and a height of 50. // The test notification will have a width of 200 and a height of 50.
gfx::Rect bounds(0, 0, 200, 50); gfx::Rect bounds(0, 0, 200, 50);
// Check the artwork is not visible by default. // Check the artwork is not visible by default.
EXPECT_EQ(0, background.GetArtworkWidth(bounds.size())); EXPECT_EQ(0, background()->GetArtworkWidth(bounds.size()));
EXPECT_EQ(0, background.GetArtworkVisibleWidth(bounds.size())); EXPECT_EQ(0, background()->GetArtworkVisibleWidth(bounds.size()));
EXPECT_EQ(gfx::Rect(200, 0, 0, 50), background.GetArtworkBounds(bounds)); EXPECT_EQ(gfx::Rect(200, 0, 0, 50), background()->GetArtworkBounds(bounds));
EXPECT_EQ(gfx::Rect(0, 0, 200, 50), EXPECT_EQ(gfx::Rect(0, 0, 200, 50),
background.GetFilledBackgroundBounds(bounds)); background()->GetFilledBackgroundBounds(bounds));
EXPECT_EQ(gfx::Rect(0, 0, 0, 0), background.GetGradientBounds(bounds)); EXPECT_EQ(gfx::Rect(0, 0, 0, 0), background()->GetGradientBounds(bounds));
// The background artwork image will have an aspect ratio of 2:1. // The background artwork image will have an aspect ratio of 2:1.
SkBitmap bitmap; SkBitmap bitmap;
bitmap.allocN32Pixels(20, 10); bitmap.allocN32Pixels(20, 10);
background.UpdateArtwork(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); background()->UpdateArtwork(gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
// The artwork width will be 2x the height of the notification and the visible // The artwork width will be 2x the height of the notification and the visible
// width will be limited to 10% the width of the notification. // width will be limited to 10% the width of the notification.
EXPECT_EQ(100, background.GetArtworkWidth(bounds.size())); EXPECT_EQ(100, background()->GetArtworkWidth(bounds.size()));
EXPECT_EQ(20, background.GetArtworkVisibleWidth(bounds.size())); EXPECT_EQ(20, background()->GetArtworkVisibleWidth(bounds.size()));
// Update the visible width % to be greater than the width of the image. // Update the visible width % to be greater than the width of the image.
background.UpdateArtworkMaxWidthPct(0.6); background()->UpdateArtworkMaxWidthPct(0.6);
EXPECT_EQ(100, background.GetArtworkVisibleWidth(bounds.size())); EXPECT_EQ(100, background()->GetArtworkVisibleWidth(bounds.size()));
// Check the artwork is positioned to the right. // Check the artwork is positioned to the right.
EXPECT_EQ(gfx::Rect(100, 0, 100, 50), background.GetArtworkBounds(bounds)); EXPECT_EQ(gfx::Rect(100, 0, 100, 50), background()->GetArtworkBounds(bounds));
// Check the filled background is to the left of the image. // Check the filled background is to the left of the image.
EXPECT_EQ(gfx::Rect(0, 0, 100, 50), EXPECT_EQ(gfx::Rect(0, 0, 100, 50),
background.GetFilledBackgroundBounds(bounds)); background()->GetFilledBackgroundBounds(bounds));
// Check the gradient is positioned above the artwork. // Check the gradient is positioned above the artwork.
EXPECT_EQ(gfx::Rect(100, 0, 40, 50), background.GetGradientBounds(bounds)); EXPECT_EQ(gfx::Rect(100, 0, 40, 50), background()->GetGradientBounds(bounds));
}
// If we have no artwork then we should use the default background color.
TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoArtwork) {
background()->UpdateArtwork(gfx::ImageSkia());
EXPECT_FALSE(GetBackgroundColor().has_value());
}
// If we have artwork with no popular color then we should use the default
// background color.
TEST_F(MediaNotificationBackgroundTest, DeriveBackgroundColor_NoPopularColor) {
background()->UpdateArtwork(CreateTestBackgroundImage(SK_ColorTRANSPARENT));
EXPECT_FALSE(GetBackgroundColor().has_value());
}
// If the most popular color is not white or black then we should use that
// color.
TEST_F(MediaNotificationBackgroundTest,
DeriveBackgroundColor_PopularNonWhiteBlackColor) {
constexpr SkColor kTestColor = SK_ColorYELLOW;
background()->UpdateArtwork(CreateTestBackgroundImage(kTestColor));
EXPECT_EQ(kTestColor, GetBackgroundColor());
}
// MediaNotificationBackgroundBlackWhiteTest will repeat these tests with a
// parameter that is either black or white.
class MediaNotificationBackgroundBlackWhiteTest
: public MediaNotificationBackgroundTest,
public testing::WithParamInterface<SkColor> {};
INSTANTIATE_TEST_SUITE_P(,
MediaNotificationBackgroundBlackWhiteTest,
testing::Values(SK_ColorBLACK, SK_ColorWHITE));
// If the most popular color is black or white but there is no secondary color
// we should use the most popular color.
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
DeriveBackgroundColor_PopularBlackWhiteNoSecondaryColor) {
background()->UpdateArtwork(CreateTestBackgroundImage(GetParam()));
EXPECT_EQ(GetParam(), GetBackgroundColor());
}
// If the most popular color is black or white and there is a secondary color
// that is very minor then we should use the most popular color.
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
DeriveBackgroundColor_VeryPopularBlackWhite) {
background()->UpdateArtwork(
CreateTestBackgroundImage(GetParam(), SK_ColorYELLOW, 20));
EXPECT_EQ(GetParam(), GetBackgroundColor());
}
// If the most popular color is black or white but it is not that popular then
// we should use the secondary color.
TEST_P(MediaNotificationBackgroundBlackWhiteTest,
DeriveBackgroundColor_NotVeryPopularBlackWhite) {
constexpr SkColor kTestColor = SK_ColorYELLOW;
background()->UpdateArtwork(
CreateTestBackgroundImage(GetParam(), kTestColor, 40));
EXPECT_EQ(kTestColor, GetBackgroundColor());
} }
} // namespace ash } // namespace ash
This diff is collapsed.
...@@ -16,6 +16,10 @@ ...@@ -16,6 +16,10 @@
class SkBitmap; class SkBitmap;
namespace gfx {
class Rect;
} // namespace gfx
namespace color_utils { namespace color_utils {
struct HSL; struct HSL;
...@@ -136,6 +140,35 @@ struct ColorProfile { ...@@ -136,6 +140,35 @@ struct ColorProfile {
SaturationRange saturation = SaturationRange::MUTED; SaturationRange saturation = SaturationRange::MUTED;
}; };
// A color value with an associated weight.
struct Swatch {
Swatch(SkColor color, size_t population)
: color(color), population(population) {}
SkColor color;
// The population correlates to a count, so it should be 1 or greater.
size_t population;
bool operator==(const Swatch& other) const {
return color == other.color && population == other.population;
}
};
// Returns a vector of |Swatch| that represent the prominent colors of the
// bitmap within |region|. The |max_swatches| is the maximum number of swatches.
// For landscapes, good values are in the range 12-16. For images which are
// largely made up of people's faces then this value should be increased to
// 24-32. |exclude_uninteresting| will exclude colors that are not interesting
// (e.g. too white or black).
// This is an implementation of the Android Palette API:
// https://developer.android.com/reference/android/support/v7/graphics/Palette
GFX_EXPORT std::vector<Swatch> CalculateColorSwatches(
const SkBitmap& bitmap,
size_t max_swatches,
const gfx::Rect& region,
bool exclude_uninteresting);
// Returns a vector of RGB colors that represents the bitmap based on the // Returns a vector of RGB colors that represents the bitmap based on the
// |color_profiles| provided. For each value, if a value is succesfully // |color_profiles| provided. For each value, if a value is succesfully
// calculated, the calculated value is fully opaque. For failure, the calculated // calculated, the calculated value is fully opaque. For failure, the calculated
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <exception>
#include <vector> #include <vector>
#include "skia/ext/platform_canvas.h" #include "skia/ext/platform_canvas.h"
...@@ -546,4 +547,67 @@ TEST_F(ColorAnalysisTest, ComputeProminentColors) { ...@@ -546,4 +547,67 @@ TEST_F(ColorAnalysisTest, ComputeProminentColors) {
EXPECT_EQ(expectations, computations); EXPECT_EQ(expectations, computations);
} }
TEST_F(ColorAnalysisTest, ComputeColorSwatches) {
SkBitmap bitmap;
bitmap.allocN32Pixels(100, 100);
bitmap.eraseColor(SK_ColorMAGENTA);
bitmap.erase(SK_ColorGREEN, {10, 10, 90, 90});
bitmap.erase(SK_ColorYELLOW, {40, 40, 60, 60});
const Swatch kYellowSwatch = Swatch(SK_ColorYELLOW, (20u * 20u));
const Swatch kGreenSwatch =
Swatch(SK_ColorGREEN, (80u * 80u) - kYellowSwatch.population);
const Swatch kMagentaSwatch =
Swatch(SK_ColorMAGENTA, (100u * 100u) - kGreenSwatch.population -
kYellowSwatch.population);
{
std::vector<Swatch> colors =
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), false);
EXPECT_EQ(3u, colors.size());
EXPECT_EQ(kGreenSwatch, colors[0]);
EXPECT_EQ(kMagentaSwatch, colors[1]);
EXPECT_EQ(kYellowSwatch, colors[2]);
}
{
std::vector<Swatch> colors =
CalculateColorSwatches(bitmap, 10, gfx::Rect(10, 10, 80, 80), false);
EXPECT_EQ(2u, colors.size());
EXPECT_EQ(kGreenSwatch, colors[0]);
EXPECT_EQ(kYellowSwatch, colors[1]);
}
}
TEST_F(ColorAnalysisTest, ComputeColorSwatches_Uninteresting) {
SkBitmap bitmap;
bitmap.allocN32Pixels(100, 100);
bitmap.eraseColor(SK_ColorMAGENTA);
bitmap.erase(SK_ColorBLACK, {10, 10, 90, 90});
bitmap.erase(SK_ColorWHITE, {40, 40, 60, 60});
const Swatch kWhiteSwatch = Swatch(SK_ColorWHITE, (20u * 20u));
const Swatch kBlackSwatch =
Swatch(SK_ColorBLACK, (80u * 80u) - kWhiteSwatch.population);
const Swatch kMagentaSwatch =
Swatch(SK_ColorMAGENTA,
(100u * 100u) - kBlackSwatch.population - kWhiteSwatch.population);
{
std::vector<Swatch> colors =
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), true);
EXPECT_EQ(1u, colors.size());
EXPECT_EQ(kMagentaSwatch, colors[0]);
}
{
std::vector<Swatch> colors =
CalculateColorSwatches(bitmap, 10, gfx::Rect(100, 100), false);
EXPECT_EQ(3u, colors.size());
EXPECT_EQ(kBlackSwatch, colors[0]);
EXPECT_EQ(kMagentaSwatch, colors[1]);
EXPECT_EQ(kWhiteSwatch, colors[2]);
}
}
} // namespace color_utils } // namespace color_utils
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment