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