Commit de982e96 authored by Daniel Brötzmann's avatar Daniel Brötzmann Committed by Philipp Hörist

Add avatar generator

parent 5fdb76d0
Pipeline #3588 passed with stages
in 2 minutes and 44 seconds
......@@ -1220,10 +1220,7 @@ class ChatControl(ChatControlBase):
self.account, self.contact.jid, AvatarSize.CHAT, scale)
image = self.xml.get_object('avatar_image')
if surface is None:
image.set_from_icon_name('avatar-default', Gtk.IconSize.DIALOG)
else:
image.set_from_surface(surface)
image.set_from_surface(surface)
def _nec_update_avatar(self, obj):
if obj.account != self.account:
......
......@@ -294,6 +294,7 @@ class Config:
'show_chatstate_in_banner': [opt_bool, True, _('Shows a text in the banner that describes the current chatstate of the contact')],
'send_chatstate_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: all, composing_only, disabled')],
'send_chatstate_muc_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: all, composing_only, disabled')],
'avatar_clipping': [opt_str, 'rounded_corners', _('How to display avatars:\n\'rounded_corners\' - Display avatars with cropped corners.\n\'circle\' - Display round avatars.\n\'no_clipping\' - Display avatar as-is.')],
}, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]]
__options_per_key = {
......
......@@ -251,7 +251,7 @@ class GC_Contact(CommonContact):
return self.name
def get_avatar(self, *args, **kwargs):
return common.app.interface.get_avatar(self.avatar_sha, *args, **kwargs)
return common.app.interface.get_avatar(self, *args, **kwargs)
def as_contact(self):
"""
......@@ -599,10 +599,8 @@ class Contacts():
return None
for resource in self._contacts[jid]:
if resource.avatar_sha is None:
continue
avatar = common.app.interface.get_avatar(
resource.avatar_sha, size, scale)
resource, size, scale)
if avatar is None:
self.set_avatar(jid, None)
return avatar
......
......@@ -746,7 +746,7 @@ class GroupchatControl(ChatControlBase):
transient_for=self.parent_win.window)
return
publish = app.interface.get_avatar(sha, publish=True)
publish = app.interface.get_avatar_from_storage(sha, publish=True)
avatar = base64.b64encode(publish).decode('utf-8')
con = app.connections[self.account]
con.get_module('VCardTemp').upload_room_avatar(
......@@ -1074,7 +1074,7 @@ class GroupchatControl(ChatControlBase):
banner_status_img = self.xml.get_object('gc_banner_status_image')
if self.is_connected:
if self.contact.avatar_sha:
surface = app.interface.get_avatar(self.contact.avatar_sha,
surface = app.interface.get_avatar(self.contact,
AvatarSize.ROSTER,
self.scale_factor)
banner_status_img.set_from_surface(surface)
......@@ -1694,7 +1694,7 @@ class GroupchatControl(ChatControlBase):
return
surface = app.interface.get_avatar(
gc_contact.avatar_sha, AvatarSize.ROSTER, self.scale_factor)
gc_contact, AvatarSize.ROSTER, self.scale_factor)
image = Gtk.Image.new_from_surface(surface)
self.model[iter_][Column.AVATAR_IMG] = image
......@@ -2119,7 +2119,7 @@ class GroupchatControl(ChatControlBase):
image = None
if app.config.get('show_avatars_in_roster'):
surface = app.interface.get_avatar(
contact.avatar_sha, AvatarSize.ROSTER, self.scale_factor)
contact, AvatarSize.ROSTER, self.scale_factor)
image = Gtk.Image.new_from_surface(surface)
# Add to model
......
......@@ -135,7 +135,7 @@ class ProfileWindow(Gtk.ApplicationWindow):
return
scale = self.get_scale_factor()
surface = app.interface.get_avatar(sha, AvatarSize.VCARD, scale)
surface = app.interface.get_avatar_from_storage(sha, AvatarSize.VCARD, scale)
button = self.xml.get_object('PHOTO_button')
image = self.xml.get_object('PHOTO_image')
......@@ -145,7 +145,7 @@ class ProfileWindow(Gtk.ApplicationWindow):
text_button.hide()
self.avatar_sha = sha
publish = app.interface.get_avatar(sha, publish=True)
publish = app.interface.get_avatar_from_storage(sha, publish=True)
self.avatar_encoded = base64.b64encode(publish).decode('utf-8')
self.avatar_mime_type = 'image/png'
......@@ -212,7 +212,7 @@ class ProfileWindow(Gtk.ApplicationWindow):
self.avatar_mime_type = vcard_[i]['TYPE']
scale = self.get_scale_factor()
surface = app.interface.get_avatar(
surface = app.interface.get_avatar_from_storage(
self.avatar_sha, AvatarSize.VCARD, scale)
if surface is None:
pixbuf = gtkgui_helpers.scale_pixbuf_from_data(
......
......@@ -232,13 +232,12 @@ class GCTooltip():
if contact.avatar_sha is not None:
app.log('avatar').debug(
'Load GCTooltip: %s %s', contact.name, contact.avatar_sha)
scale = self._ui.tooltip_grid.get_scale_factor()
surface = app.interface.get_avatar(
contact.avatar_sha, AvatarSize.TOOLTIP, scale)
if surface is not None:
self._ui.avatar.set_from_surface(surface)
self._ui.avatar.show()
self._ui.fillelement.show()
scale = self._ui.tooltip_grid.get_scale_factor()
surface = app.interface.get_avatar(
contact, AvatarSize.TOOLTIP, scale)
self._ui.avatar.set_from_surface(surface)
self._ui.avatar.show()
self._ui.fillelement.show()
app.plugin_manager.gui_extension_point(
'gc_tooltip_populate', self, contact, self._ui.tooltip_grid)
......@@ -442,9 +441,8 @@ class RosterTooltip(StatusTable):
scale = self._ui.tooltip_grid.get_scale_factor()
surface = app.contacts.get_avatar(
account, self.prim_contact.jid, AvatarSize.TOOLTIP, scale)
if surface is not None:
self._ui.avatar.set_from_surface(surface)
self._ui.avatar.show()
self._ui.avatar.set_from_surface(surface)
self._ui.avatar.show()
app.plugin_manager.gui_extension_point(
'roster_tooltip_populate', self, contacts, self._ui.tooltip_grid)
......
......@@ -26,7 +26,9 @@ import logging
import textwrap
import xml.etree.ElementTree as ET
from pathlib import Path
from functools import lru_cache
from functools import wraps
from math import pi
from gi.repository import Gdk
from gi.repository import Gtk
......@@ -618,6 +620,87 @@ def find_widget(name, container):
return find_widget(name, child)
@lru_cache(maxsize=1024)
def generate_avatar(letters, color, size, scale):
# Get color for nickname with XEP-0392
color_r, color_b, color_g = color
# Set up colors and size
if scale is not None:
size = size * scale
width = size
height = size
font_size = size * 0.5
# Set up surface
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
context = cairo.Context(surface)
context.set_source_rgb(color_r, color_g, color_b)
context.rectangle(0, 0, width, height)
context.fill()
# Draw letters
context.select_font_face('sans-serif',
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
context.set_font_size(font_size)
extends = context.text_extents(letters)
x_pos = width / 2 - (extends.width / 2 + extends.x_bearing)
y_pos = height / 2 - (extends.height / 2 + extends.y_bearing)
context.move_to(x_pos, y_pos)
context.set_source_rgb(0.95, 0.95, 0.95)
context.set_operator(cairo.Operator.OVER)
context.show_text(letters)
return context.get_target()
def clip_circle(surface):
new_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
surface.get_width(),
surface.get_height())
context = cairo.Context(new_surface)
context.set_source_surface(surface, 0, 0)
width = surface.get_width()
height = surface.get_height()
radius = width / 2
context.arc(width / 2, height / 2, radius, 0, 2 * pi)
context.clip()
context.paint()
return context.get_target()
def clip_rounded_corners(surface):
new_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
surface.get_width(),
surface.get_height())
context = cairo.Context(new_surface)
context.set_source_surface(surface, 0, 0)
width = surface.get_width()
height = surface.get_height()
radius = width * 0.1
deg = pi / 180.0
context.new_sub_path()
context.arc(width - radius, radius, radius, -90 * deg, 0)
context.arc(width - radius, height - radius, radius, 0, 90 * deg)
context.arc(radius, height - radius, radius, 90 * deg, 180 * deg)
context.arc(radius, radius, radius, 180 * deg, 270 * deg)
context.close_path()
context.clip()
context.paint()
return context.get_target()
class MultiLineLabel(Gtk.Label):
def __init__(self, *args, **kwargs):
Gtk.Label.__init__(self, *args, **kwargs)
......
......@@ -159,7 +159,7 @@ def on_avatar_save_as_menuitem_activate(widget, avatar, default_name=''):
app.config.set('last_save_dir', os.path.dirname(file_path))
if isinstance(avatar, str):
# We got a SHA
pixbuf = app.interface.get_avatar(avatar)
pixbuf = app.interface.get_avatar_from_storage(avatar)
else:
# We got a pixbuf
pixbuf = avatar
......
......@@ -124,6 +124,10 @@ from gajim.gtk.filetransfer import FileTransfersWindow
from gajim.gtk.subscription_request import SubscriptionRequestWindow
from gajim.gtk.util import get_show_in_roster
from gajim.gtk.util import get_show_in_systray
from gajim.gtk.util import generate_avatar
from gajim.gtk.util import clip_circle
from gajim.gtk.util import clip_rounded_corners
from gajim.gtk.util import text_to_color
parser = optparser.OptionsParser(configpaths.get('CONFIG_FILE'))
......@@ -2044,8 +2048,37 @@ class Interface:
return sha
@staticmethod
def get_avatar(filename, size=None, scale=None, publish=False):
def get_avatar(self, contact, size=None, scale=None, publish=False):
surface = self.get_avatar_from_storage(contact.avatar_sha,
size,
scale,
publish)
if surface is None:
# No avatar found, generate one
# Get initial from name
name = contact.get_shown_name()
letter = name[0].capitalize()
# Use nickname for group chats and bare JID for single contacts
if contact.is_gc_contact:
color_string = contact.name
else:
color_string = contact.jid
color = text_to_color(color_string)
surface = generate_avatar(letter, color, size, scale)
# Clip avatar
clip_setting = app.config.get('avatar_clipping')
if clip_setting == 'circle':
return clip_circle(surface)
if clip_setting == 'rounded_corners':
return clip_rounded_corners(surface)
return surface
def get_avatar_from_storage(self, filename, size=None, scale=None, publish=False):
if filename is None or '':
return
......
......@@ -231,7 +231,7 @@ class PrivateChatControl(ChatControl):
scale = self.parent_win.window.get_scale_factor()
surface = app.interface.get_avatar(
self.gc_contact.avatar_sha, AvatarSize.CHAT, scale)
self.gc_contact, AvatarSize.CHAT, scale)
image = self.xml.get_object('avatar_image')
if surface is None:
image.set_from_icon_name('avatar-default', Gtk.IconSize.DIALOG)
......
......@@ -918,7 +918,7 @@ class GajimRemote(Server):
if sha is None:
return
app.config.set_per('accounts', self.name, 'avatar_sha', sha)
data = app.interface.get_avatar(sha, publish=True)
data = app.interface.get_avatar_from_storage(sha, publish=True)
avatar = base64.b64encode(data).decode('utf-8')
avatar_mime_type = mimetypes.guess_type(picture)[0]
vcard = {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment