Daniel Brötzmann pushed to branch emoji-improvements at gajim / gajim
Commits: 9c8ae80e by wurstsalat at 2022-09-24T20:58:07+02:00 imprv: Emojis: Extract codepoints for all emoji variants
- - - - - 47dd30ab by wurstsalat at 2022-09-24T20:58:12+02:00 imprv: Avatars: Use PangoCairo for generate_avatar
Fixes #10960
- - - - - 86c8c93a by wurstsalat at 2022-09-24T20:58:12+02:00 chore: emoji_data: Remove unused method
- - - - -
6 changed files:
- gajim/gajim.py - gajim/gtk/avatar.py - gajim/gtk/emoji_data.py - gajim/gtk/emoji_data.pyi - gajim/gtk/emoji_data_gtk.py - gajim/gtk/workspace_dialog.py
Changes:
===================================== gajim/gajim.py ===================================== @@ -66,6 +66,7 @@ def _check_required_deps() -> None: 'GtkSource': '4', 'GObject': '2.0', 'Pango': '1.0', + 'PangoCairo': '1.0', 'Soup': '2.4'}) except ValueError as error: sys.exit('Missing dependency: %s' % error)
===================================== gajim/gtk/avatar.py ===================================== @@ -26,6 +26,8 @@
from gi.repository import Gdk from gi.repository import GdkPixbuf +from gi.repository import Pango +from gi.repository import PangoCairo import cairo from nbxmpp.protocol import JID
@@ -38,8 +40,7 @@ from gajim.common.const import StyleAttr
from .const import DEFAULT_WORKSPACE_COLOR -from .emoji_data import is_emoji -from .emoji_data import get_emoji_font +from .emoji_data_gtk import get_emoji_data from .util import load_icon_surface from .util import load_pixbuf from .util import text_to_color @@ -56,6 +57,23 @@ cairo.ImageSurface]]
+def generate_avatar_letter(text: str) -> str: + if not text: + return '' + + if text[0].isalpha(): + return text[0].upper() + + emoji_data = get_emoji_data() + + # Max (arbitrary) length for emoji ZJW sequences: 11 + for length in range(11, 0, -1): + if text[:length] in emoji_data.values(): + return text[:length] + + return text[0].upper() + + def generate_avatar(letters: str, color: tuple[float, float, float], size: int, @@ -70,7 +88,7 @@ def generate_avatar(letters: str,
width = size height = size - font_size = size * 0.5 + font_size = size * 0.4
# Set up surface surface = cairo.ImageSurface(cairo.Format.ARGB32, width, height) @@ -81,26 +99,25 @@ def generate_avatar(letters: str, context.fill()
# Draw letters - if is_emoji(letters): - font_face = get_emoji_font() - else: - font_face = 'sans-serif' - context.select_font_face(font_face, - cairo.FontSlant.NORMAL, - cairo.FontWeight.NORMAL) - context.set_font_size(font_size) - extends = context.text_extents(letters) - x_bearing = extends.x_bearing - y_bearing = extends.y_bearing - ex_width = extends.width - ex_height = extends.height - - x_pos = width / 2 - (ex_width / 2 + x_bearing) - y_pos = height / 2 - (ex_height / 2 + y_bearing) + layout = PangoCairo.create_layout(context) + layout.set_text(generate_avatar_letter(letters)) + + description = Pango.FontDescription() + description.set_family('Sans') + description.set_size(int(font_size * Pango.SCALE)) + + layout.set_font_description(description) + + _ink_rect, logical_rect = layout.get_extents() + layout_width = logical_rect.width / Pango.SCALE + layout_height = logical_rect.height / Pango.SCALE + x_pos = (width - layout_width) / 2 + y_pos = (height - layout_height) / 2 + 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) + PangoCairo.show_layout(context, layout)
return context.get_target() # pyright: ignore
@@ -399,7 +416,7 @@ def get_surface(self, name = contact.name color_string = str(contact.jid)
- letter = self._generate_letter(name) + letter = generate_avatar_letter(name) surface = generate_default_avatar( letter, color_string, size, scale, style=style) if show is not None: @@ -441,7 +458,7 @@ def get_muc_surface(self,
con = app.connections[account] name = get_groupchat_name(con, jid) - letter = self._generate_letter(name) + letter = generate_avatar_letter(name) surface = generate_default_avatar(letter, str(jid), size, scale, style) self._cache[jid][(size, scale, None)] = surface return surface @@ -469,9 +486,8 @@ def get_workspace_surface(self, app.settings.set_workspace_setting(workspace_id, 'avatar_sha', '')
rgba = make_rgba(color or DEFAULT_WORKSPACE_COLOR) - letter = name[:1].upper() surface = make_workspace_avatar( - letter, rgba_to_float(rgba), size, scale) + name, rgba_to_float(rgba), size, scale) self._cache[workspace_id][(size, scale, None)] = surface return surface
@@ -571,10 +587,3 @@ def _get_avatar_from_storage(self, if surface is None: return None return clip(surface, style) - - @staticmethod - def _generate_letter(name: str) -> str: - for letter in name: - if letter.isalpha(): - return letter.capitalize() - return name[0].capitalize()
===================================== gajim/gtk/emoji_data.py ===================================== @@ -17,7 +17,6 @@ from typing import Optional
import re -import sys import weakref from enum import IntEnum from collections import OrderedDict @@ -62,12 +61,6 @@ def get_emoji_pixbuf(codepoints: str) -> Optional[GdkPixbuf.Pixbuf]: return pixbuf
-def get_emoji_font() -> str: - if sys.platform == 'win32': - return 'Segoe UI Emoji' - return 'Noto Color Emoji' - - class EmojiData(OrderedDict): def __getitem__(self, key): try:
===================================== gajim/gtk/emoji_data.pyi ===================================== @@ -22,7 +22,6 @@ class Emoji(IntEnum): ...
def is_emoji(codepoints: str) -> bool: ... -def get_emoji_font() -> str: ... def get_emoji_pixbuf(codepoints: str) -> GdkPixbuf.Pixbuf: ...
class EmojiData(OrderedDict):
===================================== gajim/gtk/emoji_data_gtk.py ===================================== @@ -26,6 +26,40 @@
log = logging.getLogger('gajim.gui.emoji_data_gtk')
+SKIN_TONE_MODIFIERS = { + 127995: 'light skin tone', + 127996: 'medium-light skin tone', + 127997: 'medium skin tone', + 127998: 'medium-dark skin tone', + 127999: 'dark skin tone' +} + + +def generate_unicode_sequence(c_sequence: list[int]) -> str: + ''' + Generates a unicode sequence from a list of codepoints + ''' + u_sequence = '' + for codepoint in c_sequence: + u_sequence += chr(codepoint) + return u_sequence + + +def replace_skin_tone_placeholder(c_sequence: list[int], + modifier: int + ) -> list[int]: + + ''' + Replaces GTKs placeholder '0' for skin tone modifiers + with a given modifier + ''' + c_mod_sequence: list[int] = [] + for codepoint in c_sequence: + if codepoint == 0: + codepoint = modifier + c_mod_sequence.append(codepoint) + return c_mod_sequence +
def get_emoji_data() -> dict[str, str]: return emoji_data @@ -46,33 +80,57 @@ def load_emoji_data() -> dict[str, str]: # We have to store bytes_data in a variable to make sure Python # keeps it until it is processed. bytes_data = bytes_.get_data() - - def _pass(_user_data: Any) -> None: - pass - - variant = GLib.Variant.new_from_data( - GLib.VariantType('a(auss)'), - bytes_data, - True, - _pass) - - emoji_data_dict: dict[str, str] = {} - index = 0 - for _item in variant: - emoji = variant.get_child_value(index) - points = emoji.get_child_value(0) - codepoint = points.get_child_value(0).get_uint32() - shortcode = emoji.get_child_value(1).get_string() - emoji_data_dict[shortcode] = chr(codepoint) - index += 1 - - # Add commonly used shortcodes - emoji_data_dict['+1'] = '\U0001F44D' - emoji_data_dict['-1'] = '\U0001F44E' - return emoji_data_dict - - except GLib.Error: + except GLib.Error as err: + log.warning('Loading emoji resources failed: %s', err) return {}
- -emoji_data = load_emoji_data() + def _pass(_user_data: Any) -> None: + pass + + variant = GLib.Variant.new_from_data( + GLib.VariantType('a(auss)'), + bytes_data, + True, + _pass) + + emoji_data_dict: dict[str, str] = {} + for c_sequence, shortcode, _ in variant: + # Example item: + # ([128105, 0, 8205, 10084, 65039, 8205, 128104, 0], + # 'couple with heart: woman, man', + # 'couple'), + # GTK sets '0' as a placeholder for skin tone modifiers, see: + # https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gtk/emoji/ + # convert-emoji.c + + if 0 not in c_sequence: + # No skin tone modifiers present + u_sequence = generate_unicode_sequence(c_sequence) + emoji_data_dict[shortcode] = u_sequence + continue + + # Filter out 0 in order to generate basic (yellow) variation + c_basic_sequence = [c for c in c_sequence if c != 0] + u_sequence = generate_unicode_sequence(c_basic_sequence) + emoji_data_dict[shortcode] = u_sequence + + # Add variations with skin tone modifiers + for modifier, mod_shortcode in SKIN_TONE_MODIFIERS.items(): + new_shortcode = f'{shortcode}, {mod_shortcode}' + c_mod_sequence = replace_skin_tone_placeholder( + c_sequence, modifier) + u_mod_sequence = generate_unicode_sequence(c_mod_sequence) + emoji_data_dict[new_shortcode] = u_mod_sequence + + # Add commonly used shortcodes + emoji_data_dict['+1'] = '\U0001F44D' + emoji_data_dict['-1'] = '\U0001F44E' + + return emoji_data_dict + + +try: + emoji_data = load_emoji_data() +except Exception as err: + log.warning('Unable to load emoji data: %s', err) + emoji_data = {}
===================================== gajim/gtk/workspace_dialog.py ===================================== @@ -131,7 +131,7 @@ def _update_avatar(self) -> None: scale) else: surface = make_workspace_avatar( - name[:1].upper(), + name, rgba_to_float(rgba), AvatarSize.WORKSPACE_EDIT, scale)
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/a0bbc43286af3d804d69337267a89036...