- image helper creates a mask image for later processing

- image helper can force transparency
- added option for semitransparent avatars
This commit is contained in:
Trigg 2022-07-11 14:49:22 +00:00
parent 729f7ebd92
commit d1560d57a8
5 changed files with 147 additions and 78 deletions

View file

@ -21,6 +21,7 @@ import PIL
import PIL.Image as Image
import os
import io
import copy
gi.require_version('GdkPixbuf', '2.0')
# pylint: disable=wrong-import-position
from gi.repository import Gio, GdkPixbuf # nopep8
@ -37,7 +38,7 @@ class SurfaceGetter():
self.url = url
self.size = size
def get_url(self):
def get_url(self, alpha):
"""Downloads and decodes"""
try:
resp = requests.get(
@ -48,9 +49,9 @@ class SurfaceGetter():
)
raw = resp.raw
image = Image.open(raw)
surface = from_pil(image)
(surface, mask) = from_pil(image, alpha)
self.func(self.identifier, surface)
self.func(self.identifier, surface, mask)
except requests.HTTPError:
log.error("Unable to open %s", self.url)
except requests.TooManyRedirects:
@ -66,7 +67,7 @@ class SurfaceGetter():
except PIL.UnidentifiedImageError:
log.error("Unknown image type")
def get_file(self):
def get_file(self, alpha):
locations = [os.path.expanduser('~/.local/'), '/usr/', '/app']
for prefix in locations:
mixpath = os.path.join(prefix, self.url)
@ -82,56 +83,53 @@ class SurfaceGetter():
except FileNotFoundError:
log.error("File not found: %s", mixpath)
if image:
surface = from_pil(image)
(surface, mask) = from_pil(image, alpha)
if surface:
self.func(self.identifier, surface)
self.func(self.identifier, surface, mask)
return
def from_pil(image, alpha=1.0):
def from_pil(image, alpha):
"""
:param im: Pillow Image
:param alpha: 0..1 alpha to add to non-alpha images
:param format: Pixel format for output surface
"""
arr = bytearray()
mask = bytearray()
if 'A' not in image.getbands():
image.putalpha(int(alpha * 256.))
arr = bytearray(image.tobytes('raw', 'BGRa'))
image.putalpha(int(alpha * 255.0))
arr = bytearray(image.tobytes('raw', 'BGRa'))
mask = arr
else:
arr = bytearray(image.tobytes('raw', 'BGRa'))
mask = copy.deepcopy((arr))
idx = 3
while idx < len(arr):
if arr[idx] > 0:
mask[idx] = 255
else:
mask[idx] = 0
arr[idx] = int(arr[idx] * alpha)
idx += 4
surface = cairo.ImageSurface.create_for_data(
arr, cairo.FORMAT_ARGB32, image.width, image.height)
return surface
mask = cairo.ImageSurface.create_for_data(
mask, cairo.FORMAT_ARGB32, image.width, image.height)
return (surface, mask)
def get_surface(func, identifier, ava, size):
def get_surface(func, identifier, ava, size, alpha=1.0):
"""Download to cairo surface"""
image_getter = SurfaceGetter(func, identifier, ava, size)
if identifier.startswith('http'):
thread = threading.Thread(target=image_getter.get_url, args=())
thread = threading.Thread(target=image_getter.get_url, args=[alpha])
thread.start()
else:
thread = threading.Thread(target=image_getter.get_file, args=())
thread = threading.Thread(target=image_getter.get_file, args=[alpha])
thread.start()
def make_surface_from_raw(raw, size):
"""Create surface from raw notification data"""
width = raw[0]
height = raw[1]
rowstride = raw[2]
hasalpha = raw[3]
bitspersample = raw[4]
channels = raw[5]
image_raw_dbus = raw[6]
image_raw = bytes(image_raw_dbus)
image = None
if hasalpha:
image = Image.frombytes('RGBA', [width, height], image_raw, 'raw')
else:
image = Image.frombytes('RGB', [width, height], image_raw, 'raw')
surface = from_pil(image)
return surface
def get_aspected_size(img, width, height, anchor=0, hanchor=0):
"""Get dimensions of image keeping current aspect ratio"""
pic_width = img.get_width()

View file

@ -17,7 +17,7 @@ import re
import cairo
import math
import gi
from .image_getter import get_surface, draw_img_to_rect, get_aspected_size, make_surface_from_raw
from .image_getter import get_surface, draw_img_to_rect, get_aspected_size
from .overlay import OverlayWindow
gi.require_version("Gtk", "3.0")
gi.require_version('PangoCairo', '1.0')
@ -67,8 +67,8 @@ class NotificationOverlayWindow(OverlayWindow):
self.redraw()
def set_blank(self):
self.content=[]
self.needsredraw=True
self.content = []
self.needsredraw = True
def tick(self):
# This doesn't really belong in overlay or settings
@ -164,7 +164,7 @@ class NotificationOverlayWindow(OverlayWindow):
get_surface(self.recv_icon, icon, icon,
self.icon_size)
def recv_icon(self, identifier, pix):
def recv_icon(self, identifier, pix, mask):
"""
Called when image_getter has downloaded an image
"""

View file

@ -56,8 +56,8 @@ class TextOverlayWindow(OverlayWindow):
self.redraw()
def set_blank(self):
self.content=[]
self.needsredraw=True
self.content = []
self.needsredraw = True
def tick(self):
if len(self.attachment) > self.line_limit:
@ -181,7 +181,7 @@ class TextOverlayWindow(OverlayWindow):
self.warned_filetypes.append(message['type'])
return ret
def recv_attach(self, identifier, pix):
def recv_attach(self, identifier, pix, mask):
"""
Called when an image has been downloaded by image_getter
"""

View file

