From d1560d57a88e4f5fce666f10fd703cd07f09d775 Mon Sep 17 00:00:00 2001 From: Trigg Date: Mon, 11 Jul 2022 14:49:22 +0000 Subject: [PATCH] - image helper creates a mask image for later processing - image helper can force transparency - added option for semitransparent avatars --- discover_overlay/image_getter.py | 62 ++++++++++--------- discover_overlay/notification_overlay.py | 8 +-- discover_overlay/text_overlay.py | 6 +- discover_overlay/voice_overlay.py | 76 +++++++++++++++++++----- discover_overlay/voice_settings.py | 73 +++++++++++++++-------- 5 files changed, 147 insertions(+), 78 deletions(-) diff --git a/discover_overlay/image_getter.py b/discover_overlay/image_getter.py index 42d4b39..a812161 100644 --- a/discover_overlay/image_getter.py +++ b/discover_overlay/image_getter.py @@ -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() diff --git a/discover_overlay/notification_overlay.py b/discover_overlay/notification_overlay.py index f56c66d..a01b95c 100644 --- a/discover_overlay/notification_overlay.py +++ b/discover_overlay/notification_overlay.py @@ -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 """ diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py index 6bef9c6..eb28ba6 100644 --- a/discover_overlay/text_overlay.py +++ b/discover_overlay/text_overlay.py @@ -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 """ diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py index cdc1752..54f0f62 100644 --- a/discover_overlay/voice_overlay.py +++ b/discover_overlay/voice_overlay.py @@ -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() diff --git a/discover_overlay/voice_settings.py b/discover_overlay/voice_settings.py index a9e6797..e0b4b25 100644 --- a/discover_overlay/voice_settings.py +++ b/discover_overlay/voice_settings.py @@ -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: