[gnome-music/wip/jfelder/album-cover-draw] albumwidget: Album art cover background
- From: Jean Felder <jfelder src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music/wip/jfelder/album-cover-draw] albumwidget: Album art cover background
- Date: Sun, 24 Mar 2019 10:25:39 +0000 (UTC)
commit a645bd9ad462d8feda9f921d291f18f31d65eb85
Author: Jean Felder <jfelder src gnome org>
Date: Tue May 29 20:28:17 2018 +0200
albumwidget: Album art cover background
Display a blurred version of the album art cover as background. Labels
color are updated (black or white) to be visible with this new
background.
Closes: #10
data/org.gnome.Music.css | 8 ++++
data/ui/AlbumWidget.ui | 4 +-
gnomemusic/albumartcache.py | 67 +++++++++++++++++++++++++++++
gnomemusic/utils.py | 90 +++++++++++++++++++++++++++++++++++++++
gnomemusic/widgets/albumwidget.py | 48 ++++++++++++++++++++-
5 files changed, 214 insertions(+), 3 deletions(-)
---
diff --git a/data/org.gnome.Music.css b/data/org.gnome.Music.css
index f5ea3b7b..89985425 100644
--- a/data/org.gnome.Music.css
+++ b/data/org.gnome.Music.css
@@ -94,3 +94,11 @@ box#ArtistAlbumsWidget .artist-label {
.tooltip-title {
font-weight: bold;
}
+
+.black {
+ color: black;
+}
+
+.white {
+ color: white;
+}
diff --git a/data/ui/AlbumWidget.ui b/data/ui/AlbumWidget.ui
index 9dd7253d..e8fde2ab 100644
--- a/data/ui/AlbumWidget.ui
+++ b/data/ui/AlbumWidget.ui
@@ -9,7 +9,7 @@
<class name="content-view"/>
</style>
<child>
- <object class="GtkBox">
+ <object class="GtkBox" id="_main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
@@ -23,7 +23,7 @@
<property name="margin_bottom">32</property>
<property name="vexpand">True</property>
<child>
- <object class="GtkBox" id="albumDetails">
+ <object class="GtkBox" id="_album_details">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
diff --git a/gnomemusic/albumartcache.py b/gnomemusic/albumartcache.py
index 27ce2ec4..19f9242e 100644
--- a/gnomemusic/albumartcache.py
+++ b/gnomemusic/albumartcache.py
@@ -28,6 +28,7 @@ from math import pi
import os
import cairo
+from PIL import Image, ImageFilter
import gi
gi.require_version('GstTag', '1.0')
gi.require_version('MediaArt', '2.0')
@@ -169,6 +170,7 @@ class Art(GObject.GObject):
MEDIUM = (128, 128)
LARGE = (256, 256)
XLARGE = (512, 512)
+ XXLARGE = (1024, 1024)
def __init__(self, width, height):
"""Intialize width and height"""
@@ -188,6 +190,10 @@ class Art(GObject.GObject):
self._surface = None
self._scale = scale
+ self._blurred_surface = None
+ self._blurred_size = None
+ self._label_color = None
+
@log
def lookup(self):
"""Starts the art lookup sequence"""
@@ -304,6 +310,67 @@ class Art(GObject.GObject):
return self._surface
+ @log
+ def get_blurred_surface(self, width, height):
+ """Compute the blurred cairo surface of an ArtImage.
+
+ self._surface is resized and then blurred with a gaussian
+ filter. The dominant color of the blurred image is extracted
+ to detect which color (white or black) can be displayed on this
+ surface.
+ :param int width: requested width surface
+ :param int height: requested height surface
+ :returns: blurred cairo surface and foreground color
+ :rtype: (cairo.surface, Gdk.RGBA)
+ """
+ if not self._surface:
+ return None, None
+
+ size = (self._surface.get_width(), self._surface.get_height())
+ full_size = (width, height)
+
+ if (self._blurred_surface
+ and self._blurred_size[0] >= full_size[0]
+ and self._blurred_size[1] >= full_size[1]):
+ return self._blurred_surface, self._label_color
+
+ # convert cairo surface to a pillow image
+ img = Image.frombuffer(
+ "RGBA", size, self._surface.get_data(), "raw", "RGBA", 0, 1)
+
+ # resize and blur the image
+ ratio = full_size[0] / full_size[1]
+ h = int((1 / ratio) * full_size[1])
+ diff = full_size[1] - h
+ img_cropped = img.crop(
+ (0, diff // 2, size[0], size[1] - diff // 2))
+ img_scaled = img_cropped.resize(full_size, Image.BICUBIC)
+ img_blurred = img_scaled.filter(ImageFilter.GaussianBlur(30))
+
+ # convert the image to a cairo suface
+ arr = bytearray(img_blurred.tobytes('raw', 'RGBA'))
+ self._blurred_surface = cairo.ImageSurface.create_for_data(
+ arr, cairo.FORMAT_ARGB32, img_blurred.width, img_blurred.height)
+
+ self._blurred_size = (
+ self._blurred_surface.get_width(),
+ self._blurred_surface.get_height()
+ )
+
+ # compute dominant color of the blurred image to update
+ # foreground color in white or black
+ b, g, r, a = img_blurred.split()
+ img_blurred_rgb = Image.merge('RGB', (r, g, b))
+ dominant_color = utils.dominant_color(img_blurred_rgb)
+ white_ratio = utils.contrast_ratio(*dominant_color, 1., 1., 1.)
+ black_ratio = utils.contrast_ratio(*dominant_color, 0., 0., 0.)
+ if white_ratio > black_ratio:
+ self._label_color = Gdk.RGBA(1.0, 1.0, 1.0, 1.0)
+ else:
+ self._label_color = Gdk.RGBA(0.0, 0.0, 0.0, 0.0)
+
+ return self._blurred_surface, self._label_color
+
class Cache(GObject.GObject):
"""Handles retrieval of MediaArt cache art
diff --git a/gnomemusic/utils.py b/gnomemusic/utils.py
index ec171663..ba63ee4d 100644
--- a/gnomemusic/utils.py
+++ b/gnomemusic/utils.py
@@ -22,7 +22,10 @@
# code, but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version.
+import colorsys
from enum import IntEnum
+from math import pow
+from PIL import Image
from gettext import gettext as _
@@ -114,3 +117,90 @@ def seconds_to_string(duration):
seconds %= 60
return '{:d}:{:02d}'.format(minutes, seconds)
+
+
+def relative_luminance(r, g, b):
+ """Compute relative luminance of an RGB color.
+
+ Relative luminance is the relative brightness of any point in a
+ colorspace, normalized to 0 for darkest black and 1 for lightest
+ white.
+ See: https://www.w3.org/TR/WCAG20/#relativeluminancedef
+ :param float r: r channel between 0. and 1.
+ :param float g: g channel between 0. and 1.
+ :param float b: b channel between 0. and 1.
+ :returns: relative luminance
+ :rtype: float
+ """
+ params = []
+ for channel in [r, g, b]:
+ if channel <= 0.03928:
+ value = channel / 12.92
+ else:
+ value = pow((channel + 0.055) / 1.055, 2.4)
+ params.append(value)
+
+ luminance = 0.2126 * params[0] + 0.7152 * params[1] + 0.0722 * params[2]
+ return luminance
+
+
+def contrast_ratio(r1, g1, b1, r2, g2, b2):
+ """Compute contrast ratio between two RGB colors.
+
+ Contrat ratio is a measure to indicate how readable the color
+ combination is.
+ See: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
+ :param float r1: r channel from color 1 between 0. and 1.
+ :param float g1: g channel from color 1 between 0. and 1.
+ :param float b1: b channel from color 1 between 0. and 1.
+ :param float r2: r channel from color 2 between 0. and 1.
+ :param float g2: g channel from color 2 between 0. and 1.
+ :param float b2: b channel from color 2 between 0. and 1.
+ :returns: constrat ratio
+ :rtype: float
+ """
+ l1 = relative_luminance(r1, g1, b1)
+ l2 = relative_luminance(r2, g2, b2)
+ l_max = max(l1, l2)
+ l_min = min(l1, l2)
+ return (l_max + 0.05) / (l_min + 0.05)
+
+
+def dominant_color(image):
+ """Compute dominant color of a pillow image.
+
+ Compute dominant color by extracting the peak of the hue channel of
+ the image's histogram.
+ :param Image image: a pillow image
+ :returns: dominant color
+ :rtype: (r, b, g) between 0. and 1.
+
+ """
+ # reduce image size if necessary to minimize computation time
+ if (image.width > 256
+ or image.height > 256):
+ image.thumbnail((256, 256), Image.ANTIALIAS)
+
+ im_hsv = image.convert('HSV')
+ histogram = im_hsv.histogram()[:256]
+ h_max = max(histogram)
+ h_max_value = histogram.index(h_max)
+
+ px = im_hsv.load()
+ width = image.size[0]
+ height = image.size[1]
+
+ s_max_value = 0
+ v_max_value = 0
+ for i in range(width):
+ for j in range(height):
+ pixel = px[i, j]
+ if pixel[0] == h_max_value:
+ s_max_value += pixel[1]
+ v_max_value += pixel[2]
+
+ s_max_value /= h_max
+ v_max_value /= h_max
+ r, g, b = colorsys.hsv_to_rgb(
+ h_max_value / 255.0, s_max_value / 255.0, v_max_value / 255.0)
+ return r, g, b
diff --git a/gnomemusic/widgets/albumwidget.py b/gnomemusic/widgets/albumwidget.py
index ecdc2c93..83ea41af 100644
--- a/gnomemusic/widgets/albumwidget.py
+++ b/gnomemusic/widgets/albumwidget.py
@@ -23,7 +23,7 @@
# delete this exception statement from your version.
from gettext import ngettext
-from gi.repository import GdkPixbuf, GObject, Gtk
+from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk
from gnomemusic import log
from gnomemusic.albumartcache import Art
@@ -45,11 +45,13 @@ class AlbumWidget(Gtk.EventBox):
__gtype_name__ = 'AlbumWidget'
+ _album_details = Gtk.Template.Child()
_artist_label = Gtk.Template.Child()
_composer_label = Gtk.Template.Child()
_composer_info_label = Gtk.Template.Child()
_cover_stack = Gtk.Template.Child()
_disc_listbox = Gtk.Template.Child()
+ _main_box = Gtk.Template.Child()
_released_info_label = Gtk.Template.Child()
_running_info_label = Gtk.Template.Child()
_title_label = Gtk.Template.Child()
@@ -81,6 +83,8 @@ class AlbumWidget(Gtk.EventBox):
self._create_model()
self._album_name = None
+ self._window_draw_handler = None
+
self.bind_property(
'selection-mode', self._disc_listbox, 'selection-mode',
GObject.BindingFlags.BIDIRECTIONAL)
@@ -130,6 +134,16 @@ class AlbumWidget(Gtk.EventBox):
self._album_name = utils.get_album_title(album)
artist = utils.get_artist_name(album)
+ self._set_foreground_color(Gdk.RGBA(0.0, 0.0, 0.0, 0.0))
+ if self._window_draw_handler:
+ self._main_box.disconnect(self._window_draw_handler)
+ self._window_draw_handler = None
+ self._foreground_color = None
+ self._art_background = Art(
+ Art.Size.XXLARGE, album, self.props.scale_factor)
+ self._art_background.connect('finished', self._set_background)
+ self._art_background.lookup()
+
self._title_label.props.label = self._album_name
self._title_label.props.tooltip_text = self._album_name
@@ -186,6 +200,38 @@ class AlbumWidget(Gtk.EventBox):
return disc_box
+ @log
+ def _set_foreground_color(self, color):
+ remove_color = "black"
+ add_color = "white"
+ if color == Gdk.RGBA(0.0, 0.0, 0.0, 0.0):
+ remove_color = "white"
+ add_color = "black"
+
+ for widget in [self._album_details, self._disc_listbox]:
+ style_ctx = widget.get_style_context()
+ style_ctx.remove_class(remove_color)
+ style_ctx.add_class(add_color)
+
+ @log
+ def _set_background(self, klass):
+ self._window_draw_handler = self._main_box.connect(
+ "draw", self._draw_background, klass)
+
+ @log
+ def _draw_background(self, widget, ctx, klass):
+ width = self.get_allocated_width()
+ height = self.get_allocated_height()
+ background_surface, fg_color = klass.get_blurred_surface(width, height)
+
+ if not self._foreground_color:
+ self._foreground_color = fg_color
+ self._set_foreground_color(fg_color)
+
+ ctx.set_source_surface(background_surface)
+ ctx.rectangle(0, 0, width, height)
+ ctx.fill()
+
@log
def _song_activated(self, widget, song_widget):
if self.props.selection_mode:
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]