1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/media_message_center/media_notification_background.h"
6
7 #include <algorithm>
8 #include <vector>
9
10 #include "skia/ext/image_operations.h"
11 #include "ui/gfx/canvas.h"
12 #include "ui/gfx/color_analysis.h"
13 #include "ui/gfx/color_utils.h"
14 #include "ui/gfx/scoped_canvas.h"
15 #include "ui/native_theme/native_theme.h"
16 #include "ui/views/style/typography.h"
17 #include "ui/views/view.h"
18
19 namespace media_message_center {
20
21 namespace {
22
23 constexpr int kMediaImageGradientWidth = 40;
24
25 // The ratio for a background color option to be considered very popular.
26 constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
27
28 // The ratio for the most popular foreground color to be used.
29 constexpr double kMediaNotificationForegroundColorMostPopularRatio = 0.01;
30
31 // The minimum saturation for the most popular foreground color to be used.
32 constexpr double kMediaNotificationForegroundColorMostPopularMinSaturation =
33 0.19;
34
35 // The ratio for the more vibrant foreground color to use.
36 constexpr double kMediaNotificationForegroundColorMoreVibrantRatio = 1.0;
37
38 constexpr float kMediaNotificationMinimumContrastRatio = 7.0;
39
IsNearlyWhiteOrBlack(SkColor color)40 bool IsNearlyWhiteOrBlack(SkColor color) {
41 color_utils::HSL hsl;
42 color_utils::SkColorToHSL(color, &hsl);
43 return hsl.l >= 0.9 || hsl.l <= 0.08;
44 }
45
GetHueDegrees(const SkColor & color)46 int GetHueDegrees(const SkColor& color) {
47 color_utils::HSL hsl;
48 color_utils::SkColorToHSL(color, &hsl);
49 return hsl.h * 360;
50 }
51
GetSaturation(const color_utils::Swatch & swatch)52 double GetSaturation(const color_utils::Swatch& swatch) {
53 color_utils::HSL hsl;
54 color_utils::SkColorToHSL(swatch.color, &hsl);
55 return hsl.s;
56 }
57
IsForegroundColorSwatchAllowed(const SkColor & background,const SkColor & candidate)58 bool IsForegroundColorSwatchAllowed(const SkColor& background,
59 const SkColor& candidate) {
60 if (IsNearlyWhiteOrBlack(candidate))
61 return false;
62
63 if (IsNearlyWhiteOrBlack(background))
64 return true;
65
66 int diff = abs(GetHueDegrees(candidate) - GetHueDegrees(background));
67 return diff > 10 && diff < 350;
68 }
69
GetNotificationBackgroundColor(const SkBitmap * source)70 base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
71 if (!source || source->empty() || source->isNull())
72 return base::nullopt;
73
74 std::vector<color_utils::Swatch> swatches =
75 color_utils::CalculateColorSwatches(
76 *source, 16, gfx::Rect(source->width() / 2, source->height()),
77 base::nullopt);
78
79 if (swatches.empty())
80 return base::nullopt;
81
82 base::Optional<color_utils::Swatch> most_popular;
83 base::Optional<color_utils::Swatch> non_white_black;
84
85 // Find the most popular color with the most weight and the color which
86 // is the color with the most weight that is not white or black.
87 for (auto& swatch : swatches) {
88 if (!IsNearlyWhiteOrBlack(swatch.color) &&
89 (!non_white_black || swatch.population > non_white_black->population)) {
90 non_white_black = swatch;
91 }
92
93 if (most_popular && swatch.population < most_popular->population)
94 continue;
95
96 most_popular = swatch;
97 }
98
99 DCHECK(most_popular);
100
101 // If the most popular color is not white or black then we should use that.
102 if (!IsNearlyWhiteOrBlack(most_popular->color))
103 return most_popular->color;
104
105 // If we could not find a color that is not white or black then we should
106 // use the most popular color.
107 if (!non_white_black)
108 return most_popular->color;
109
110 // If the most popular color is very popular then we should use that color.
111 if (static_cast<double>(most_popular->population) /
112 non_white_black->population >
113 kMediaNotificationBackgroundColorVeryPopularRatio) {
114 return most_popular->color;
115 }
116
117 return non_white_black->color;
118 }
119
SelectVibrantSwatch(const color_utils::Swatch & more_vibrant,const color_utils::Swatch & vibrant)120 color_utils::Swatch SelectVibrantSwatch(const color_utils::Swatch& more_vibrant,
121 const color_utils::Swatch& vibrant) {
122 if ((static_cast<double>(more_vibrant.population) / vibrant.population) <
123 kMediaNotificationForegroundColorMoreVibrantRatio) {
124 return vibrant;
125 }
126
127 return more_vibrant;
128 }
129
SelectMutedSwatch(const color_utils::Swatch & muted,const color_utils::Swatch & more_muted)130 color_utils::Swatch SelectMutedSwatch(const color_utils::Swatch& muted,
131 const color_utils::Swatch& more_muted) {
132 double population_ratio =
133 static_cast<double>(muted.population) / more_muted.population;
134
135 // Use the swatch with the higher saturation ratio.
136 return (GetSaturation(muted) * population_ratio > GetSaturation(more_muted))
137 ? muted
138 : more_muted;
139 }
140
GetNotificationForegroundColor(const base::Optional<SkColor> & background_color,const SkBitmap * source)141 base::Optional<SkColor> GetNotificationForegroundColor(
142 const base::Optional<SkColor>& background_color,
143 const SkBitmap* source) {
144 if (!background_color || !source || source->empty() || source->isNull())
145 return base::nullopt;
146
147 const bool is_light =
148 color_utils::GetRelativeLuminance(*background_color) > 0.5;
149 const SkColor fallback_color = is_light ? SK_ColorBLACK : SK_ColorWHITE;
150
151 gfx::Rect bitmap_area(source->width(), source->height());
152 bitmap_area.Inset(source->width() * 0.4, 0, 0, 0);
153
154 // If the background color is dark we want to look for colors that are darker
155 // and vice versa.
156 const color_utils::LumaRange more_luma_range =
157 is_light ? color_utils::LumaRange::DARK : color_utils::LumaRange::LIGHT;
158
159 std::vector<color_utils::ColorProfile> color_profiles;
160 color_profiles.push_back(color_utils::ColorProfile(
161 more_luma_range, color_utils::SaturationRange::VIBRANT));
162 color_profiles.push_back(color_utils::ColorProfile(
163 color_utils::LumaRange::NORMAL, color_utils::SaturationRange::VIBRANT));
164 color_profiles.push_back(color_utils::ColorProfile(
165 color_utils::LumaRange::NORMAL, color_utils::SaturationRange::MUTED));
166 color_profiles.push_back(color_utils::ColorProfile(
167 more_luma_range, color_utils::SaturationRange::MUTED));
168 color_profiles.push_back(color_utils::ColorProfile(
169 color_utils::LumaRange::ANY, color_utils::SaturationRange::ANY));
170
171 std::vector<color_utils::Swatch> best_swatches =
172 color_utils::CalculateProminentColorsOfBitmap(
173 *source, color_profiles, &bitmap_area,
174 base::BindRepeating(&IsForegroundColorSwatchAllowed,
175 background_color.value()));
176
177 if (best_swatches.empty())
178 return fallback_color;
179
180 DCHECK_EQ(5u, best_swatches.size());
181
182 const color_utils::Swatch& more_vibrant = best_swatches[0];
183 const color_utils::Swatch& vibrant = best_swatches[1];
184 const color_utils::Swatch& muted = best_swatches[2];
185 const color_utils::Swatch& more_muted = best_swatches[3];
186 const color_utils::Swatch& most_popular = best_swatches[4];
187
188 // We are looking for a fraction that is at least 0.2% of the image.
189 const size_t population_min =
190 std::min(bitmap_area.width() * bitmap_area.height(),
191 color_utils::kMaxConsideredPixelsForSwatches) *
192 0.002;
193
194 // This selection algorithm is an implementation of MediaNotificationProcessor
195 // from Android. It will select more vibrant colors first since they stand out
196 // better against the background. If not, it will fallback to muted colors,
197 // the most popular color and then either white/black. Any swatch has to be
198 // above a minimum population threshold to be determined significant enough in
199 // the artwork to be used.
200 base::Optional<color_utils::Swatch> swatch;
201 if (more_vibrant.population > population_min &&
202 vibrant.population > population_min) {
203 swatch = SelectVibrantSwatch(more_vibrant, vibrant);
204 } else if (more_vibrant.population > population_min) {
205 swatch = more_vibrant;
206 } else if (vibrant.population > population_min) {
207 swatch = vibrant;
208 } else if (muted.population > population_min &&
209 more_muted.population > population_min) {
210 swatch = SelectMutedSwatch(muted, more_muted);
211 } else if (muted.population > population_min) {
212 swatch = muted;
213 } else if (more_muted.population > population_min) {
214 swatch = more_muted;
215 } else if (most_popular.population > population_min) {
216 return most_popular.color;
217 } else {
218 return fallback_color;
219 }
220
221 if (most_popular == *swatch)
222 return swatch->color;
223
224 if (static_cast<double>(swatch->population) / most_popular.population <
225 kMediaNotificationForegroundColorMostPopularRatio &&
226 GetSaturation(most_popular) >
227 kMediaNotificationForegroundColorMostPopularMinSaturation) {
228 return most_popular.color;
229 }
230
231 return swatch->color;
232 }
233
234 } // namespace
235
MediaNotificationBackground(int top_radius,int bottom_radius,double artwork_max_width_pct)236 MediaNotificationBackground::MediaNotificationBackground(
237 int top_radius,
238 int bottom_radius,
239 double artwork_max_width_pct)
240 : top_radius_(top_radius),
241 bottom_radius_(bottom_radius),
242 artwork_max_width_pct_(artwork_max_width_pct) {}
243
244 MediaNotificationBackground::~MediaNotificationBackground() = default;
245
Paint(gfx::Canvas * canvas,views::View * view) const246 void MediaNotificationBackground::Paint(gfx::Canvas* canvas,
247 views::View* view) const {
248 DCHECK(view);
249
250 gfx::ScopedCanvas scoped_canvas(canvas);
251 gfx::Rect bounds = view->GetContentsBounds();
252
253 {
254 // Draw a rounded rectangle which the background will be clipped to. The
255 // radius is provided by the notification and can change based on where in
256 // the list the notification is.
257 const SkScalar top_radius = SkIntToScalar(top_radius_);
258 const SkScalar bottom_radius = SkIntToScalar(bottom_radius_);
259
260 const SkScalar radii[8] = {top_radius, top_radius, top_radius,
261 top_radius, bottom_radius, bottom_radius,
262 bottom_radius, bottom_radius};
263
264 SkPath path;
265 path.addRoundRect(gfx::RectToSkRect(bounds), radii, SkPathDirection::kCW);
266 canvas->ClipPath(path, true);
267 }
268
269 {
270 // Draw the artwork. The artwork is resized to the height of the view while
271 // maintaining the aspect ratio.
272 gfx::Rect source_bounds =
273 gfx::Rect(0, 0, artwork_.width(), artwork_.height());
274 gfx::Rect artwork_bounds = GetArtworkBounds(*view);
275
276 canvas->DrawImageInt(
277 artwork_, source_bounds.x(), source_bounds.y(), source_bounds.width(),
278 source_bounds.height(), artwork_bounds.x(), artwork_bounds.y(),
279 artwork_bounds.width(), artwork_bounds.height(), false /* filter */);
280 }
281
282 // Draw a filled rectangle which will act as the main background of the
283 // notification. This may cover up some of the artwork.
284 const SkColor background_color =
285 background_color_.value_or(GetDefaultBackgroundColor(*view));
286 canvas->FillRect(GetFilledBackgroundBounds(*view), background_color);
287
288 {
289 // Draw a gradient to fade the color background and the image together.
290 gfx::Rect draw_bounds = GetGradientBounds(*view);
291
292 const SkColor colors[2] = {
293 background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
294 const SkPoint points[2] = {GetGradientStartPoint(draw_bounds),
295 GetGradientEndPoint(draw_bounds)};
296
297 cc::PaintFlags flags;
298 flags.setAntiAlias(true);
299 flags.setStyle(cc::PaintFlags::kFill_Style);
300 flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
301 2, SkTileMode::kClamp));
302
303 canvas->DrawRect(draw_bounds, flags);
304 }
305 }
306
UpdateArtwork(const gfx::ImageSkia & image)307 void MediaNotificationBackground::UpdateArtwork(const gfx::ImageSkia& image) {
308 if (artwork_.BackedBySameObjectAs(image))
309 return;
310
311 artwork_ = image;
312
313 UpdateColorsInternal();
314 }
315
UpdateCornerRadius(int top_radius,int bottom_radius)316 bool MediaNotificationBackground::UpdateCornerRadius(int top_radius,
317 int bottom_radius) {
318 if (top_radius_ == top_radius && bottom_radius_ == bottom_radius)
319 return false;
320
321 top_radius_ = top_radius;
322 bottom_radius_ = bottom_radius;
323 return true;
324 }
325
UpdateArtworkMaxWidthPct(double max_width_pct)326 bool MediaNotificationBackground::UpdateArtworkMaxWidthPct(
327 double max_width_pct) {
328 if (artwork_max_width_pct_ == max_width_pct)
329 return false;
330
331 artwork_max_width_pct_ = max_width_pct;
332 return true;
333 }
334
UpdateFavicon(const gfx::ImageSkia & icon)335 void MediaNotificationBackground::UpdateFavicon(const gfx::ImageSkia& icon) {
336 if (favicon_.BackedBySameObjectAs(icon))
337 return;
338
339 favicon_ = icon;
340
341 if (!artwork_.isNull())
342 return;
343
344 UpdateColorsInternal();
345 }
346
GetBackgroundColor(const views::View & owner) const347 SkColor MediaNotificationBackground::GetBackgroundColor(
348 const views::View& owner) const {
349 if (background_color_.has_value())
350 return *background_color_;
351 return GetDefaultBackgroundColor(owner);
352 }
353
GetForegroundColor(const views::View & owner) const354 SkColor MediaNotificationBackground::GetForegroundColor(
355 const views::View& owner) const {
356 const SkColor foreground =
357 foreground_color_.has_value()
358 ? *foreground_color_
359 : views::style::GetColor(owner, views::style::CONTEXT_LABEL,
360 views::style::STYLE_PRIMARY);
361 return color_utils::BlendForMinContrast(
362 foreground, GetBackgroundColor(owner), base::nullopt,
363 kMediaNotificationMinimumContrastRatio)
364 .color;
365 }
366
GetArtworkWidth(const gfx::Size & view_size) const367 int MediaNotificationBackground::GetArtworkWidth(
368 const gfx::Size& view_size) const {
369 if (artwork_.isNull())
370 return 0;
371
372 // Calculate the aspect ratio of the image and determine what the width of the
373 // image should be based on that ratio and the height of the notification.
374 float aspect_ratio = (float)artwork_.width() / artwork_.height();
375 return ceil(view_size.height() * aspect_ratio);
376 }
377
GetArtworkVisibleWidth(const gfx::Size & view_size) const378 int MediaNotificationBackground::GetArtworkVisibleWidth(
379 const gfx::Size& view_size) const {
380 // The artwork should only take up a maximum percentage of the notification.
381 return std::min(GetArtworkWidth(view_size),
382 (int)ceil(view_size.width() * artwork_max_width_pct_));
383 }
384
GetArtworkBounds(const views::View & owner) const385 gfx::Rect MediaNotificationBackground::GetArtworkBounds(
386 const views::View& owner) const {
387 const gfx::Rect& view_bounds = owner.GetContentsBounds();
388 int width = GetArtworkWidth(view_bounds.size());
389 int visible_width = GetArtworkVisibleWidth(view_bounds.size());
390
391 // This offset is for centering artwork if artwork visible width is smaller
392 // than artwork width.
393 int horizontal_offset = (width - visible_width) / 2;
394
395 // The artwork should be positioned on the far right hand side of the
396 // notification and be the same height.
397 return owner.GetMirroredRect(
398 gfx::Rect(view_bounds.right() - width + horizontal_offset, 0, width,
399 view_bounds.height()));
400 }
401
GetFilledBackgroundBounds(const views::View & owner) const402 gfx::Rect MediaNotificationBackground::GetFilledBackgroundBounds(
403 const views::View& owner) const {
404 // The filled background should take up the full notification except the area
405 // taken up by the artwork.
406 const gfx::Rect& view_bounds = owner.GetContentsBounds();
407 gfx::Rect bounds = gfx::Rect(view_bounds);
408 bounds.Inset(0, 0, GetArtworkVisibleWidth(view_bounds.size()), 0);
409 return owner.GetMirroredRect(bounds);
410 }
411
GetGradientBounds(const views::View & owner) const412 gfx::Rect MediaNotificationBackground::GetGradientBounds(
413 const views::View& owner) const {
414 if (artwork_.isNull())
415 return gfx::Rect(0, 0, 0, 0);
416
417 // The gradient should appear above the artwork on the left.
418 const gfx::Rect& view_bounds = owner.GetContentsBounds();
419 return owner.GetMirroredRect(gfx::Rect(
420 view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
421 view_bounds.y(), kMediaImageGradientWidth, view_bounds.height()));
422 }
423
GetGradientStartPoint(const gfx::Rect & draw_bounds) const424 SkPoint MediaNotificationBackground::GetGradientStartPoint(
425 const gfx::Rect& draw_bounds) const {
426 return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.right_center()
427 : draw_bounds.left_center());
428 }
429
GetGradientEndPoint(const gfx::Rect & draw_bounds) const430 SkPoint MediaNotificationBackground::GetGradientEndPoint(
431 const gfx::Rect& draw_bounds) const {
432 return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.left_center()
433 : draw_bounds.right_center());
434 }
435
GetDefaultBackgroundColor(const views::View & owner) const436 SkColor MediaNotificationBackground::GetDefaultBackgroundColor(
437 const views::View& owner) const {
438 return owner.GetNativeTheme()->GetSystemColor(
439 ui::NativeTheme::kColorId_BubbleBackground);
440 }
441
UpdateColorsInternal()442 void MediaNotificationBackground::UpdateColorsInternal() {
443 // If there is an artwork, it should be used.
444 // If there is no artwork, neither a favicon, the artwork bitmap will be used
445 // which is going to be a null bitmap and produce a default value.
446 // In the case of there is a favicon and no artwork, the favicon should be
447 // used to generate the colors.
448 if (!artwork_.isNull() || favicon_.isNull()) {
449 background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
450 foreground_color_ =
451 GetNotificationForegroundColor(background_color_, artwork_.bitmap());
452 return;
453 }
454
455 background_color_ = GetNotificationBackgroundColor(favicon_.bitmap());
456 if (background_color_) {
457 // Apply a shade factor on the color as favicons often are fairly bright.
458 *background_color_ = SkColorSetRGB(
459 SkColorGetR(*background_color_) * kBackgroundFaviconColorShadeFactor,
460 SkColorGetG(*background_color_) * kBackgroundFaviconColorShadeFactor,
461 SkColorGetB(*background_color_) * kBackgroundFaviconColorShadeFactor);
462 }
463 foreground_color_ =
464 GetNotificationForegroundColor(background_color_, favicon_.bitmap());
465 }
466
467 } // namespace media_message_center
468