@ -41,6 +41,7 @@ class VoiceOverlayWindow(OverlayWindow):
OverlayWindow.__init__(self, discover, piggyback)
self.avatars = {}
self.avatar_masks = {}
self.dummy_data = []
mostly_false = [False, False, False, False, False, False, False, True]
@ -75,6 +76,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.order = None
self.def_avatar = None
self.channel_icon = None
self.channel_mask = None
self.overflow = None
self.use_dummy = False
self.dummy_count = 10
@ -83,6 +85,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.show_disconnected = True
self.channel_title = ""
self.border_width = 2
self.icon_transparency = 0.0
self.round_avatar = True
self.icon_only = True
@ -101,10 +104,24 @@ class VoiceOverlayWindow(OverlayWindow):
self.force_location()
get_surface(self.recv_avatar,
"share/icons/hicolor/256x256/apps/discover-overlay-default.png",
'def', self.avatar_size)
'def', self.avatar_size, self.icon_transparency)
self.set_title("Discover Voice")
self.redraw()
def set_icon_transparency(self, trans):
self.icon_transparency = trans
get_surface(self.recv_avatar,
"share/icons/hicolor/256x256/apps/discover-overlay-default.png",
'def', self.avatar_size, self.icon_transparency)
self.avatars = {}
self.avatar_masks = {}
self.channel_icon = None
self.channel_mask = None
self.needsredraw = True
def set_blank(self):
self.userlist = []
self.needsredraw = True
@ -317,12 +334,11 @@ class VoiceOverlayWindow(OverlayWindow):
"""
Change the icon for channel
"""
print("set_channel_icon : %s" % (url))
if not url:
self.channel_icon = None
else:
get_surface(self.recv_avatar, url, "channel",
self.avatar_size)
self.avatar_size, self.icon_transparency)
def set_user_list(self, userlist, alt):
"""
@ -550,17 +566,19 @@ class VoiceOverlayWindow(OverlayWindow):
context.restore()
self.context = None
def recv_avatar(self, identifier, pix):
def recv_avatar(self, identifier, pix, mask):
"""
Called when image_getter has downloaded an image
"""
print(identifier)
if identifier == 'def':
self.def_avatar = pix
self.def_avatar_mask = mask
elif identifier == 'channel':
self.channel_icon = pix
self.channel_mask = mask
else:
self.avatars[identifier] = pix
self.avatar_masks[identifier] = mask
self.needsredraw = True
def delete_avatar(self, identifier):
@ -587,7 +605,7 @@ class VoiceOverlayWindow(OverlayWindow):
self.title_font
)
if self.channel_icon:
self.draw_avatar_pix(context, self.channel_icon,
self.draw_avatar_pix(context, self.channel_icon, self.channel_mask,
pos_x, pos_y, None, avatar_size)
return tw
@ -629,12 +647,13 @@ class VoiceOverlayWindow(OverlayWindow):
url = "https://cdn.discordapp.com/avatars/%s/%s.png" % (
user['id'], user['avatar'])
get_surface(self.recv_avatar, url, user["id"],
self.avatar_size)
self.avatar_size, self.icon_transparency)
# Set the key with no value to avoid spamming requests
self.avatars[user["id"]] = None
self.avatar_masks[user["id"]] = None
colour = self.border_col
colour = None
mute = False
deaf = False
bg_col = None
@ -655,8 +674,10 @@ class VoiceOverlayWindow(OverlayWindow):
fg_col = self.text_col
pix = None
mask = None
if user["id"] in self.avatars:
pix = self.avatars[user["id"]]
mask = self.avatar_masks[user["id"]]
if not self.horizontal:
if not self.icon_only:
tw = self.draw_text(
@ -668,7 +689,8 @@ class VoiceOverlayWindow(OverlayWindow):
avatar_size,
self.text_font
)
self.draw_avatar_pix(context, pix, pos_x, pos_y, colour, avatar_size)
self.draw_avatar_pix(context, pix, mask, pos_x,
pos_y, colour, avatar_size)
if deaf:
self.draw_deaf(context, pos_x, pos_y, [
0.0, 0.0, 0.0, 0.5], avatar_size)
@ -748,16 +770,25 @@ class VoiceOverlayWindow(OverlayWindow):
context.fill()
context.restore()
def draw_avatar_pix(self, context, pixbuf, pos_x, pos_y, border_colour, avatar_size):
def draw_avatar_pix(self, context, pixbuf, mask, pos_x, pos_y, border_colour, avatar_size):
"""
Draw avatar image at given position
"""
# Empty the space for this
self.blank_avatar(context, pos_x, pos_y, avatar_size)
# fallback default or fallback further to no image here
if not pixbuf:
pixbuf = self.def_avatar
if not pixbuf:
return
context.set_operator(cairo.OPERATOR_OVER)
if not mask:
mask = self.def_avatar_mask
if not mask:
return
# Draw the "border" by doing a scaled-up copy in a flat colour
if border_colour:
border_size = avatar_size + (self.border_width * 2)
border_x = pos_x - self.border_width
@ -769,17 +800,30 @@ class VoiceOverlayWindow(OverlayWindow):
(border_size / 2), border_size / 2, 0, 2 * math.pi)
context.clip()
self.col(border_colour)
draw_img_to_mask(pixbuf, context, border_x, border_y,
context.set_operator(cairo.OPERATOR_OVER)
draw_img_to_mask(mask, context, border_x, border_y,
border_size, border_size)
context.restore()
# Cut the image back out
context.save()
if self.round_avatar:
context.new_path()
context.arc(pos_x + (avatar_size / 2), pos_y +
(avatar_size / 2), avatar_size / 2, 0, 2 * math.pi)
context.clip()
self.col([0.0, 0.0, 0.0, 0.0])
context.set_operator(cairo.OPERATOR_SOURCE)
draw_img_to_mask(mask, context, pos_x, pos_y,
avatar_size, avatar_size)
context.restore()
# Draw the image
context.save()
if self.round_avatar:
context.new_path()
context.arc(pos_x + (avatar_size / 2), pos_y +
(avatar_size / 2), avatar_size / 2, 0, 2 * math.pi)
context.clip()
context.set_operator(cairo.OPERATOR_OVER)
draw_img_to_rect(pixbuf, context, pos_x, pos_y,
avatar_size, avatar_size)
context.restore()
@ -797,6 +841,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.rectangle(0.0, 0.0, 1.0, 1.0)
self.col(bg_col)
context.fill()
context.set_operator(cairo.OPERATOR_OVER)
self.set_mute_col()
context.save()
@ -846,6 +891,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi)
context.close_path()
context.fill()
context.set_fill_rule(cairo.FILL_RULE_WINDING)
context.restore()
@ -862,6 +908,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.rectangle(0.0, 0.0, 1.0, 1.0)
self.col(bg_col)
context.fill()
context.set_operator(cairo.OPERATOR_OVER)
self.set_mute_col()
context.save()
@ -904,6 +951,7 @@ class VoiceOverlayWindow(OverlayWindow):
context.arc(0.3, 0.7, 0.035, .25 * math.pi, 1.25 * math.pi)
context.close_path()
context.fill()
context.set_fill_rule(cairo.FILL_RULE_WINDING)
context.restore()

View file

@ -86,6 +86,7 @@ class VoiceSettingsWindow(SettingsWindow):
self.show_title = None
self.show_disconnected = None
self.border_width = 2
self.icon_transparency = 1.0
self.init_config()
self.create_gui()
@ -167,6 +168,8 @@ class VoiceSettingsWindow(SettingsWindow):
self.show_disconnected = config.getboolean(
"main", "show_disconnected", fallback=False)
self.border_width = config.getint("main", "border_width", fallback=2)
self.icon_transparency = config.getfloat(
"main", "icon_transparency", fallback=1.0)
# Pass all of our config over to the overlay
self.overlay.set_align_x(self.align_x)
@ -200,6 +203,7 @@ class VoiceSettingsWindow(SettingsWindow):
self.overlay.set_show_title(self.show_title)
self.overlay.set_show_disconnected(self.show_disconnected)
self.overlay.set_border_width(self.border_width)
self.overlay.set_icon_transparency(self.icon_transparency)
self.overlay.set_floating(
self.floating, self.floating_x, self.floating_y, self.floating_w, self.floating_h)
@ -262,6 +266,8 @@ class VoiceSettingsWindow(SettingsWindow):
config.set("main", "show_disconnected", "%d" %
(int(self.show_disconnected)))
config.set("main", "border_width", "%s" % (int(self.border_width)))
config.set("main", "icon_transparency", "%.2f" %
(self.icon_transparency))
with open(self.config_file, 'w') as file:
config.write(file)
@ -293,10 +299,10 @@ class VoiceSettingsWindow(SettingsWindow):
outer_box.attach(dummy_box, 0, 2, 2, 1)
# Autohide
#autohide_label = Gtk.Label.new("Hide on mouseover")
#autohide = Gtk.CheckButton.new()
# autohide_label = Gtk.Label.new("Hide on mouseover")
# autohide = Gtk.CheckButton.new()
# autohide.set_active(self.autohide)
#autohide.connect("toggled", self.change_hide_on_mouseover)
# autohide.connect("toggled", self.change_hide_on_mouseover)
# Font chooser
font_label = Gtk.Label.new(_("Font"))
@ -455,8 +461,8 @@ class VoiceSettingsWindow(SettingsWindow):
avatar_size = Gtk.SpinButton.new(avatar_adjustment, 0, 0)
avatar_size.connect("value-changed", self.change_avatar_size)
avatar_box.attach(avatar_size_label, 0, 0, 1, 1)
avatar_box.attach(avatar_size, 1, 0, 1, 1)
avatar_box.attach(avatar_size_label, 0, 1, 1, 1)
avatar_box.attach(avatar_size, 1, 1, 1, 1)
# Avatar shape
square_avatar_label = Gtk.Label.new(_("Square Avatar"))
@ -465,8 +471,8 @@ class VoiceSettingsWindow(SettingsWindow):
square_avatar.set_active(self.square_avatar)
square_avatar.connect("toggled", self.change_square_avatar)
avatar_box.attach(square_avatar_label, 0, 2, 1, 1)
avatar_box.attach(square_avatar, 1, 2, 1, 1)
avatar_box.attach(square_avatar_label, 0, 3, 1, 1)
avatar_box.attach(square_avatar, 1, 3, 1, 1)
# Display icon only
icon_only_label = Gtk.Label.new(_("Display Icon Only"))
@ -475,8 +481,8 @@ class VoiceSettingsWindow(SettingsWindow):
icon_only.set_active(self.icon_only)
icon_only.connect("toggled", self.change_icon_only)
avatar_box.attach(icon_only_label, 0, 1, 1, 1)
avatar_box.attach(icon_only, 1, 1, 1, 1)
avatar_box.attach(icon_only_label, 0, 2, 1, 1)
avatar_box.attach(icon_only, 1, 2, 1, 1)
# Display Speaker only
only_speaking_label = Gtk.Label.new(_("Display Speakers Only"))
@ -485,8 +491,8 @@ class VoiceSettingsWindow(SettingsWindow):
only_speaking.set_active(self.only_speaking)
only_speaking.connect("toggled", self.change_only_speaking)
avatar_box.attach(only_speaking_label, 0, 3, 1, 1)
avatar_box.attach(only_speaking, 1, 3, 1, 1)
alignment_box.attach(only_speaking_label, 0, 8, 1, 1)
alignment_box.attach(only_speaking, 1, 8, 1, 1)
# Highlight self
highlight_self_label = Gtk.Label.new(_("Highlight Self"))
@ -495,8 +501,8 @@ class VoiceSettingsWindow(SettingsWindow):
highlight_self.set_active(self.highlight_self)
highlight_self.connect("toggled", self.change_highlight_self)
avatar_box.attach(highlight_self_label, 0, 4, 1, 1)
avatar_box.attach(highlight_self, 1, 4, 1, 1)
avatar_box.attach(highlight_self_label, 0, 5, 1, 1)
avatar_box.attach(highlight_self, 1, 5, 1, 1)
# Order avatars
order_label = Gtk.Label.new(_("Order Avatars By"))
@ -512,8 +518,8 @@ class VoiceSettingsWindow(SettingsWindow):
order.pack_start(renderer_text, True)
order.add_attribute(renderer_text, "text", 0)
avatar_box.attach(order_label, 0, 5, 1, 1)
avatar_box.attach(order, 1, 5, 1, 1)
avatar_box.attach(order_label, 0, 6, 1, 1)
avatar_box.attach(order, 1, 6, 1, 1)
# Icon spacing
icon_spacing_label = Gtk.Label.new(_("Icon Spacing"))
@ -580,8 +586,20 @@ class VoiceSettingsWindow(SettingsWindow):
border_width = Gtk.SpinButton.new(border_width_adjustment, 0, 0)
border_width.connect("value-changed", self.change_border_width)
avatar_box.attach(border_width_label, 0, 6, 1, 1)
avatar_box.attach(border_width, 1, 6, 1, 1)
avatar_box.attach(border_width_label, 0, 7, 1, 1)
avatar_box.attach(border_width, 1, 7, 1, 1)
# Icon Transparency
icon_transparency_label = Gtk.Label.new(_("Icon Opacity"))
icon_transparency_label.set_xalign(0)
icon_transparency_adjustment = Gtk.Adjustment.new(
self.icon_transparency, 0.5, 1.0, 0.01, 0.1, 0.0)
icon_transparency = Gtk.HScale.new(icon_transparency_adjustment)
icon_transparency.connect(
"value-changed", self.change_icon_transparency)
avatar_box.attach(icon_transparency_label, 0, 0, 1, 1)
avatar_box.attach(icon_transparency, 1, 0, 1, 1)
# Display icon horizontally
horizontal_label = Gtk.Label.new(_("Display Horizontally"))
@ -607,8 +625,8 @@ class VoiceSettingsWindow(SettingsWindow):
overflow.pack_start(renderer_text, True)
overflow.add_attribute(renderer_text, "text", 0)
avatar_box.attach(overflow_label, 0, 7, 1, 1)
avatar_box.attach(overflow, 1, 7, 1, 1)
avatar_box.attach(overflow_label, 0, 8, 1, 1)
avatar_box.attach(overflow, 1, 8, 1, 1)
# Show Title
show_title_label = Gtk.Label.new(_("Show Title"))
@ -617,8 +635,8 @@ class VoiceSettingsWindow(SettingsWindow):
show_title.set_active(self.show_title)
show_title.connect("toggled", self.change_show_title)
avatar_box.attach(show_title_label, 0, 8, 1, 1)
avatar_box.attach(show_title, 1, 8, 1, 1)
avatar_box.attach(show_title_label, 0, 9, 1, 1)
avatar_box.attach(show_title, 1, 9, 1, 1)
# Show Connection
show_connection_label = Gtk.Label.new(_("Show Connection Status"))
@ -627,8 +645,8 @@ class VoiceSettingsWindow(SettingsWindow):
show_connection.set_active(self.show_connection)
show_connection.connect("toggled", self.change_show_connection)
avatar_box.attach(show_connection_label, 0, 9, 1, 1)
avatar_box.attach(show_connection, 1, 9, 1, 1)
avatar_box.attach(show_connection_label, 0, 10, 1, 1)
avatar_box.attach(show_connection, 1, 10, 1, 1)
# Show Disconnected
show_disconnected_label = Gtk.Label.new(_("Show while disconnected"))
@ -637,8 +655,8 @@ class VoiceSettingsWindow(SettingsWindow):
show_disconnected.set_active(self.show_disconnected)
show_disconnected.connect("toggled", self.change_show_disconnected)
avatar_box.attach(show_disconnected_label, 0, 10, 1, 1)
avatar_box.attach(show_disconnected, 1, 10, 1, 1)
avatar_box.attach(show_disconnected_label, 0, 11, 1, 1)
avatar_box.attach(show_disconnected, 1, 11, 1, 1)
# use dummy
dummy_label = Gtk.Label.new(_("Show test content"))
@ -909,6 +927,11 @@ class VoiceSettingsWindow(SettingsWindow):
self.border_width = button.get_value()
self.save_config()
def change_icon_transparency(self, button):
self.overlay.set_icon_transparency(button.get_value())
self.icon_transparency = button.get_value()
self.save_config()
def on_guild_selection_changed(self, tree, number, selection):
model, treeiter = tree.get_selection().get_selected()
if treeiter is not None: