From 69fcb22df26ff86ac01b7d5683ac71a0601162ea Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Sat, 22 Jan 2022 10:55:49 +0100 Subject: [PATCH] Use SixLabor.Fonts for rendering --- src/Greenshot.Editor/Controls/EmojiControl.cs | 23 + src/Greenshot.Editor/Controls/EmojiData.cs | 281 + .../Controls/EmojiPicker.xaml | 129 + .../Controls/EmojiPicker.xaml.cs | 123 + .../Drawing/EmojiContainer.cs | 24 +- src/Greenshot.Editor/Drawing/EmojiRenderer.cs | 86 + src/Greenshot.Editor/Greenshot.Editor.csproj | 11 +- .../Resources/TwemojiMozilla.ttf | Bin 0 -> 1437964 bytes src/Greenshot.Editor/Resources/emoji-test.txt | 4879 +++++++++++++++++ src/SixLabors.Fonts.dll | Bin 0 -> 335872 bytes src/SixLabors.ImageSharp.Drawing.dll | Bin 0 -> 175104 bytes 11 files changed, 5536 insertions(+), 20 deletions(-) create mode 100644 src/Greenshot.Editor/Controls/EmojiControl.cs create mode 100644 src/Greenshot.Editor/Controls/EmojiData.cs create mode 100644 src/Greenshot.Editor/Controls/EmojiPicker.xaml create mode 100644 src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs create mode 100644 src/Greenshot.Editor/Drawing/EmojiRenderer.cs create mode 100644 src/Greenshot.Editor/Resources/TwemojiMozilla.ttf create mode 100644 src/Greenshot.Editor/Resources/emoji-test.txt create mode 100644 src/SixLabors.Fonts.dll create mode 100644 src/SixLabors.ImageSharp.Drawing.dll diff --git a/src/Greenshot.Editor/Controls/EmojiControl.cs b/src/Greenshot.Editor/Controls/EmojiControl.cs new file mode 100644 index 000000000..d9667a080 --- /dev/null +++ b/src/Greenshot.Editor/Controls/EmojiControl.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using Greenshot.Editor.Drawing; + +namespace Greenshot.Editor.Controls +{ + internal class EmojiControl : Image + { + public static readonly DependencyProperty EmojiProperty = DependencyProperty.Register("Emoji", typeof(string), typeof(EmojiControl), new PropertyMetadata(default(string), PropertyChangedCallback)); + + private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((EmojiControl)d).Source = EmojiRenderer.GetBitmapSource((string)e.NewValue, 48); + } + + public string Emoji + { + get { return (string)GetValue(EmojiProperty); } + set { SetValue(EmojiProperty, value); } + } + } +} diff --git a/src/Greenshot.Editor/Controls/EmojiData.cs b/src/Greenshot.Editor/Controls/EmojiData.cs new file mode 100644 index 000000000..e1ae58e4e --- /dev/null +++ b/src/Greenshot.Editor/Controls/EmojiData.cs @@ -0,0 +1,281 @@ +// +// Emoji.Wpf — Emoji support for WPF +// +// Copyright © 2017—2021 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Greenshot.Editor.Controls +{ + public static class EmojiData + { + public static IEnumerable AllEmoji + => from g in AllGroups + from e in g.EmojiList + select e; + + public static IList AllGroups { get; private set; } + + public static IDictionary LookupByText { get; private set; } + = new Dictionary(); + public static IDictionary LookupByName { get; private set; } + = new Dictionary(); + + public static Regex MatchOne { get; private set; } + public static HashSet MatchStart { get; private set; } + = new HashSet(); + + // FIXME: should we lazy load this? If the user calls Load() later, then + // this first Load() call will have been for nothing. + static EmojiData() => Load(); + + public static void Load() + { + ParseEmojiList(); + } + + public class Emoji + { + public string Name { get; set; } + public string Text { get; set; } + public bool HasVariations => VariationList.Count > 0; + + public Group Group => SubGroup.Group; + public SubGroup SubGroup; + + public IList VariationList { get; } = new List(); + } + + public class SubGroup + { + public string Name { get; set; } + public Group Group; + + public IList EmojiList { get; } = new List(); + } + + public class Group + { + public string Name { get; set; } + public string Icon => SubGroups.FirstOrDefault()?.EmojiList.FirstOrDefault()?.Text; + + public IList SubGroups { get; } = new List(); + + public int EmojiCount + => SubGroups.Select(s => s.EmojiList.Count).Sum(); + + public IEnumerable> EmojiChunkList + => new ChunkHelper(EmojiList, 8); + + public IEnumerable EmojiList + => from s in SubGroups + from e in s.EmojiList + select e; + } + + private static string m_match_one_string; + + // FIXME: this could be read directly from emoji-test.txt.gz + private static List SkinToneComponents = new List + { + "🏻", // light skin tone + "🏼", // medium-light skin tone + "🏽", // medium skin tone + "🏾", // medium-dark skin tone + "🏿", // dark skin tone + }; + + private static List HairStyleComponents = new List + { + "🦰", // red hair + "🦱", // curly hair + "🦳", // white hair + "🦲", // bald + }; + + private static string ToColonSyntax(string s) + => Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-"); + + private static void ParseEmojiList() + { + var match_group = new Regex(@"^# group: (.*)"); + var match_subgroup = new Regex(@"^# subgroup: (.*)"); + var match_sequence = new Regex(@"^([0-9a-fA-F ]+[0-9a-fA-F]).*; *([-a-z]*) *# [^ ]* (E[0-9.]* )?(.*)"); + var match_skin_tone = new Regex($"({string.Join("|", SkinToneComponents)})"); + var match_hair_style = new Regex($"({string.Join("|", HairStyleComponents)})"); + + var adult = "(👨|👩)(🏻|🏼|🏽|🏾|🏿)?"; + var child = "(👦|👧|👶)(🏻|🏼|🏽|🏾|🏿)?"; + var match_family = new Regex($"{adult}(\u200d{adult})*(\u200d{child})+"); + + var qualified_lut = new Dictionary(); + var list = new List(); + var alltext = new List(); + + Group current_group = null; + SubGroup current_subgroup = null; + + foreach (var line in EmojiDescriptionLines()) + { + var m = match_group.Match(line); + if (m.Success) + { + current_group = new Group { Name = m.Groups[1].ToString() }; + list.Add(current_group); + continue; + } + + m = match_subgroup.Match(line); + if (m.Success) + { + current_subgroup = new SubGroup { Name = m.Groups[1].ToString(), Group = current_group }; + current_group.SubGroups.Add(current_subgroup); + continue; + } + + m = match_sequence.Match(line); + if (m.Success) + { + string sequence = m.Groups[1].ToString(); + string name = m.Groups[4].ToString(); + + string text = string.Join("", from n in sequence.Split(' ') + select char.ConvertFromUtf32(Convert.ToInt32(n, 16))); + bool has_modifier = false; + + if (match_family.Match(text).Success) + { + // If this is a family emoji, no need to add it to our big matching + // regex, since the match_family regex is already included. + } + else + { + // Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big + // regex so that we can match all variations of this Emoji even if they are + // not in the standard. + bool has_nonfirst_modifier = false; + var regex_text = match_skin_tone.Replace( + match_hair_style.Replace(text, (x) => + { + has_modifier = true; + has_nonfirst_modifier |= x.Value != HairStyleComponents[0]; + return match_hair_style.ToString(); + }), (x) => + { + has_modifier = true; + has_nonfirst_modifier |= x.Value != SkinToneComponents[0]; + return match_skin_tone.ToString(); + }); + + if (!has_nonfirst_modifier) + alltext.Add(has_modifier ? regex_text : text); + } + + // If there is already a differently-qualified version of this character, skip it. + // FIXME: this only works well if fully-qualified appears first in the list. + var unqualified = text.Replace("\ufe0f", ""); + if (qualified_lut.ContainsKey(unqualified)) + continue; + + qualified_lut[unqualified] = text; + + var emoji = new Emoji + { + Name = name, + Text = text, + SubGroup = current_subgroup, + }; + // FIXME: this prevents LookupByText from working with the unqualified version + LookupByText[text] = emoji; + LookupByName[ToColonSyntax(name)] = emoji; + MatchStart.Add(text[0]); + + // Get the left part of the name and check whether we’re a variation of an existing + // emoji. If so, append to that emoji. Otherwise, add to current subgroup. + // FIXME: does not work properly because variations can appear before the generic emoji + if (name.Contains(":") && LookupByName.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parent_emoji)) + { + if (parent_emoji.VariationList.Count == 0) + parent_emoji.VariationList.Add(parent_emoji); + parent_emoji.VariationList.Add(emoji); + } + else + current_subgroup.EmojiList.Add(emoji); + } + } + + // Remove the Component group. Not sure we want to have the skin tones in the picker. + list.RemoveAll(g => g.Name == "Component"); + AllGroups = list; + + // Make U+fe0f optional in the regex so that we can match any combination. + // FIXME: this is the starting point to implement variation selectors. + var sortedtext = alltext.OrderByDescending(x => x.Length); + var match_other = string.Join("|", sortedtext) + .Replace("*", "[*]") + .Replace("\ufe0f", "\ufe0f?"); + + // Build a regex that matches any Emoji + m_match_one_string = match_family.ToString() + "|" + match_other; + MatchOne = new Regex("(" + m_match_one_string + ")"); + } + + private static IEnumerable EmojiDescriptionLines() + { + using var stream = Assembly.GetCallingAssembly().GetManifestResourceStream("Greenshot.Editor.Resources.emoji-test.txt"); + using var streamReader = new StreamReader(stream); + return streamReader.ReadToEnd().Split('\r', '\n'); + } + } + + sealed class ChunkHelper : IEnumerable> + { + public ChunkHelper(IEnumerable elements, int size) + { + m_elements = elements; + m_size = size; + } + + public IEnumerator> GetEnumerator() + { + using (var enumerator = m_elements.GetEnumerator()) + { + m_has_more = enumerator.MoveNext(); + while (m_has_more) + yield return GetNextBatch(enumerator).ToList(); + } + } + + private IEnumerable GetNextBatch(IEnumerator enumerator) + { + for (int i = 0; i < m_size; ++i) + { + yield return enumerator.Current; + m_has_more = enumerator.MoveNext(); + if (!m_has_more) + yield break; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private readonly IEnumerable m_elements; + private readonly int m_size; + private bool m_has_more; + } +} + diff --git a/src/Greenshot.Editor/Controls/EmojiPicker.xaml b/src/Greenshot.Editor/Controls/EmojiPicker.xaml new file mode 100644 index 000000000..c24961597 --- /dev/null +++ b/src/Greenshot.Editor/Controls/EmojiPicker.xaml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs b/src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs new file mode 100644 index 000000000..fc255b4a5 --- /dev/null +++ b/src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs @@ -0,0 +1,123 @@ +// +// Emoji.Wpf — Emoji support for WPF +// +// Copyright © 2017—2021 Sam Hocevar +// +// This program is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +namespace Greenshot.Editor.Controls +{ + public class EmojiPickedEventArgs : EventArgs + { + public EmojiPickedEventArgs() { } + public EmojiPickedEventArgs(string emoji) => Emoji = emoji; + + public string Emoji; + } + + public delegate void EmojiPickedEventHandler(object sender, EmojiPickedEventArgs e); + + /// + /// Interaction logic for Picker.xaml + /// + public partial class EmojiPicker : StackPanel + { + public EmojiPicker() + { + InitializeComponent(); + } + + public IList EmojiGroups => EmojiData.AllGroups; + + // Backwards compatibility for when the backend was a TextBlock. + public double FontSize + { + get => Image.Height * 0.75; + set => Image.Height = value / 0.75; + } + + public event PropertyChangedEventHandler SelectionChanged; + + public event EmojiPickedEventHandler Picked; + + private static void OnSelectionPropertyChanged(DependencyObject source, + DependencyPropertyChangedEventArgs e) + { + (source as EmojiPicker)?.OnSelectionChanged(e.NewValue as string); + } + + public string Selection + { + get => (string)GetValue(SelectionProperty); + set => SetValue(SelectionProperty, value); + } + + private void OnSelectionChanged(string s) + { + var is_disabled = string.IsNullOrEmpty(s); + Image.Emoji = is_disabled ? "???" : s; + Image.Opacity = is_disabled ? 0.3 : 1.0; + SelectionChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Selection))); + } + + private void OnEmojiPicked(object sender, RoutedEventArgs e) + { + if (sender is Control control && control.DataContext is EmojiData.Emoji emoji) + { + if (emoji.VariationList.Count == 0 || sender is Button) + { + Selection = emoji.Text; + Button_INTERNAL.IsChecked = false; + e.Handled = true; + Picked?.Invoke(this, new EmojiPickedEventArgs(Selection)); + } + } + } + + public static readonly DependencyProperty SelectionProperty = DependencyProperty.Register( + nameof(Selection), typeof(string), typeof(EmojiPicker), + new FrameworkPropertyMetadata("☺", OnSelectionPropertyChanged)); + + private void OnPopupKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape && sender is Popup popup) + { + popup.IsOpen = false; + e.Handled = true; + } + } + + private void OnPopupLoaded(object sender, RoutedEventArgs e) + { + if (!(sender is Popup popup)) + return; + + var child = popup.Child; + IInputElement old_focus = null; + child.Focusable = true; + child.IsVisibleChanged += (o, ea) => + { + if (child.IsVisible) + { + old_focus = Keyboard.FocusedElement; + Keyboard.Focus(child); + } + }; + + popup.Closed += (o, ea) => Keyboard.Focus(old_focus); + } + } +} diff --git a/src/Greenshot.Editor/Drawing/EmojiContainer.cs b/src/Greenshot.Editor/Drawing/EmojiContainer.cs index 19e232afe..bbfdf1990 100644 --- a/src/Greenshot.Editor/Drawing/EmojiContainer.cs +++ b/src/Greenshot.Editor/Drawing/EmojiContainer.cs @@ -30,9 +30,9 @@ using System.Windows.Forms; using System.Windows.Forms.Integration; using System.Windows.Media; using System.Windows.Media.Imaging; -using Emoji.Wpf; using Greenshot.Base.Core; using Greenshot.Base.Interfaces.Drawing; +using Greenshot.Editor.Controls; using Greenshot.Editor.Helpers; using Image = System.Drawing.Image; using Matrix = System.Drawing.Drawing2D.Matrix; @@ -48,7 +48,7 @@ namespace Greenshot.Editor.Drawing { [NonSerialized] private static EmojiContainer _currentContainer; [NonSerialized] private static ElementHost _emojiPickerHost; - [NonSerialized] private static Picker _emojiPicker; + [NonSerialized] private static EmojiPicker _emojiPicker; [NonSerialized] private bool _justCreated = true; [NonSerialized] private Image _cachedImage = null; @@ -106,7 +106,7 @@ namespace Greenshot.Editor.Drawing _emojiPickerHost = _parent.Controls.Find("EmojiPickerHost", false).OfType().FirstOrDefault(); if (_emojiPickerHost == null) { - _emojiPicker = new Picker(); + _emojiPicker = new EmojiPicker(); _emojiPicker.Picked += (_, args) => { _currentContainer.Emoji = args.Emoji; @@ -207,19 +207,7 @@ namespace Greenshot.Editor.Drawing private Image ComputeBitmap(int iconSize) { - // Create WPF control that will be used to render the emoji - var image = new System.Windows.Controls.Image(); - global::Emoji.Wpf.Image.SetSource(image, Emoji); - - image.RenderTransformOrigin = new System.Windows.Point(0.5, 0.5); - image.RenderTransform = new RotateTransform(_rotationAngle); - image.Measure(new Size(iconSize, iconSize)); - image.Arrange(new Rect(0, 0, iconSize, iconSize)); - - var renderTargetBitmap = new RenderTargetBitmap(iconSize, iconSize, 96, 96, PixelFormats.Pbgra32); - renderTargetBitmap.Render(image); - - return renderTargetBitmap.ToBitmap(); + return EmojiRenderer.GetBitmap(Emoji, iconSize); } private void ResetCachedBitmap() @@ -231,9 +219,9 @@ namespace Greenshot.Editor.Drawing internal static class PickerExtensions { - public static void ShowPopup(this Picker picker, bool show) + public static void ShowPopup(this EmojiPicker emojiPicker, bool show) { - foreach (var child in picker.Children) + foreach (var child in emojiPicker.Children) { if (child is ToggleButton button) { diff --git a/src/Greenshot.Editor/Drawing/EmojiRenderer.cs b/src/Greenshot.Editor/Drawing/EmojiRenderer.cs new file mode 100644 index 000000000..d36bbab22 --- /dev/null +++ b/src/Greenshot.Editor/Drawing/EmojiRenderer.cs @@ -0,0 +1,86 @@ +using System.Drawing; +using System.IO; +using System.Reflection; +using System.Windows.Media.Imaging; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using FontStyle = SixLabors.Fonts.FontStyle; +using Image = System.Drawing.Image; + +namespace Greenshot.Editor.Drawing +{ + internal static class EmojiRenderer + { + private static SixLabors.Fonts.FontFamily? _fontFamily; + + public static Image GetImage(string emoji, int iconSize) + { + if (_fontFamily == null) + { + using var stream = Assembly.GetCallingAssembly().GetManifestResourceStream("Greenshot.Editor.Resources.TwemojiMozilla.ttf"); + var fontCollection = new FontCollection(); + fontCollection.Add(stream); + if (fontCollection.TryGet("Twemoji Mozilla", out var fontFamily)) + { + _fontFamily = fontFamily; + } + } + + var font = _fontFamily.Value.CreateFont(iconSize, FontStyle.Regular); + var image = new Image(iconSize, iconSize); + + var textOptions = new TextOptions(font) { Origin = new SixLabors.ImageSharp.PointF(0, iconSize / 2.0f), HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center }; + + image.Mutate(x => x.DrawText(textOptions, emoji, SixLabors.ImageSharp.Color.Black)); + return image; + } + + public static Image GetBitmap(string emoji, int iconSize) + { + using var image = GetImage(emoji, iconSize); + return image.ToBitmap(); + } + + public static BitmapSource GetBitmapSource(string emoji, int iconSize) + { + using var image = GetImage(emoji, iconSize); + return image.ToBitmapSource(); + } + + public static Bitmap ToBitmap(this Image image) + { + using var memoryStream = new MemoryStream(); + + var imageEncoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance); + image.Save(memoryStream, imageEncoder); + + memoryStream.Seek(0, SeekOrigin.Begin); + + return new Bitmap(memoryStream); + } + + public static BitmapSource ToBitmapSource(this Image image) + { + using var memoryStream = new MemoryStream(); + + var imageEncoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance); + image.Save(memoryStream, imageEncoder); + + memoryStream.Seek(0, SeekOrigin.Begin); + + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = memoryStream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + return bitmap; + } + } +} diff --git a/src/Greenshot.Editor/Greenshot.Editor.csproj b/src/Greenshot.Editor/Greenshot.Editor.csproj index 601a9f38d..da3979874 100644 --- a/src/Greenshot.Editor/Greenshot.Editor.csproj +++ b/src/Greenshot.Editor/Greenshot.Editor.csproj @@ -5,7 +5,7 @@ - + @@ -83,9 +83,16 @@ ImageEditorForm.cs + + - + + ..\SixLabors.Fonts.dll + + + ..\SixLabors.ImageSharp.Drawing.dll + \ No newline at end of file diff --git a/src/Greenshot.Editor/Resources/TwemojiMozilla.ttf b/src/Greenshot.Editor/Resources/TwemojiMozilla.ttf new file mode 100644 index 0000000000000000000000000000000000000000..75ddc213ff8fc6d84167f7bb9d1b787ce40160a4 GIT binary patch literal 1437964 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 z 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